summaryrefslogtreecommitdiffhomepage
path: root/wgengine
AgeCommit message (Collapse)AuthorFilesLines
2026-04-24wgengine/netstack: absorb all quad-100 traffic locally, never leak to peersJames Tucker2-14/+223
Previously, handleLocalPackets intercepted traffic to the Tailscale service IP (100.100.100.100 / fd7a:115c:a1e0::53) only for an allow-list of ports: TCP 53/80/8080 and UDP 53. Any other port returned filter.Accept, letting the packet fall through to the ACL filter and wireguard-go, which would attempt a peer lookup. No peer owns the quad-100 AllowedIP, so after ~5s pendopen.go would log: open-conn-track: timeout opening ...; no associated peer node This is the common "conntrack error no peer found for 100.100.100.100:853" log spam seen in the wild (e.g. from systemd-resolved or another resolver speculatively trying DoT on quad-100). It also leaks quad-100 packets onto the tailnet. Remove the port allow-list so handleLocalPackets absorbs every quad-100 packet into netstack regardless of IP protocol or port. Traffic never reaches the conntrack / peer-routing layers. With the allow-list gone, acceptTCP needs a corresponding guard: on a quad-100 TCP port we don't serve, execution used to fall through to the isTailscaleIP case (quad-100 is in the tailscale IP range), which rewrote the dial target to 127.0.0.1:<port> and forwardTCP'd the connection to whatever happened to be listening on the host's loopback at that port. Add a hittingServiceIP case that RSTs cleanly instead, placed before the isTailscaleIP fallthrough. TestQuad100UnservedTCPPortDoesNotForward is a new integration test that injects a TCP SYN to 100.100.100.100:853 via handleLocalPackets, stubs forwardDialFunc, and asserts the dialer is not invoked; it catches regressions of the acceptTCP recursion/loopback-redirection case. Fixes #15796 Fixes #19421 Updates #3261 Updates #11305 Signed-off-by: James Tucker <james@tailscale.com>
2026-04-23wgengine/magicsock: do not send TSMP disco when connected (#19497)Claus Lensbøl2-14/+39
When there is an active connection between devices, do not send new disco keys via TSMP. Updates #12639 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-04-22wgengine/magicsock: replace peers slice with peersByID map; add ↵Brad Fitzpatrick5-192/+359
Upsert/RemovePeer Replace Conn.peers (sorted views.Slice) with peersByID, a map[tailcfg.NodeID]tailcfg.NodeView. The only caller that needed the sorted slice (the disco message receive path's binary search) becomes a single map lookup. Drop nodesEqual. Add Conn.UpsertPeer / Conn.RemovePeer for O(1) single-peer endpoint work. RemovePeer also performs a targeted single-disco-key cleanup (previously that scan was O(discoInfo)). Extract the shared per-peer upsert body as upsertPeerLocked; still used by SetNetworkMap's bulk path. SetNetworkMap is documented as the bulk / initial / self-change path; UpsertPeer and RemovePeer are preferred for single-peer changes. Make the relay server set update O(1) per peer: add serverUpsertCh / serverRemoveCh to relayManager with matching run-loop handlers. UpsertPeer / RemovePeer evaluate the per-peer relay predicate locally and dispatch upsert or remove. The full-rebuild updateRelayServersSet stays for the initial netmap, filter changes, and fallback. Move the hasPeerRelayServers atomic from Conn onto relayManager, next to the serversByNodeKey map it summarizes. The run loop is now the single writer and needs no back-pointer to Conn; endpoint's two hot-path readers take one extra hop to de.c.relayManager.hasPeerRelayServers but the cost is the same atomic load. No callers use UpsertPeer/RemovePeer yet; a subsequent change will plumb per-peer add/remove through the incremental map update path. Updates #12542 Change-Id: If6a3442fe29ccbd77890ea61b754a4d1ad6ef225 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-15magicsock: invalidate endpoint on trust timeout (#19415)Alex Valiushko2-8/+237
Endpoint's best address was cleared on trustBestAddrUntil expiry only if it was a udprelay connection. This generalizes invalidation to also cover direct UDP. Trust deadline is checked in two cases: On disco ping timeout from the endpoint's best address. Traffic goes DERP-only, heartbeats to the old address stop. The discovery pings are still in flight, handled by the following. On disco ping success from an alternative. BestAddr switches to the working path, trust refreshed, eager discovery stops. The still in flight pongs are handled by betterAddr(). Updates #19407 Change-Id: Ic41ed18edb4a6e4350a2d49271ba01566a6a6964 Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-04-14wgengine/magicsock: remove pickPort, use port 0 to avoid TOCTOU raceAvery Pennarun1-14/+12
pickPort would bind a UDP socket on :0 to get a free port, close the socket, then hope to rebind to the same port in NewConn. This is a TOCTOU race that can cause flaky test failures when another process grabs the port in between. Instead, pass Port: 0 to NewConn and let the OS assign the port atomically, then read back the assigned port via conn.LocalPort(). Fixes #19409 Change-Id: Ie44b599fb93c361e29a05f2171ad747c46f82b7a Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com> Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2026-04-14util/linuxfw,wgengine/router: allow incoming CGNAT range traffic with nodeattrNaman Sood4-11/+110
Clients with the newly added node attribute `"disable-linux-cgnat-drop-rule"` will not automatically drop inbound traffic on non-Tailscale network interfaces with the source IP in the CGNAT IP range. This is an initial proof-of-concept for enabling connectivity with off-Tailnet CGNAT endpoints. Fixes tailscale/corp#36270. Signed-off-by: Naman Sood <mail@nsood.in>
2026-04-14wgengine: replace reflect.DeepEqual with typed Equal for maybeReconfigInputs ↵Fernando Serboncini2-4/+151
(#19365) reflect.DeepEqual is expensive and allocates heavily. Replace it with a field-by-field comparison that does zero allocations. Adds tests and benchmarks for the new Equal method. Fixes #19363 Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
2026-04-14wgengine/magicsock: deflake TestTwoDevicePing compare-metrics-statsBrad Fitzpatrick1-72/+107
The compare-metrics-stats subtest reset two independent counting systems (physical connection counters and expvar.Int user metrics) non-atomically. Background WireGuard keepalives arriving between the resets could increment one system but not the other, causing off-by-one packet/byte mismatches in either direction. Replace the reset-then-compare pattern with snapshot-and-delta: snapshot both systems before pings, snapshot again after, and compare the deltas. This eliminates the non-atomic reset window entirely. As a belt-and-suspenders safety net, tolerate a difference of exactly one packet (and corresponding bytes) from a stray keepalive that could still arrive in the narrow window between the two snapshots. flakestress passes with ~5900 runs (~2800 without -race, ~3100 with -race) but it also passed previously too. This is an annoying one to repro. Fixes #11762 Change-Id: I3447ad67e71c8146e85eed38b7a665033ef9e284 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-13all: fix six tests that failed with -count=2Brad Fitzpatrick1-0/+3
Avery found a bunch of tests that fail with -count=2. Updates tailscale/corp#40176 (tracks making our CI detect them) Change-Id: Ie3e4398070dd92e4fe0146badddf1254749cca20 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Co-authored-by: Avery Pennarun <apenwarr@tailscale.com>
2026-04-13wgengine/netstack: fix data race on in-flight connection test globalsBrad Fitzpatrick2-9/+13
The maxInFlightConnectionAttemptsForTest and maxInFlightConnectionAttemptsPerClientForTest globals were plain ints read by background gVisor TCP handler goroutines (via wrapTCPProtocolHandler) and written by tstest.Replace cleanup in TestTCPForwardLimits_PerClient. When a gVisor goroutine outlived the test cleanup window, the race detector caught the unsynchronized access. The race-prone code was introduced in c5abbcd4b4d8 (2024-02-26, "wgengine/netstack: add a per-client limit for in-flight TCP forwards") which added both the plain int globals and the TestTCPForwardLimits_PerClient test that writes them via tstest.Replace. It is not obvious why this has only recently started being detected as a data race; likely some combination of gVisor version bumps, Go toolchain scheduler changes, and additional TCP-injecting subtests (e.g. 03461ea7f, 2026-01-30) increased goroutine churn enough to hit the window. Change both globals to atomic.Int32 and replace tstest.Replace (which does non-atomic *target = old on cleanup) with explicit Store/Cleanup pairs. Fixes #19118 Change-Id: Id26ba6fbfb2e4ade319976db80af8e16c7c8778e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-10wgengine/router/osrouter: fix privileged tests missing fake netfilter runnerAmal Bansode1-0/+4
These test failures were never caught by CI because the package in question was missing from our privileged tests list. tailscale/corp#40007 covers improving our process around this. Fixes #19316 Signed-off-by: Amal Bansode <amal@tailscale.com>
2026-04-10tstest: add RequireRoot helperBrad Fitzpatrick1-3/+1
Start using a common helper for tests to declare that they require root. This is step 1. A later step will then make this helper track which tests were skipped so a subsequent pass will run these test as root. Updates tailscale/corp#40007 Change-Id: I4979e1def0fa3691d38c83f48c89aaa443e7f62e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-08wgengine/netstack: allow UDP listeners to receive traffic on Service VIP ↵Tom Meadows2-0/+216
addresses (#18972) Fixes UDP listeners on VIP Service addresses not receiving inbound traffic. - Modified shouldProcessInbound to check for registered UDP transport endpoints when processing packets to service VIPs - Uses FindTransportEndpoint to determine if a UDP listener exists for the destination VIP/port - Supports both IPv4 and IPv6 The aim was to mirror the existing TCP logic, providing feature parity for UDP-based services on VIP Services. Fixes #18971 Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-04-07tsd, all: add Sys.ExtraRootCAs, plumb through TLS dial pathsBrad Fitzpatrick3-0/+17
Add ExtraRootCAs *x509.CertPool to tsd.System and plumb it through the control client, noise transport, DERP, and wgengine layers so that platforms like Android can inject user-installed CA certificates into Go's TLS verification. tlsdial.Config now honors base.RootCAs as additional trusted roots, tried after system roots and before the baked-in LetsEncrypt fallback. SetConfigExpectedCert gets the same treatment for domain-fronted DERP. The Android client will set sys.ExtraRootCAs with a pool built from x509.SystemCertPool + user-installed certs obtained via the Android KeyStore API, replacing the current SSL_CERT_DIR environment variable approach. Updates #8085 Change-Id: Iecce0fd140cd5aa0331b124e55a7045e24d8e0c2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-07wgengine/userspace: add extra check for tsmp learned keys in engine (#19223)Claus Lensbøl2-4/+115
If an entry in the tsmpLearnedDisco does not match the disco key of the key currently being processed, overwrite the key, and leave the entry in the map for later processing. In reality, this should not happen, but is put in as a safety measure with logging of the situation so we can replicate the behaviour and correct it should it happen. Updates #12639 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-04-05cmd/vet: add subtestnames analyzer; fix all existing violationsBrad Fitzpatrick7-107/+107
Add a new vet analyzer that checks t.Run subtest names don't contain characters requiring quoting when re-running via "go test -run". This enforces the style guide rule: don't use spaces or punctuation in subtest names. The analyzer flags: - Direct t.Run calls with string literal names containing spaces, regex metacharacters, quotes, or other problematic characters - Table-driven t.Run(tt.name, ...) calls where tt ranges over a slice/map literal with bad name field values Also fix all 978 existing violations across 81 test files, replacing spaces with hyphens and shortening long sentence-like names to concise hyphenated forms. Updates #19242 Change-Id: Ib0ad96a111bd8e764582d1d4902fe2599454ab65 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-01tailcfg,ipn/ipnlocal: regulate netmap caching via a node attribute (#19117)M. J. Fromberger1-1/+1
Add a new tailcfg.NodeCapability (NodeAttrCacheNetworkMaps) to control whether a node with support for caching network maps will attempt to do so. Update the capability version to reflect this change (mainly as a safety measure, as the control plane does not currently need to know about it). Use the presence (or absence) of the node attribute to decide whether to create and update a netmap cache for each profile. If caching is disabled, discard the cached data; this allows us to use the presence of a cached netmap as an indicator it should be used (unless explicitly overridden). Add a test that verifies the attribute is respected. Reverse the sense of the environment knob to be true by default, with an override to disable caching at the client regardless what the node attribute says. Move the creation/update of the netmap cache (when enabled) until after successfully applying the network map, to reduce the possibility that we will cache (and thus reuse after a restart) a network map that fails to correctly configure the client. Updates #12639 Change-Id: I1df4dd791fdb485c6472a9f741037db6ed20c47e Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2026-04-01wgengine/magicsock: send out disco keys over TSMP periodically (#19212)Claus Lensbøl3-4/+53
Instead of sending out disco keys via TSMP once, send them out in intervals of 60+ seconds. The trigger is still callmemaaybe and the keys will not be send if no direct connection needs to be established. This fixes a case where a node can have stale keys but have communicated with the other peer before, leading to an infinite DERP state. Updates #12639 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-03-31wgengine/magicsock: assume network up for testsHarry Harpham2-1/+14
Without this, any test relying on underlying use of magicsock will fail without network connectivity, even when the test logic has no need for a network connection. Tests currently in this bucket include many in tstest/integration and in tsnet. Further explanation: ipn only becomes Running when it sees at least one live peer or DERP connection: https://github.com/tailscale/tailscale/blob/0cc1b2ff76560ee4675909272fa37ba6b397744c/ipn/ipnlocal/local.go#L5861-L5866 When tests only use a single node, they will never see a peer, so the node has to wait to see a DERP server. magicsock sets the preferred DERP server in updateNetInfo(), but this function returns early if the network is down. https://github.com/tailscale/tailscale/blob/0cc1b2ff76560ee4675909272fa37ba6b397744c/wgengine/magicsock/magicsock.go#L1053-L1106 Because we're checking the real network, this prevents ipn from entering "Running" and causes the test to fail or hang. In tests, we can assume the network is up unless we're explicitly testing the behaviour of tailscaled when the network is down. We do something similar in magicsock/derp.go, where we assume we're connected to control unless explicitly testing otherwise: https://github.com/tailscale/tailscale/blob/7d2101f3520f16b86f2ed5e15f23c44d720534e6/wgengine/magicsock/derp.go#L166-L177 This is the template for the changes to `networkDown()`. Fixes #17122 Co-authored-by: Alex Chan <alexc@tailscale.com> Signed-off-by: Harry Harpham <harry@tailscale.com>
2026-03-30control/controlclient,ipn/ipnlocal,wgengine: avoid restarting wireguard when ↵Claus Lensbøl3-1/+121
key is learned via tsmp (#19142) When disco keys are learned on a node that is connected to control and has a mapSession, wgengine will see the key as having changed, and assume that any existing connections will need to be reset. For keys learned via TSMP, the connection should not be reset as that key is learned via an active wireguard connection. If wgengine resets that connetion, a 15s timeout will occur. This change adds a map to track new keys coming in via TSMP, and removes them from the list of keys that needs to trigger wireguard resets. This is done with an interface chain from controlclient down via localBackend to userspaceEngine via the watchdog. Once a key has been actively used for preventing a wireguard reset, the key is removed from the map. If mapSession becomes a long lived process instead of being dependent on having a connection to control. This interface chain can be removed, and the event sequence from wrap->controlClient->userspaceEngine, can be changed to wrap->userspaceEngine->controlClient as we know the map will not be gunked up with stale TSMP entries. Updates #12639 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-03-24all: use `bart.Lite` instead of `bart.Table` where appropriateAlex Chan1-6/+3
When we don't care about the payload value and are just checking whether a set contains an IP/prefix, we can use `bart.Lite` for the same lookup times but a lower memory footprint. Fixes #19075 Change-Id: Ia709e8b718666cc61ea56eac1066467ae0b6e86c Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-03-20wgengine/magicsock,control/controlclient: do not overwrite discokey with old ↵Claus Lensbøl3-18/+19
key (#18606) When a client starts up without being able to connect to control, it sends its discoKey to other nodes it wants to communicate with over TSMP. This disco key will be a newer key than the one control knows about. If the client that can connect to control gets a full netmap, ensure that the disco key for the node not connected to control is not overwritten with the stale key control knows about. This is implemented through keeping track of mapSession and use that for the discokey injection if it is available. This ensures that we are not constantly resetting the wireguard connection when getting the wrong keys from control. This is implemented as: - If the key is received via TSMP: - Set lastSeen for the peer to now() - Set online for the peer to false - When processing new keys, only accept keys where either: - Peer is online - lastSeen is newer than existing last seen If mapSession is not available, as in we are not yet connected to control, punt down the disco key injection to magicsock. Ideally, we will want to have mapSession be long lived at some point in the near future so we only need to inject keys in one location and then also use that for testing and loading the cache, but that is a yak for another PR. Updates #12639 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-03-17wgengine/netstack: add TS_NETSTACK_KEEPALIVE_{IDLE,INTERVAL} envknobsJosef Bacik1-8/+36
Adds envknobs to override the netstack default TCP keepalive idle time (~2h) and probe interval (75s) for forwarded connections. When a tailnet peer goes away without closing its connections (pod deleted, peer removed from the netmap, silent network partition), the forwardTCP io.Copy goroutines block until keepalive fires: the gvisor-side Read waits on a peer that will never send again, and the backend-side Read waits on a backend that is alive and idle. With the netstack default of 7200s idle + 9×75s probes, dead-peer detection takes a little over two hours. Under high-churn forwarding — many short-lived peers, or peers holding thousands of proxied connections that drop at once — stuck goroutines accumulate faster than they clear. The existing SetKeepAlive(true) at this site enables keepalive without setting the timers; the TODO above it noted "a shorter default might be better" and "might be a useful user-tunable". This makes both timers tunable without changing the defaults: unset preserves the ~2h behavior, which is the right trade-off for battery-powered peers. The two knobs are independent — setting one leaves the other at the netstack default. The options are set before SetKeepAlive(true) so the timer arms with the configured values rather than the defaults — matches the order in ipnlocal/local.go for SSH keepalive. Updates #4522 Signed-off-by: Josef Bacik <josefbacik@anthropic.com>
2026-03-13wgengine/netstack: don't register subnet/4via6 TCP flows with proxymapBrad Fitzpatrick1-6/+9
Fixes #18991 Change-Id: I29a609dcd401854026aef4a5ad8d5806c3249ea6 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-03-13net/{batching,udprelay},wgengine/magicsock: add SO_RXQ_OVFL clientmetricsJordan Whited1-1/+1
For the purpose of improved observability of UDP socket receive buffer overflows on Linux. Updates tailscale/corp#37679 Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-03-11wgengine{,/magicsock}: add DERP hooks for filtering+sending packetsBrad Fitzpatrick4-11/+51
Add two small APIs to support out-of-tree projects to exchange custom signaling messages over DERP without requiring disco protocol extensions: - OnDERPRecv callback on magicsock.Options / wgengine.Config: called for every non-disco DERP packet before the peer map lookup, allowing callers to intercept packets from unknown peers that would otherwise be dropped. - SendDERPPacketTo method on magicsock.Conn: sends arbitrary bytes to a node key via a DERP region, creating the connection if needed. Thin wrapper around the existing internal sendAddr. Also allow netstack.Start to accept a nil LocalBackend for use cases that wire up TCP/UDP handlers directly without a full LocalBackend. Updates tailscale/corp#24454 Change-Id: I99a523ef281625b8c0024a963f5f5bf5d8792c17 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-03-11wgengine: search randomly for unused port instead of in contiguous range ↵kari-ts1-2/+3
(#18974) In TestUserspaceEnginePortReconfig, when selecting a port, use a random offset rather than searching in a continguous range in case there is a range that is blocked Updates tailscale/tailscale#2855 Signed-off-by: kari-ts <kari@tailscale.com>
2026-03-11wgengine/magicsock: fix three race conditions in TestTwoDevicePingBrad Fitzpatrick1-31/+70
Fix three independent flake sources, at least as debugged by Claude, though empirically no longer flaking as it was before: 1. Poll for connection counter data instead of reading immediately. The conncount callback fires asynchronously on received WireGuard traffic, so after counts.Reset() there is no guarantee the counter has been repopulated before checkStats reads it. Use tstest.WaitFor with a 5s timeout to retry until a matching connection appears. 2. Replace the *2 symmetry assumption in global metric assertions. metricSendUDP and friends are AggregateCounters that sum per-conn expvars from both magicsock instances. The old assertion assumed both instances had identical packet counts, which breaks under asymmetric background WireGuard activity (handshake retries, etc). The new assertGlobalMetricsMatchPerConn computes the actual sum of both conns' expvars and compares against the AggregateCounter value. 3. Tolerate physical stats being 0 when user metrics are non-zero. A rebind event replaces the socket mid-measurement, resetting the physical connection counter while user metrics still reflect packets processed before the rebind. Log instead of failing in this case. Also move counts.Reset() after metric reads and reorder the reset sequence (counts before metrics) to minimize the race window. Fixes tailscale/tailscale#13420 Change-Id: I7b090a4dc229a862c1a52161b3f2547ec1d1f23f Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-03-10wgengine: add API to force a disco key for experiments, testingBrad Fitzpatrick2-0/+18
Updates #12639 Updates tailscale/corp#24454 Change-Id: I2361206aec197a7eecbdf29d87b1b75335ee8eec Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-03-06all: use Go 1.26 things, run most gofix modernizersBrad Fitzpatrick8-29/+19
I omitted a lot of the min/max modernizers because they didn't result in more clear code. Some of it's older "for x := range 123". Also: errors.AsType, any, fmt.Appendf, etc. Updates #18682 Change-Id: I83a451577f33877f962766a5b65ce86f7696471c Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-03-05types/ptr: deprecate ptr.To, use Go 1.26 newBrad Fitzpatrick2-5/+3
Updates #18682 Change-Id: I62f6aa0de2a15ef8c1435032c6aa74a181c25f8f Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-03-05all: fix typos in commentsBrad Fitzpatrick1-1/+1
Fix its/it's, who's/whose, wether/whether, missing apostrophes in contractions, and other misspellings across the codebase. Updates #cleanup Change-Id: I20453b81a7aceaa14ea2a551abba08a2e7f0a1d8 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-03-05tstest/natlab: add test for no control and rotated disco key (#18261)Claus Lensbøl1-0/+1
Updates #12639 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-03-04util/linuxfw,wgengine/router: add connmark rules for rp_filter workaround ↵Mike O'Driscoll2-12/+226
(#18860) When a Linux system acts as an exit node or subnet router with strict reverse path filtering (rp_filter=1), reply packets may be dropped because they fail the RPF check. Reply packets arrive on the WAN interface but the routing table indicates they should have arrived on the Tailscale interface, causing the kernel to drop them. This adds firewall rules in the mangle table to save outbound packet marks to conntrack and restore them on reply packets before the routing decision. When reply packets have their marks restored, the kernel uses the correct routing table (based on the mark) and the packets pass the rp_filter check. Implementation adds two rules per address family (IPv4/IPv6): - mangle/OUTPUT: Save packet marks to conntrack for NEW connections with non-zero marks in the Tailscale fwmark range (0xff0000) - mangle/PREROUTING: Restore marks from conntrack to packets for ESTABLISHED,RELATED connections before routing decision and rp_filter check The workaround is automatically enabled when UseConnmarkForRPFilter is set in the router configuration, which happens when subnet routes are advertised on Linux systems. Both iptables and nftables implementations are provided, with automatic backend detection. Fixes #3310 Fixes #14409 Fixes #12022 Fixes #15815 Fixes #9612 Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2026-03-03wgengine/magicsoc,net/tstun: put disco key advertisement behind a nob (#18857)Claus Lensbøl1-0/+4
To be less spammy in stable, add a nob that disables the creation and processing of TSMPDiscoKeyAdvertisements until we have a proper rollout mechanism. Updates #12639 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-03-03wgengine/magicsock: improve error message for moving Mullvad node keysAlex Chan1-2/+12
The "public key moved" panic has caused confusion on multiple occasions, and is a known issue for Mullvad. Add a loose heuristic to detect Mullvad nodes, and trigger distinct panics for Mullvad and non-Mullvad instances, with a link to the associated bug. When this occurs again with Mullvad, it'll be easier for somebody to find the existing bug. If it occurs again with something other than Mullvad, it'll be more obvious that it's a distinct issue. Updates tailscale/corp#27300 Change-Id: Ie47271f45f2ff28f767578fcca5e6b21731d08a1 Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-02-27wgengine/netstack: deliver self-addressed packets via loopbackJames Tucker3-0/+355
When a tsnet.Server dials its own Tailscale IP, TCP SYN packets are silently dropped. In inject(), outbound packets with dst=self fail the shouldSendToHost check and fall through to WireGuard, which has no peer for the node's own address. Fix this by detecting self-addressed packets in inject() using isLocalIP and delivering them back into gVisor's network stack as inbound packets via a new DeliverLoopback method on linkEndpoint. The outbound packet must be re-serialized into a new PacketBuffer because outbound packets have their headers parsed into separate views, but DeliverNetworkPacket expects raw unparsed data. Updates #18829 Signed-off-by: James Tucker <james@tailscale.com>
2026-02-26go.mod: bump gvisorBrad Fitzpatrick1-1/+14
Updates #8043 Change-Id: Ia229ad4f28f2ff20e0bdecb99ca9e1bd0356ad8e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-02-26wgengine/magicsock: only run derpActiveFunc after connecting to DERP (#18814)Fernando Serboncini2-1/+59
derpActiveFunc was being called immediately as a bare goroutine, before startGate was resolved. For the firstDerp case, startGate is c.derpStarted which only closes after dc.Connect() completes, so derpActiveFunc was firing before the DERP connection existed. We now block it with the same logic used by runDerpReader and by runDerpWriter. Updates: #18810 Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
2026-02-25netns,wgengine: add OpenBSD support to netns via an rtablejoshua stein1-8/+41
When an exit node has been set and a new default route is added, create a new rtable in the default rdomain and add the current default route via its physical interface. When control() is requesting a connection not go through the exit-node default route, we can use the SO_RTABLE socket option to force it through the new rtable we created. Updates #17321 Signed-off-by: joshua stein <jcs@jcs.org>
2026-02-24ipnext,ipnlocal,wgengine/filter: add extension hooks for custom filter matchersMichael Ben-Ami2-17/+257
Add PacketMatch hooks to the packet filter, allowing extensions to customize filtering decisions: - IngressAllowHooks: checked in RunIn after pre() but before the standard runIn4/runIn6 match rules. Hooks can accept packets to destinations outside the local IP set. First match wins; the returned why string is used for logging. - LinkLocalAllowHooks: checked inside pre() for both ingress and egress, providing exceptions to the default policy of dropping link-local unicast packets. First match wins. The GCP DNS address (169.254.169.254) is always allowed regardless of hooks. PacketMatch returns (match bool, why string) to provide a log reason consistent with the existing filter functions. Hooks are registered via the new FilterHooks struct in ipnext.Hooks and wired through to filter.Filter in LocalBackend.updateFilterLocked. Fixes tailscale/corp#35989 Fixes tailscale/corp#37207 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-02-13util/clientmetric, wgengine/watchdog: report watchdog errors in user/client ↵Jonathan Nobels2-18/+176
metrics (#18591) fixes tailscale/corp#36708 Sets up a set of metrics to report watchdog timeouts for wgengine and reports an event for any watchdog timeout. Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2026-02-10magicsock, ipnlocal: revert eventbus-based node/filter updates, remove ↵Brad Fitzpatrick2-172/+81
Synchronize hack Restore synchronous method calls from LocalBackend to magicsock.Conn for node views, filter, and delta mutations. The eventbus delivery introduced in 8e6f63cf1 was invalid for these updates because subsequent operations in the same call chain depend on magicsock already having the current state. The Synchronize/settleEventBus workaround was fragile and kept requiring more workarounds and introducing new mystery bugs. Since eventbus was added, we've since learned more about when to use eventbus, and this wasn't one of the cases. We can take another swing at using eventbus for netmap changes in a future change. Fixes #16369 Updates #18575 (likely fixes) Change-Id: I79057cc9259993368bb1e350ff0e073adf6b9a8f Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-02-09tstun,wgengine: add new datapath hooks for intercepting Connectors 2025Michael Ben-Ami1-15/+44
app connector packets We introduce the Conn25PacketHooks interface to be used as a nil-able field in userspaceEngine. The engine then plumbs through the functions to the corresponding tstun.Wrapper intercepts. The new intercepts run pre-filter when egressing toward WireGuard, and post-filter when ingressing from WireGuard. This is preserve the design invariant that the filter recognizes the traffic as interesting app connector traffic. This commit does not plumb through implementation of the interface, so should be a functional no-op. Fixes tailscale/corp#35985 Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-01-30wgengine/netstack: add local tailscale service IPs to route and terminate ↵KevinLiang102-6/+133
locally (#18461) * wgengine/netstack: add local tailscale service IPs to route and terminate locally This commit adds the tailscales service IPs served locally to OS routes, and make interception to packets so that the traffic terminates locally without making affects to the HA traffics. Fixes tailscale/corp#34048 Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * fix test Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * add ready field to avoid accessing lb before netstack starts Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * wgengine/netstack: store values from lb to avoid acquiring a lock Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * add active services to netstack on starts with stored prefs. Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * fix comments Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> * update comments Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com> --------- Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
2026-01-30wgengine/magicsock: make debugNeverDirectUDP influence remote peer decisionsJordan Whited2-1/+6
By dropping inbound disco.Ping messages received over direct UDP paths. Fixes #18560 Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-01-26ipn/localapi: stop logging "broken pipe" errors (#18487)Amal Bansode1-1/+3
The Tailscale CLI has some methods to watch the IPN bus for messages, say, the current netmap (`tailscale debug netmap`). The Tailscale daemon supports this using a streaming HTTP response. Sometimes, the client can close its connection abruptly -- due to an interruption, or in the case of `debug netmap`, intentionally after consuming one message. If the server daemon is writing a response as the client closes its end of the socket, the daemon typically encounters a "broken pipe" error. The "Watch IPN Bus" handler currently logs such errors after they're propagated by a JSON encoding/writer helper. Since the Tailscale CLI nominally closes its socket with the daemon in this slightly ungraceful way (viz. `debug netmap`), stop logging these broken pipe errors as far as possible. This will help avoid confounding users when they scan backend logs. Updates #18477 Signed-off-by: Amal Bansode <amal@tailscale.com>
2026-01-23all: remove AUTHORS file and references to itWill Norris104-104/+104
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
2026-01-22tsnet: add support for a user-supplied tun.DeviceJames Tucker1-1/+85
tsnet users can now provide a tun.Device, including any custom implementation that conforms to the interface. netstack has a new option CheckLocalTransportEndpoints that when used alongside a TUN enables netstack listens and dials to correctly capture traffic associated with those sockets. tsnet with a TUN sets this option, while all other builds leave this at false to preserve existing performance. Updates #18423 Signed-off-by: James Tucker <james@tailscale.com>
2026-01-22wgengine: send disco key via TSMP on first contact (#18215)Claus Lensbøl3-4/+67
When we have not yet communicated with a peer, send a TSMPDiscoAdvertisement to let the peer know of our disco key. This is in most cases redundant, but will allow us to set up direct connections when the client cannot access control. Some parts taken from: #18073 Updates #12639 Signed-off-by: Claus Lensbøl <claus@tailscale.com>