Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
package wgengine
import ( "context" crand "crypto/rand" "crypto/x509" "errors" "fmt" "io" "math" "net/netip" "runtime" "slices" "sync" "sync/atomic" "time"
"github.com/gaissmai/bart" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun" "go4.org/mem" "tailscale.com/control/controlknobs" "tailscale.com/drive" "tailscale.com/envknob" "tailscale.com/feature" "tailscale.com/feature/buildfeatures" "tailscale.com/health" "tailscale.com/ipn/ipnstate" "tailscale.com/net/dns" "tailscale.com/net/dns/resolver" "tailscale.com/net/ipset" "tailscale.com/net/netmon" "tailscale.com/net/packet" "tailscale.com/net/routemanager" "tailscale.com/net/sockstats" "tailscale.com/net/tsdial" "tailscale.com/net/tstun" "tailscale.com/syncs" "tailscale.com/tailcfg" "tailscale.com/tstime/mono" "tailscale.com/types/dnstype" "tailscale.com/types/events" "tailscale.com/types/ipproto" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/views" "tailscale.com/util/checkchange" "tailscale.com/util/clientmetric" "tailscale.com/util/eventbus" "tailscale.com/util/execqueue" "tailscale.com/util/mak" "tailscale.com/util/testenv" "tailscale.com/util/usermetric" "tailscale.com/version" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/magicsock" "tailscale.com/wgengine/netstack/gro" "tailscale.com/wgengine/router" "tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgint" "tailscale.com/wgengine/wglog")
type userspaceEngine struct { // eventBus will eventually become required, but for now may be nil. eventBus *eventbus.Bus eventClient *eventbus.Client
// connReject is storage for the optional connection-rejection // callback installed via [SetConnRejectCallback]. The type is // defined per build tag (see connreject.go / connreject_stub.go) // so this always-built file does not reference // tailscale.com/net/connreject. connReject connRejectState
linkChangeQueue execqueue.ExecQueue
logf logger.Logf wgLogger *wglog.Logger // a wireguard-go logging wrapper reqCh chan struct{} waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool timeNow func() mono.Time tundev *tstun.Wrapper wgdev *device.Device router router.Router dialer *tsdial.Dialer confListenPort uint16 // original conf.ListenPort dns *dns.Manager magicConn *magicsock.Conn netMon *netmon.Monitor health *health.Tracker netMonOwned bool // whether we created netMon (and thus need to close it) controlKnobs *controlknobs.Knobs // or nil
// bird is the BIRD integration handle constructed via // [HookNewBird], or nil if [Config.BIRDSocket] was empty or the // feature/bird package is not linked into the binary. bird Bird
// isLocalAddr reports the whether an IP is assigned to the local // tunnel interface. It's used to reflect local packets // incorrectly sent to us. isLocalAddr syncs.AtomicValue[func(netip.Addr) bool]
// isDNSIPOverTailscale reports the whether a DNS resolver's IP // is being routed over Tailscale. isDNSIPOverTailscale syncs.AtomicValue[func(netip.Addr) bool]
wgLock sync.Mutex // serializes all wgdev operations; see lock order comment below
// peerForIPFn, if non-nil, is the callback installed via // [userspaceEngine.SetPeerForIPFunc]. peerForIP delegates to it // for the cold-path control lookups (Ping, TSMP, pendopen, etc). peerForIPFn atomic.Pointer[func(netip.Addr) (_ PeerForIP, ok bool)]
// peerConfigFn, if non-nil, is the live per-peer WireGuard config // source installed via [userspaceEngine.SetPeerConfigFunc]. When // set, wgdev's PeerLookupFunc queries it directly, so reconfigs // no longer install per-config lookup closures. peerConfigFn atomic.Pointer[func(key.NodePublic) (config wgcfg.PeerConfig, ok bool)]
lastCfg wgcfg.Config lastRouter *router.Config lastDNSConfig dns.ConfigView // or invalid if none reconfigureVPN func() error // or nil
// lastAppliedDisableTUNUDPGRO and lastAppliedDisableTUNTCPGRO cache the // controlknobs values that were last applied to the TUN device. They are // read and updated under e.mu and only consulted when buildfeatures.HasGRO // is true. Note: wireguard-go's GRO disablement is one-way (sticky), so // transitions from disabled back to enabled require a client restart. lastAppliedDisableTUNUDPGRO bool lastAppliedDisableTUNTCPGRO bool
mu sync.Mutex // guards following; see lock order comment below selfNode tailcfg.NodeView // or invalid if none closing bool // Close was called (even if we're still closing) statusCallback StatusCallback endpoints []tailcfg.Endpoint pendOpen map[flowtrackTuple]*pendingOpenFlow // see pendopen.go
// pongCallback is the map of response handlers waiting for disco or TSMP // pong callbacks. The map key is a random slice of bytes. pongCallback map[[8]byte]func(packet.TSMPPongReply) // icmpEchoResponseCallback is the map of response handlers waiting for ICMP // echo responses. The map key is a random uint32 that is the little endian // value of the ICMP identifier and sequence number concatenated. icmpEchoResponseCallback map[uint32]func()
// netlogger is the network flow logger handle constructed via // [HookNewNetLogger], or nil if the feature/netlog package is not // linked into the binary. netlogger NetLogger
// netLogSource is the [NetLogSource] installed via // [Engine.SetNetLogSource]; the netlogger reads it on each // Reconfig via [NetLoggerDeps.Source]. It may be nil if no source // was installed. netLogSource syncs.AtomicValue[NetLogSource]
// wgPeerLookup is the lookup function installed via // [Engine.SetWGPeerLookup]; it is consulted by [wgLogger] to translate // wireguard-go peer references in log lines. It may be nil if no // lookup was installed, in which case peer references are not // rewritten. wgPeerLookup syncs.AtomicValue[func(wgString string) (tsString string, ok bool)]
// Lock ordering: magicsock.Conn.mu, wgLock, then mu.}
// Config is the engine configuration.type Config struct { // Tun is the device used by the Engine to exchange packets with // the OS. // If nil, a fake Device that does nothing is used. Tun tun.Device
// IsTAP is whether Tun is actually a TAP (Layer 2) device that'll // require ethernet headers. IsTAP bool
// Router interfaces the Engine to the OS network stack. // If nil, a fake Router that does nothing is used. Router router.Router
// DNS interfaces the Engine to the OS DNS resolver configuration. // If nil, a fake OSConfigurator that does nothing is used. DNS dns.OSConfigurator
// ReconfigureVPN provides an optional hook for platforms like Android to // know when it's time to reconfigure their VPN implementation. Such // platforms can only set their entire VPN configuration (routes, DNS, etc) // at all once and can't make piecemeal incremental changes, so this // provides a hook to "flush" a batch of Router and/or DNS changes. ReconfigureVPN func() error
// NetMon optionally provides an existing network monitor to re-use. // If nil, a new network monitor is created. NetMon *netmon.Monitor
// HealthTracker, if non-nil, is the health tracker to use. HealthTracker *health.Tracker
// Metrics is the usermetrics registry to use. // Mandatory, if not set, an error is returned. Metrics *usermetric.Registry
// Dialer is the dialer to use for outbound connections. // If nil, a new Dialer is created. Dialer *tsdial.Dialer
// ExtraRootCAs, if non-nil, specifies additional trusted root CAs for TLS // connections (e.g. DERP). Passed through to magicsock. ExtraRootCAs *x509.CertPool
// DERPAppName, if non-empty, is an opaque app name string to // advertise to DERP servers for stats purposes. It is passed // through to magicsock. DERPAppName string
// ControlKnobs is the set of control plane-provied knobs // to use. // If nil, defaults are used. ControlKnobs *controlknobs.Knobs
// ListenPort is the port on which the engine will listen. // If zero, a port is automatically selected. ListenPort uint16
// RespondToPing determines whether this engine should internally // reply to ICMP pings, without involving the OS. // Used in "fake" mode for development. RespondToPing bool
// BIRDSocket, if non-empty, is the path of the BIRD unix socket to // configure whenever this node is a primary subnet router. It // requires the feature/bird package to be linked in. BIRDSocket string
// SetSubsystem, if non-nil, is called for each new subsystem created, just before a successful return. SetSubsystem func(any)
// DriveForLocal, if populated, will cause the engine to expose a Taildrive // listener at 100.100.100.100:8080. DriveForLocal drive.FileSystemForLocal
// EventBus, if non-nil, is used for event publication and subscription by // the Engine and its subsystems. // // TODO(creachadair): As of 2025-03-19 this is optional, but is intended to // become required non-nil. EventBus *eventbus.Bus
// ForceDiscoKey, if non-zero, forces the use of a specific disco // private key. This should only be used for special cases and // experiments, not for production. The recommended normal path is to // leave it zero, in which case a new disco key is generated per // Tailscale start and kept only in memory. ForceDiscoKey key.DiscoPrivate
// OnDERPRecv, if non-nil, is called for every non-disco packet // received from DERP before the peer map lookup. If it returns // true, the packet is considered handled and is not passed to // WireGuard. The pkt slice is borrowed and must be copied if // the callee needs to retain it. OnDERPRecv func(regionID tailcfg.DERPRegionID, src key.NodePublic, pkt []byte) (handled bool)}
// NewFakeUserspaceEngine returns a new userspace engine for testing.//// The opts may contain the following types://// - int or uint16: to set the ListenPort.func NewFakeUserspaceEngine(logf logger.Logf, opts ...any) (Engine, error) { conf := Config{ RespondToPing: true, } for _, o := range opts { switch v := o.(type) { case uint16: conf.ListenPort = v case int: if v < 0 || v > math.MaxUint16 { return nil, fmt.Errorf("invalid ListenPort: %d", v) } conf.ListenPort = uint16(v) case func(any): conf.SetSubsystem = v case *controlknobs.Knobs: conf.ControlKnobs = v case *health.Tracker: conf.HealthTracker = v case *usermetric.Registry: conf.Metrics = v case *eventbus.Bus: conf.EventBus = v default: return nil, fmt.Errorf("unknown option type %T", v) } } logf("Starting userspace WireGuard engine (with fake TUN device)") return NewUserspaceEngine(logf, conf)}
// NewUserspaceEngine creates the named tun device and returns a// Tailscale Engine running on it.func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) { var closePool closeOnErrorPool defer closePool.closeAllIfError(&reterr)
if testenv.InTest() && conf.HealthTracker == nil { panic("NewUserspaceEngine called without HealthTracker (being strict in tests)") }
if conf.Metrics == nil { return nil, errors.New("NewUserspaceEngine: opts.Metrics is required, please pass a *usermetric.Registry") }
if conf.Tun == nil { logf("[v1] using fake (no-op) tun device") conf.Tun = tstun.NewFake() } if conf.Router == nil { logf("[v1] using fake (no-op) OS network configurator") conf.Router = router.NewFake(logf) } if conf.DNS == nil { logf("[v1] using fake (no-op) DNS configurator") d, err := dns.NewNoopManager() if err != nil { return nil, err } conf.DNS = d } if conf.Dialer == nil { conf.Dialer = &tsdial.Dialer{Logf: logf} if conf.EventBus != nil { conf.Dialer.SetBus(conf.EventBus) } }
var tsTUNDev *tstun.Wrapper if conf.IsTAP { tsTUNDev = tstun.WrapTAP(logf, conf.Tun, conf.Metrics, conf.EventBus) } else { tsTUNDev = tstun.Wrap(logf, conf.Tun, conf.Metrics, conf.EventBus) } closePool.add(tsTUNDev)
rtr := conf.Router if version.IsMobile() { // Android and iOS don't handle large numbers of routes well, so we // wrap the Router with one that consolidates routes down to the // smallest number possible. // // On Android, too many routes at VPN configuration time result in an // android.os.TransactionTooLargeException because Android's VPNBuilder // tries to send the entire set of routes to the VPNService as a single // Bundle, which is typically limited to 1 MB. The number of routes // that's too much seems to be very roughly around 4000. // // On iOS, the VPNExtension is limited to only 50 MB of memory, so // keeping the number of routes down helps with memory consumption. rtr = router.ConsolidatingRoutes(logf, rtr) }
e := &userspaceEngine{ eventBus: conf.EventBus, timeNow: mono.Now, logf: logf, reqCh: make(chan struct{}, 1), waitCh: make(chan struct{}), tundev: tsTUNDev, router: rtr, dialer: conf.Dialer, confListenPort: conf.ListenPort, controlKnobs: conf.ControlKnobs, reconfigureVPN: conf.ReconfigureVPN, health: conf.HealthTracker, }
if buildfeatures.HasBird && conf.BIRDSocket != "" { newBird, ok := HookNewBird.GetOk() if !ok { return nil, errors.New("wgengine: Config.BIRDSocket set but the feature/bird package is not linked in") } bird, err := newBird(logf, conf.BIRDSocket) if err != nil { return nil, err } e.bird = bird } e.isLocalAddr.Store(ipset.FalseContainsIPFunc()) e.isDNSIPOverTailscale.Store(ipset.FalseContainsIPFunc())
if conf.NetMon != nil { e.netMon = conf.NetMon } else { mon, err := netmon.New(conf.EventBus, logf) if err != nil { return nil, err } closePool.add(mon) e.netMon = mon e.netMonOwned = true }
tunName, _ := conf.Tun.Name() conf.Dialer.SetTUNName(tunName) conf.Dialer.SetNetMon(e.netMon) conf.Dialer.SetBus(e.eventBus) e.dns = dns.NewManager(logf, conf.DNS, e.health, conf.Dialer, fwdDNSLinkSelector{e, tunName}, conf.ControlKnobs, runtime.GOOS, e.eventBus)
// TODO: there's probably a better place for this sockstats.SetNetMon(e.netMon)
logf("link state: %+v", e.netMon.InterfaceState())
endpointsFn := func(endpoints []tailcfg.Endpoint) { e.mu.Lock() e.endpoints = append(e.endpoints[:0], endpoints...) e.mu.Unlock()
e.RequestStatus() } magicsockOpts := magicsock.Options{ EventBus: e.eventBus, Logf: logf, Port: conf.ListenPort, EndpointsFunc: endpointsFn, DERPActiveFunc: e.RequestStatus, IdleFunc: e.tundev.IdleDuration, NetMon: e.netMon, HealthTracker: e.health, ExtraRootCAs: conf.ExtraRootCAs, DERPAppName: conf.DERPAppName, Metrics: conf.Metrics, ControlKnobs: conf.ControlKnobs, PeerByKeyFunc: e.PeerByKey, ForceDiscoKey: conf.ForceDiscoKey, OnDERPRecv: conf.OnDERPRecv, } var err error e.magicConn, err = magicsock.NewConn(magicsockOpts) if err != nil { return nil, fmt.Errorf("wgengine: %v", err) } closePool.add(e.magicConn) e.magicConn.SetNetworkUp(e.netMon.InterfaceState().AnyInterfaceUp())
if newNetLogger, ok := HookNewNetLogger.GetOk(); ok { e.netlogger = newNetLogger(NetLoggerDeps{ Logf: logf, Tun: e.tundev, Sock: e.magicConn, NetMon: e.netMon, Health: e.health, Bus: e.eventBus, Source: e.netLogSource.Load, }) }
tsTUNDev.SetDiscoKey(e.magicConn.DiscoPublicKey())
if conf.RespondToPing { e.tundev.PostFilterPacketInboundFromWireGuard = echoRespondToAll } e.tundev.PreFilterPacketOutboundToWireGuardEngineIntercept = e.handleLocalPackets
if buildfeatures.HasDebug && envknob.BoolDefaultTrue("TS_DEBUG_CONNECT_FAILURES") { if e.tundev.PreFilterPacketInboundFromWireGuard != nil { return nil, errors.New("unexpected PreFilterIn already set") } e.tundev.PreFilterPacketInboundFromWireGuard = e.trackOpenPreFilterIn if e.tundev.PostFilterPacketOutboundToWireGuard != nil { return nil, errors.New("unexpected PostFilterOut already set") } e.tundev.PostFilterPacketOutboundToWireGuard = e.trackOpenPostFilterOut }
e.wgLogger = wglog.NewLogger(logf, func(wgString string) (tsString string, ok bool) { fn := e.wgPeerLookup.Load() if fn == nil { return "", false } return fn(wgString) }) e.tundev.OnTSMPPongReceived = func(pong packet.TSMPPongReply) { e.mu.Lock() defer e.mu.Unlock() cb := e.pongCallback[pong.Data] e.logf("wgengine: got TSMP pong %02x, peerAPIPort=%v; cb=%v", pong.Data, pong.PeerAPIPort, cb != nil) if cb != nil { delete(e.pongCallback, pong.Data) go cb(pong) } }
e.tundev.OnICMPEchoResponseReceived = func(p *packet.Parsed) bool { idSeq := p.EchoIDSeq() e.mu.Lock() defer e.mu.Unlock() cb := e.icmpEchoResponseCallback[idSeq] if cb == nil { // We didn't swallow it, so let it flow to the host. return false } delete(e.icmpEchoResponseCallback, idSeq) e.logf("wgengine: got diagnostic ICMP response %02x", idSeq) go cb() return true }
// wgdev takes ownership of tundev, will close it when closed. e.logf("Creating WireGuard device...") e.wgdev = wgcfg.NewDevice(e.tundev, e.magicConn.Bind(), e.wgLogger.DeviceLogger) closePool.addFunc(e.wgdev.Close) closePool.addFunc(func() { if err := e.magicConn.Close(); err != nil { e.logf("error closing magicconn: %v", err) } })
go func() { up := false for event := range e.tundev.EventsUpDown() { if event&tun.EventUp != 0 && !up { e.logf("external route: up") e.RequestStatus() up = true } if event&tun.EventDown != 0 && up { e.logf("external route: down") e.RequestStatus() up = false } } }()
go func() { select { case <-e.wgdev.Wait(): e.mu.Lock() closing := e.closing e.mu.Unlock() if !closing { e.logf("Closing the engine because the WireGuard device has been closed...") e.Close() } case <-e.waitCh: // continue } }()
e.logf("Bringing WireGuard device up...") if err := e.wgdev.Up(); err != nil { return nil, fmt.Errorf("wgdev.Up: %w", err) } e.logf("Bringing router up...") if err := e.router.Up(); err != nil { return nil, fmt.Errorf("router.Up: %w", err) } tsTUNDev.SetLinkFeaturesPostUp(e.controlKnobs) if buildfeatures.HasGRO && runtime.GOOS == "linux" && e.controlKnobs != nil { // Seed the cached "last applied" TUN GRO knob values so the first // netmap update doesn't spuriously call ApplyGROKnobs: // SetLinkFeaturesPostUp above already applied these same values. We // only do this on Linux because ApplyGROKnobs is a no-op elsewhere. e.lastAppliedDisableTUNUDPGRO = e.controlKnobs.DisableTUNUDPGRO.Load() e.lastAppliedDisableTUNTCPGRO = e.controlKnobs.DisableTUNTCPGRO.Load() }
// It's a little pointless to apply no-op settings here (they // should already be empty?), but it at least exercises the // router implementation early on. e.logf("Clearing router settings...") if err := e.router.Set(nil); err != nil { return nil, fmt.Errorf("router.Set(nil): %w", err) } // Subscribe to network changes before starting the monitor, which // publishes nothing before Start, so no change can be missed. ec := e.eventBus.Client("userspaceEngine") eventbus.SubscribeFunc(ec, func(cd netmon.ChangeDelta) { if f, ok := feature.HookProxyInvalidateCache.GetOk(); ok { f() } e.linkChangeQueue.Add(func() { e.linkChange(&cd) }) })
e.logf("Starting network monitor...") e.netMon.Start()
if conf.SetSubsystem != nil { conf.SetSubsystem(e.tundev) conf.SetSubsystem(e.magicConn) conf.SetSubsystem(e.dns) conf.SetSubsystem(conf.Router) conf.SetSubsystem(conf.Dialer) conf.SetSubsystem(e.netMon) if conf.DriveForLocal != nil { conf.SetSubsystem(conf.DriveForLocal) } }
eventbus.SubscribeFunc(ec, func(update events.DiscoKeyAdvertisement) { e.logf("[v1] wgengine: got TSMP disco key advertisement from %v via eventbus", update.Src) if e.magicConn == nil { e.logf("wgengine: no magicConn") return }
pkt := packet.TSMPDiscoKeyAdvertisement{ Key: update.Key, } peer, ok := e.peerForIP(update.Src) if !ok { e.logf("wgengine: no peer found for %v", update.Src) return } e.magicConn.HandleDiscoKeyAdvertisement(peer.Node, pkt) }) e.eventClient = ec e.logf("Engine created.") return e, nil}
// echoRespondToAll is an inbound post-filter responding to all echo requests.func echoRespondToAll(p *packet.Parsed, t *tstun.Wrapper, gro *gro.GRO) (filter.Response, *gro.GRO) { if p.IsEchoRequest() { header := p.ICMP4Header() header.ToResponse() outp := packet.Generate(&header, p.Payload()) t.TryInjectOutbound(outp) // We already responded to it, but it's not an error. // Proceed with regular delivery. (Since this code is only // used in fake mode, regular delivery just means throwing // it away. If this ever gets run in non-fake mode, you'll // get double responses to pings, which is an indicator you // shouldn't be doing that I guess.) return filter.Accept, gro } return filter.Accept, gro}
// handleLocalPackets inspects packets coming from the local network// stack, and intercepts any packets that should be handled by// tailscaled directly. Other packets are allowed to proceed into the// main ACL filter.func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper) filter.Response { if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { isLocalAddr, ok := e.isLocalAddr.LoadOk() if !ok { e.logf("[unexpected] e.isLocalAddr was nil, can't check for loopback packet") } else if isLocalAddr(p.Dst.Addr()) { // macOS NetworkExtension directs packets destined to the // tunnel's local IP address into the tunnel, instead of // looping back within the kernel network stack. We have to // notice that an outbound packet is actually destined for // ourselves, and loop it back into macOS. t.InjectInboundCopy(p.Buffer()) metricReflectToOS.Add(1) return filter.Drop } } if runtime.GOOS == "plan9" { isLocalAddr, ok := e.isLocalAddr.LoadOk() if ok { if isLocalAddr(p.Dst.Addr()) { // On Plan9's "tun" equivalent, everything goes back in and out // the tun, even when the kernel's replying to itself. t.InjectInboundCopy(p.Buffer()) return filter.Drop } } }
return filter.Accept}
// SetPeerConfigFunc implements [Engine.SetPeerConfigFunc]. It stores// fn and installs a single wgdev PeerLookupFunc wrapping it, so// lazily-created peers always get their current config and the lookup// func never needs to be reinstalled as the peer set changes.func (e *userspaceEngine) SetPeerConfigFunc(fn func(key.NodePublic) (config wgcfg.PeerConfig, ok bool)) { if fn == nil { panic("SetPeerConfigFunc: nil fn") } e.peerConfigFn.Store(&fn) e.wgdev.SetPeerLookupFunc(wgcfg.NewPeerLookupFunc(e.wgdev.Bind(), e.logf, func(pubk device.NoisePublicKey) (wgcfg.PeerConfig, bool) { return fn(key.NodePublicFromRaw32(mem.B(pubk[:]))) }))}
// SyncDevicePeer implements [Engine.SyncDevicePeer].func (e *userspaceEngine) SyncDevicePeer(k key.NodePublic) { fn := e.peerConfigFn.Load() if fn == nil { return } e.wgLock.Lock() defer e.wgLock.Unlock() // The peer set may be about to change; drop the wgLogger's cached // peer-string rewrites so the next log line re-resolves them. e.wgLogger.Invalidate() conf, ok := (*fn)(k) if !ok { e.wgdev.RemovePeer(k.Raw32()) return } if peer, ok := e.wgdev.LookupActivePeer(k.Raw32()); ok { peer.SetPresharedKey(conf.PresharedKey) peer.SetAllowedIPs(conf.AllowedIPs) }}
// MarkDevicePeerForHandshake implements [Engine.MarkDevicePeerForHandshake].func (e *userspaceEngine) MarkDevicePeerForHandshake(k key.NodePublic) { e.wgLock.Lock() defer e.wgLock.Unlock() e.wgLogger.Invalidate() e.wgdev.ScheduleHandshakeOnUserSend(k.Raw32())}
// SetPeerByIPPacketFunc installs a callback used by wireguard-go to look up// which peer should handle an outbound packet by destination IP.//// LocalBackend's implementation consults both the exact node-address fast// path and the RouteManager's outbound table (covering subnet routes and// exit-node default routes), and stays correct under incremental netmap// deltas.//// A nil fn uninstalls the callback, reverting the device to its standard// WireGuard AllowedIPs trie, which only contains peers that already exist// in the device. Callers without a LocalBackend that need outbound packets// to lazily create peers must install their own callback.func (e *userspaceEngine) SetPeerByIPPacketFunc(fn func(netip.Addr) (_ key.NodePublic, ok bool)) { if fn == nil { e.wgdev.SetPeerByIPPacketFunc(nil) return } e.wgdev.SetPeerByIPPacketFunc(func(_, dst netip.Addr, _ []byte) (device.NoisePublicKey, bool) { if pk, ok := fn(dst); ok { return pk.Raw32(), true } return device.NoisePublicKey{}, false })}
func (e *userspaceEngine) SetPeerSessionStateFunc(fn func(key.NodePublic, PeerWireGuardState)) { e.wgdev.SetSessionStateFunc(func(pk device.NoisePublicKey, state device.PeerSessionState) { if fn != nil { fn(key.NodePublicFromRaw32(mem.B(pk[:])), peerWireGuardStateFromDevice(state)) } })}
// SetPeerPriorityMessageOnEstablishmentFunc registers a callback with a// [github.com/tailscale/wireguard-go/device.Device] to be triggered whenever// WireGuard establishes a new encryption keypair with an active peer, including// during periodic key rotation after approximately [device.RekeyAfterTime] of// activity.//// This callback must be cheap and must not call back into the// [github.com/tailscale/wireguard-go/device.Device]. The returned message must// not exceed [github.com/tailscale/wireguard-go/device.MaxPriorityMessageContentSize].func (e *userspaceEngine) SetPeerPriorityMessageOnEstablishmentFunc(fn func(key.NodePublic) (msg []byte)) { if fn != nil { e.wgdev.SetPriorityMessageOnEstablishmentFunc(func(pk device.NoisePublicKey) (msg []byte) { return fn(key.NodePublicFromRaw32(mem.B(pk[:]))) }) } else { e.wgdev.SetPriorityMessageOnEstablishmentFunc(nil) }}
// SetNetLogSource installs the [NetLogSource] consulted by the engine's// network flow logger.func (e *userspaceEngine) SetNetLogSource(src NetLogSource) { e.netLogSource.Store(src)}
// SetWGPeerLookup installs the lookup function used by the engine's// wireguard-go log wrapper to rewrite peer references in log lines.func (e *userspaceEngine) SetWGPeerLookup(fn func(wgString string) (tsString string, ok bool)) { e.wgPeerLookup.Store(fn)}
func peerWireGuardStateFromDevice(state device.PeerSessionState) PeerWireGuardState { switch state { case device.PeerSessionNone: return PeerWireGuardStateNone case device.PeerSessionHandshake: return PeerWireGuardStateHandshake case device.PeerSessionEstablished: return PeerWireGuardStateEstablished case device.PeerSessionExpired: return PeerWireGuardStateExpired default: panic(fmt.Sprintf("unexpected wireguard-go PeerSessionState %d", state)) }}
// ResetAndStop resets the engine to a clean state (like calling Reconfig// with all pointers to zero values) and returns the resulting status.//// Unlike Reconfig, it does not return ErrNoChanges.//// The returned status will not be sent to the registered status callback;// it is on the caller to ensure this status is handled appropriately.func (e *userspaceEngine) ResetAndStop() (*Status, error) { if err := e.Reconfig(&wgcfg.Config{}, &router.Config{}, &dns.Config{}); err != nil && !errors.Is(err, ErrNoChanges) { return nil, err } return e.getStatus()}
func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config) error { if routerCfg == nil { panic("routerCfg must not be nil") } if dnsCfg == nil { panic("dnsCfg must not be nil") }
e.isLocalAddr.Store(ipset.NewContainsIPFunc(views.SliceOf(routerCfg.LocalAddrs)))
e.wgLock.Lock() defer e.wgLock.Unlock() e.tundev.SetWGConfig(cfg)
e.mu.Lock() self := e.selfNode e.mu.Unlock()
listenPort := e.confListenPort if e.controlKnobs != nil && e.controlKnobs.RandomizeClientPort.Load() { listenPort = 0 }
peerMTUEnable := e.magicConn.ShouldPMTUD()
// Let the BIRD integration recompute whether this node is a // primary subnet router, before the early return below so that a // change in that state alone still reaches the protocol toggle in // ReconfigDone at the end. birdChanged := false if e.bird != nil { birdChanged = e.bird.Reconfig(self) }
engineChanged := !e.lastCfg.Equal(cfg) routerChanged := checkchange.Update(&e.lastRouter, routerCfg) dnsChanged := buildfeatures.HasDNS && !e.lastDNSConfig.Equal(dnsCfg.View()) if dnsChanged { e.lastDNSConfig = dnsCfg.View() }
listenPortChanged := listenPort != e.magicConn.LocalPort() peerMTUChanged := peerMTUEnable != e.magicConn.PeerMTUEnabled()
// Let the network flow logger react before the early return below, // so that logging identity changes take effect even when nothing // else changed, and before the router is configured, so that a // starting logger captures initial packets. netlogChanged := false if e.netlogger != nil { netlogChanged = e.netlogger.Reconfig(routerCfg, routerChanged) }
if !engineChanged && !routerChanged && !dnsChanged && !listenPortChanged && !birdChanged && !peerMTUChanged && !netlogChanged { return ErrNoChanges }
// TODO(bradfitz,danderson): maybe delete this isDNSIPOverTailscale // field and delete the resolver.ForwardLinkSelector hook and // instead have ipnlocal populate a map of DNS IP => linkName and // put that in the *dns.Config instead, and plumb it down to the // dns.Manager. Maybe also with isLocalAddr above. if buildfeatures.HasDNS { e.isDNSIPOverTailscale.Store(ipset.NewContainsIPFunc(views.SliceOf(dnsIPsOverTailscale(dnsCfg, routerCfg)))) }
if !e.lastCfg.PrivateKey.Equal(cfg.PrivateKey) { // Tell magicsock about the new (or initial) private key // (which is needed by DERP) before wgdev gets it, as wgdev // will start trying to handshake, which we want to be able to // go over DERP. if err := e.magicConn.SetPrivateKey(cfg.PrivateKey); err != nil { e.logf("wgengine: Reconfig: SetPrivateKey: %v", err) }
if err := e.wgdev.SetPrivateKey(key.NodePrivateAs[device.NoisePrivateKey](cfg.PrivateKey)); err != nil { e.logf("wgengine: Reconfig: wgdev.SetPrivateKey: %v", err) } }
e.lastCfg = *cfg.Clone()
e.magicConn.SetPreferredPort(listenPort) e.magicConn.UpdatePMTUD()
// Note: no wireguard-go device reconfig happens here. The device // learns its peer set from the live config source installed via // [Engine.SetPeerConfigFunc] (peers are lazily created and synced // per peer by [Engine.SyncDevicePeer]), and its private key is set // above when it changes.
// A router.Set error is recorded but must not abort the reconfig: DNS // configuration below must be attempted independently. See #20447. var routerErr error if routerChanged { e.logf("wgengine: Reconfig: configuring router") routerErr = e.router.Set(routerCfg) e.health.SetRouterHealth(routerErr) if routerErr != nil { e.logf("wgengine: Reconfig: router config failed (%v); continuing to DNS config so name resolution still works", routerErr) } }
// We've historically re-set DNS even after just a router change. While // refactoring in tailscale/tailscale#17448 and // tailscale/tailscale#17499, I'm erring on the side of keeping that // historical quirk for now (2025-10-08), lest it's load bearing in // unexpected ways // // TODO(bradfitz): try to do the "configuring DNS" part below only if // dnsChanged, not routerChanged. The "resolver.ShouldUseRoutes" part // probably needs to keep happening for both. var dnsErr, vpnErr error if buildfeatures.HasDNS && (routerChanged || dnsChanged) { if resolver.ShouldUseRoutes(e.controlKnobs) { e.logf("wgengine: Reconfig: user dialer") e.dialer.SetRoutes(routerCfg.Routes, routerCfg.LocalRoutes) } else { e.dialer.SetRoutes(nil, nil) }
// Keep DNS configuration after router configuration, as some // DNS managers refuse to apply settings if the device has no // assigned address. e.logf("wgengine: Reconfig: configuring DNS") dnsErr = e.dns.Set(*dnsCfg) e.health.SetDNSHealth(dnsErr) if dnsErr == nil { vpnErr = e.reconfigureVPNIfNecessary() } }
// Let the network flow logger finish reacting, pairing the Reconfig // call near the top of this function. This runs regardless of router // or DNS errors above: skipping it would leave a stopping logger // running until the next successful reconfig. // This may block to flush pending log messages. if e.netlogger != nil { e.netlogger.ReconfigDone() }
// Let the BIRD integration apply any protocol state change computed by // its Reconfig call above. As with the netlogger, this runs even if // router/DNS config failed, so BIRD's protocol state still tracks the // primary-subnet-router transition. if e.bird != nil { e.bird.ReconfigDone() }
e.logf("[v1] wgengine: Reconfig done") return errors.Join(routerErr, dnsErr, vpnErr)}
func (e *userspaceEngine) GetFilter() *filter.Filter { return e.tundev.GetFilter()}
func (e *userspaceEngine) SetFilter(filt *filter.Filter) { e.tundev.SetFilter(filt)}
func (e *userspaceEngine) GetJailedFilter() *filter.Filter { return e.tundev.GetJailedFilter()}
func (e *userspaceEngine) SetJailedFilter(filt *filter.Filter) { e.tundev.SetJailedFilter(filt)}
func (e *userspaceEngine) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) { e.tundev.SetPeerRoutes(native4, native6, routes)}
func (e *userspaceEngine) SetStatusCallback(cb StatusCallback) { e.mu.Lock() defer e.mu.Unlock() e.statusCallback = cb}
func (e *userspaceEngine) getStatusCallback() StatusCallback { e.mu.Lock() defer e.mu.Unlock() return e.statusCallback}
var ErrEngineClosing = errors.New("engine closing; no status")
func (e *userspaceEngine) PeerByKey(pubKey key.NodePublic) (_ wgint.Peer, ok bool) { e.wgLock.Lock() dev := e.wgdev e.wgLock.Unlock()
if dev == nil { return wgint.Peer{}, false } // Use LookupActivePeer (not LookupPeer) to avoid triggering on-demand // peer creation via PeerLookupFunc. PeerByKey is called from status // polling paths (getStatus, getPeerStatusLite) which iterate every peer // in the netmap; using LookupPeer would lazily create a wireguard-go // peer for every single netmap peer on each status poll, leaking // memory via per-peer queues and goroutines. peer, ok := dev.LookupActivePeer(pubKey.Raw32()) if !ok { return wgint.Peer{}, false } return wgint.PeerOf(peer), true}
func (e *userspaceEngine) getPeerStatusLite(pk key.NodePublic) (status ipnstate.PeerStatusLite, ok bool) { peer, ok := e.PeerByKey(pk) if !ok { return status, false } status.NodeKey = pk status.RxBytes = int64(peer.RxBytes()) status.TxBytes = int64(peer.TxBytes()) status.LastHandshake = peer.LastHandshake() return status, true}
func (e *userspaceEngine) getStatus() (*Status, error) { // Grab derpConns before acquiring wgLock to not violate lock ordering; // the DERPs method acquires magicsock.Conn.mu. // (See comment in userspaceEngine's declaration.) derpConns := e.magicConn.DERPs()
e.mu.Lock() closing := e.closing localAddrs := slices.Clone(e.endpoints) e.mu.Unlock()
if closing { return nil, ErrEngineClosing }
// Snapshot the set of active wgdev peers. wireguard-go has no // read-only iterator over its peer map; RemoveMatchingPeers with // a callback that always returns false is the cheap equivalent // (the callback can't itself call LookupActivePeer, though, as // RemoveMatchingPeers holds the wireguard device's mutex). var peerKeys []key.NodePublic e.wgdev.RemoveMatchingPeers(func(pk device.NoisePublicKey) bool { peerKeys = append(peerKeys, key.NodePublicFromRaw32(mem.B(pk[:]))) return false })
peers := make([]ipnstate.PeerStatusLite, 0, len(peerKeys)) for _, k := range peerKeys { if status, ok := e.getPeerStatusLite(k); ok { peers = append(peers, status) } }
return &Status{ AsOf: time.Now(), LocalAddrs: localAddrs, Peers: peers, DERPs: derpConns, }, nil}
func (e *userspaceEngine) RequestStatus() { // This is slightly tricky. e.getStatus() can theoretically get // blocked inside wireguard for a while, and RequestStatus() is // sometimes called from a goroutine, so we don't want a lot of // them hanging around. On the other hand, requesting multiple // status updates simultaneously is pointless anyway; they will // all say the same thing.
// Enqueue at most one request. If one is in progress already, this // adds one more to the queue. If one has been requested but not // started, it is a no-op. select { case e.reqCh <- struct{}{}: default: }
// Dequeue at most one request. Another thread may have already // dequeued the request we enqueued above, which is fine, since the // information is guaranteed to be at least as recent as the current // call to RequestStatus(). select { case <-e.reqCh: s, err := e.getStatus() if s == nil && err == nil { e.logf("[unexpected] RequestStatus: both s and err are nil") return } if cb := e.getStatusCallback(); cb != nil { cb(s, err) } default: }}
func (e *userspaceEngine) Close() { e.eventClient.Close() // eventClient.Close waited for the ChangeDelta subscriber, the sole // producer for linkChangeQueue, to return, so no new work can be // queued. Discard queued linkChanges and wait for an in-flight one // to finish before closing the subsystems it uses. // See tailscale/tailscale#17641. drainCtx, drainCancel := context.WithTimeout(context.Background(), 5*time.Second) defer drainCancel() e.linkChangeQueue.ShutdownAndWait(drainCtx) e.mu.Lock() if e.closing { e.mu.Unlock() return } e.closing = true e.mu.Unlock()
e.magicConn.Close() if e.netMonOwned { e.netMon.Close() } e.dns.Down() e.router.Close() e.wgdev.Close() e.tundev.Close() if e.bird != nil { e.bird.Close() } close(e.waitCh)
if e.netlogger != nil { e.netlogger.Shutdown() }}
func (e *userspaceEngine) Done() <-chan struct{} { return e.waitCh}
func (e *userspaceEngine) linkChange(delta *netmon.ChangeDelta) {
up := delta.AnyInterfaceUp() if !up { e.logf("LinkChange: all links down; pausing: %v", delta.StateDesc()) } else if delta.RebindLikelyRequired { e.logf("LinkChange: major, rebinding: %v", delta.StateDesc()) } else { e.logf("[v1] LinkChange: minor") }
e.health.SetAnyInterfaceUp(up) if !up || delta.RebindLikelyRequired { if err := e.dns.FlushCaches(); err != nil { e.logf("wgengine: dns flush failed after major link change: %v", err) } }
// Hacky workaround for Unix DNS issue 2458: on // suspend/resume or whenever NetworkManager is started, it // nukes all systemd-resolved configs. So reapply our DNS // config on major link change. // // On Darwin (netext), we reapply the DNS config when the interface flaps // because the change in interface can potentially change the nameservers // for the forwarder. On Darwin netext clients, magicDNS is ~always the default // resolver so having no nameserver to forward queries to (or one on a network we // are not currently on) breaks DNS resolution system-wide. There are notable // timing issues here with Darwin's network stack. It is not guaranteed that // the forward resolver will be available immediately after the interface // comes up. We leave it to the network extension to also poke magicDNS directly // via [dns.Manager.RecompileDNSConfig] when it detects any change in the // nameservers. // // TODO: On Android, Darwin-tailscaled, and openbsd, why do we need this? if delta.RebindLikelyRequired && up { switch runtime.GOOS { case "linux", "android", "ios", "darwin", "openbsd": e.wgLock.Lock() dnsCfg := e.lastDNSConfig e.wgLock.Unlock() if dnsCfg.Valid() { if err := e.dns.Set(*dnsCfg.AsStruct()); err != nil { e.logf("wgengine: error setting DNS config after major link change: %v", err) } else if err := e.reconfigureVPNIfNecessary(); err != nil { e.logf("wgengine: error reconfiguring VPN after major link change: %v", err) } else { e.logf("wgengine: set DNS config again after major link change") } } } }
e.magicConn.SetNetworkUp(up)
why := "link-change-minor" if delta.RebindLikelyRequired { why = "link-change-major" metricNumMajorChanges.Add(1) } else { metricNumMinorChanges.Add(1) }
// If we're up and it's a minor change, just send a STUN ping if up { if delta.RebindLikelyRequired { e.magicConn.Rebind() } e.magicConn.ReSTUN(why) }}
func (e *userspaceEngine) SetSelfNode(self tailcfg.NodeView) { e.mu.Lock() e.selfNode = self tunGROKnobsChanged := false var curUDP, curTCP bool if buildfeatures.HasGRO && runtime.GOOS == "linux" && e.controlKnobs != nil { curUDP = e.controlKnobs.DisableTUNUDPGRO.Load() curTCP = e.controlKnobs.DisableTUNTCPGRO.Load() // Only act on transitions toward "disabled"; wireguard-go's GRO // disablement is sticky and cannot be reversed without restart. if (curUDP && !e.lastAppliedDisableTUNUDPGRO) || (curTCP && !e.lastAppliedDisableTUNTCPGRO) { tunGROKnobsChanged = true } e.lastAppliedDisableTUNUDPGRO = curUDP e.lastAppliedDisableTUNTCPGRO = curTCP } e.mu.Unlock() if buildfeatures.HasGRO && tunGROKnobsChanged { e.logf("wgengine: TUN GRO knobs changed (DisableTUNUDPGRO=%v DisableTUNTCPGRO=%v); applying", curUDP, curTCP) e.tundev.ApplyGROKnobs(e.controlKnobs) }}
func (e *userspaceEngine) UpdateStatus(sb *ipnstate.StatusBuilder) { st, err := e.getStatus() if err != nil { e.logf("wgengine: getStatus: %v", err) return } if sb.WantPeers { for _, ps := range st.Peers { sb.AddPeer(ps.NodeKey, &ipnstate.PeerStatus{ RxBytes: int64(ps.RxBytes), TxBytes: int64(ps.TxBytes), LastHandshake: ps.LastHandshake, InEngine: true, }) } }
e.magicConn.UpdateStatus(sb)}
func (e *userspaceEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult)) { res := &ipnstate.PingResult{IP: ip.String()} pip, ok := e.peerForIP(ip) if !ok { e.logf("ping(%v): no matching peer", ip) res.Err = "no matching peer" cb(res) return } if pip.IsSelf { res.Err = fmt.Sprintf("%v is local Tailscale IP", ip) res.IsLocalIP = true cb(res) return } peer := pip.Node
e.logf("ping(%v): sending %v ping to %v %v ...", ip, pingType, peer.Key().ShortString(), peer.ComputedName()) switch pingType { case "disco": e.magicConn.Ping(peer, res, size, cb) case "TSMP": e.sendTSMPPing(ip, peer, res, cb) case "ICMP": e.sendICMPEchoRequest(ip, peer, res, cb) }}
func (e *userspaceEngine) mySelfIPMatchingFamily(dst netip.Addr) (src netip.Addr, err error) { var zero netip.Addr e.mu.Lock() defer e.mu.Unlock() if !e.selfNode.Valid() { return zero, errors.New("no self node") } addrs := e.selfNode.Addresses() if addrs.Len() == 0 { return zero, errors.New("no self address") } for _, p := range addrs.All() { if p.IsSingleIP() && p.Addr().BitLen() == dst.BitLen() { return p.Addr(), nil } } return zero, errors.New("no self address matching address family")}
func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer tailcfg.NodeView, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) { srcIP, err := e.mySelfIPMatchingFamily(destIP) if err != nil { res.Err = err.Error() cb(res) return } var icmph packet.Header if srcIP.Is4() { icmph = packet.ICMP4Header{ IP4Header: packet.IP4Header{ IPProto: ipproto.ICMPv4, Src: srcIP, Dst: destIP, }, Type: packet.ICMP4EchoRequest, Code: packet.ICMP4NoCode, } } else { icmph = packet.ICMP6Header{ IP6Header: packet.IP6Header{ IPProto: ipproto.ICMPv6, Src: srcIP, Dst: destIP, }, Type: packet.ICMP6EchoRequest, Code: packet.ICMP6NoCode, } }
idSeq, payload := packet.ICMPEchoPayload(nil)
expireTimer := time.AfterFunc(10*time.Second, func() { e.setICMPEchoResponseCallback(idSeq, nil) }) t0 := time.Now() e.setICMPEchoResponseCallback(idSeq, func() { expireTimer.Stop() d := time.Since(t0) res.LatencySeconds = d.Seconds() res.NodeIP = destIP.String() res.NodeName = peer.ComputedName() cb(res) })
icmpPing := packet.Generate(icmph, payload) e.tundev.InjectOutbound(icmpPing)}
func (e *userspaceEngine) sendTSMPPing(ip netip.Addr, peer tailcfg.NodeView, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) { srcIP, err := e.mySelfIPMatchingFamily(ip) if err != nil { res.Err = err.Error() cb(res) return } var iph packet.Header if srcIP.Is4() { iph = packet.IP4Header{ IPProto: ipproto.TSMP, Src: srcIP, Dst: ip, } } else { iph = packet.IP6Header{ IPProto: ipproto.TSMP, Src: srcIP, Dst: ip, } }
var data [8]byte crand.Read(data[:])
expireTimer := time.AfterFunc(10*time.Second, func() { e.setTSMPPongCallback(data, nil) }) t0 := time.Now() e.setTSMPPongCallback(data, func(pong packet.TSMPPongReply) { expireTimer.Stop() d := time.Since(t0) res.LatencySeconds = d.Seconds() res.NodeIP = ip.String() res.NodeName = peer.ComputedName() res.PeerAPIPort = pong.PeerAPIPort cb(res) })
var tsmpPayload [9]byte tsmpPayload[0] = byte(packet.TSMPTypePing) copy(tsmpPayload[1:], data[:])
tsmpPing := packet.Generate(iph, tsmpPayload[:]) e.tundev.InjectOutbound(tsmpPing)}
func (e *userspaceEngine) setTSMPPongCallback(data [8]byte, cb func(packet.TSMPPongReply)) { e.mu.Lock() defer e.mu.Unlock() if e.pongCallback == nil { e.pongCallback = map[[8]byte]func(packet.TSMPPongReply){} } if cb == nil { delete(e.pongCallback, data) } else { e.pongCallback[data] = cb }}
func (e *userspaceEngine) setICMPEchoResponseCallback(idSeq uint32, cb func()) { e.mu.Lock() defer e.mu.Unlock() if cb == nil { delete(e.icmpEchoResponseCallback, idSeq) } else { mak.Set(&e.icmpEchoResponseCallback, idSeq, cb) }}
// ProbeLocks implements [Engine.ProbeLocks].func (e *userspaceEngine) ProbeLocks() { e.mu.Lock() e.mu.Unlock()
e.wgLock.Lock() e.wgLock.Unlock()}
// SetPeerForIPFunc installs the callback used by [userspaceEngine.peerForIP].// See [Engine.SetPeerForIPFunc].func (e *userspaceEngine) SetPeerForIPFunc(fn func(netip.Addr) (PeerForIP, bool)) { if fn == nil { e.peerForIPFn.Store(nil) return } e.peerForIPFn.Store(&fn)}
// peerForIP returns the node responsible for handling the given IP.// It delegates to the callback installed via [Engine.SetPeerForIPFunc];// engines without an installed callback return (zero, false).func (e *userspaceEngine) peerForIP(ip netip.Addr) (ret PeerForIP, ok bool) { if !ip.IsValid() { return ret, false } if fn := e.peerForIPFn.Load(); fn != nil { return (*fn)(ip) } return ret, false}
type closeOnErrorPool []func()
func (p *closeOnErrorPool) add(c io.Closer) { *p = append(*p, func() { c.Close() }) }func (p *closeOnErrorPool) addFunc(fn func()) { *p = append(*p, fn) }func (p closeOnErrorPool) closeAllIfError(errp *error) { if *errp != nil { for _, closeFn := range p { closeFn() } }}
// ipInPrefixes reports whether ip is in any of pp.func ipInPrefixes(ip netip.Addr, pp []netip.Prefix) bool { for _, p := range pp { if p.Contains(ip) { return true } } return false}
// dnsIPsOverTailscale returns the IPPrefixes of DNS resolver IPs that are// routed over Tailscale. The returned value does not contain duplicates is// not necessarily sorted.func dnsIPsOverTailscale(dnsCfg *dns.Config, routerCfg *router.Config) (ret []netip.Prefix) { m := map[netip.Addr]bool{}
add := func(resolvers []*dnstype.Resolver) { for _, r := range resolvers { ip, err := netip.ParseAddr(r.Addr) if err != nil { if ipp, err := netip.ParseAddrPort(r.Addr); err == nil { ip = ipp.Addr() } else { continue } } if ipInPrefixes(ip, routerCfg.Routes) && !ipInPrefixes(ip, routerCfg.LocalRoutes) { m[ip] = true } } }
add(dnsCfg.DefaultResolvers) for _, resolvers := range dnsCfg.Routes { add(resolvers) }
ret = make([]netip.Prefix, 0, len(m)) for ip := range m { ret = append(ret, netip.PrefixFrom(ip, ip.BitLen())) } return ret}
// fwdDNSLinkSelector is userspaceEngine's resolver.ForwardLinkSelector, to pick// which network interface to send DNS queries out of.type fwdDNSLinkSelector struct { ue *userspaceEngine tunName string}
func (ls fwdDNSLinkSelector) PickLink(ip netip.Addr) (linkName string) { // sandboxed macOS does not automatically bind to the loopback interface so // we must be explicit about it. if runtime.GOOS == "darwin" && ip.IsLoopback() { return "lo0" }
if ls.ue.isDNSIPOverTailscale.Load()(ip) { return ls.tunName } return ""}
var ( metricReflectToOS = clientmetric.NewCounter("packet_reflect_to_os")
metricNumMajorChanges = clientmetric.NewCounter("wgengine_major_changes") metricNumMinorChanges = clientmetric.NewCounter("wgengine_minor_changes"))
func (e *userspaceEngine) InstallCaptureHook(cb packet.CaptureCallback) { if !buildfeatures.HasCapture { return } e.tundev.InstallCaptureHook(cb) e.magicConn.InstallCaptureHook(cb)}
func (e *userspaceEngine) reconfigureVPNIfNecessary() error { if e.reconfigureVPN == nil { return nil } return e.reconfigureVPN()}