Commit Graph
1996 Commits
Author SHA1 Message Date
Brad FitzpatrickandDavid Anderson 911d8f84a0 wgengine/wgcfg: wire up new wireguard-go hybrid handshake field
Wire up wireguard-go's new NewPeerConfig.Hybrid field. (no current users)

Updates tailscale/corp#50068

Co-authored-by: David Anderson <danderson@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I80d7af011b43273e6c949214c6d862f9247a82e1
2026-11-03 10:19:55 -08:00
James Tucker e9b151bd65 magicsock: handle UDP read feedback
Retry UDP reads returning EHOSTUNREACH or ENETUNREACH, including their
Winsock equivalents, without returning those errors to wireguard-go.
Count them in magicsock_recv_udp_error_reachability. Other read errors are unchanged.

Updates #20616
Updates #19504

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-30 16:31:39 -07:00
James Tucker 1a28f170f6 wgengine/netstack: disable RACK again
Several reports including a severe internal issue have demonstrated use
cases where the current RACK implementation still fails too poorly to be
our enabled default.

Updates #21946
Updates #21651
Updates #18307

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-30 11:42:03 -07:00
Claus Lensbøl 2c7aeadb1d ipn/ipnlocal,wgengine{,/magicsock}: cleanup TSMP disco logs (#22088)
Logging is quite aggressive and most of the logs can be hidden behind
component logging. A few logs are instead gated on if they changed or if
the original state was zero.

Updates #cleanup

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-10-30 11:08:15 -04:00
Aditya MerchantandJames Tucker 949adb0f6c wgengine/netstack: add TS_NETSTACK_ENABLE_RACK opt-out
Since 1.104.0 (#21303), netstack's TCP uses gVisor's RACK-TLP loss
detection again. In the gVisor that go.mod pins, RACK's loss marks do
not reduce the SACK recovery pipe, and after an RTO, RACK starts fast
recovery for the same flight, with ssthresh 2 and cwnd 5. After a
burst loss, the sender can then retransmit about two segments per RTO
for seconds to tens of seconds, and for minutes when the client does
not offer TCP timestamps (Windows' default), because the RTO then
keeps backing off. That stalls TCP that netstack terminates, such as
tailscale serve, Tailscale SSH and tsnet, while kernel TCP over the
same path does not stall.

Add TS_NETSTACK_ENABLE_RACK as an explicit opt-out without changing
the default. Unset keeps gVisor's default, RACK on; true keeps it on
explicitly. Setting it to false brings back the RFC 6675 SACK loss
recovery that netstack used from 1.82 through 1.102, until go.mod has
a gVisor with the fixes (google/gvisor#15432 and google/gvisor#16081).
Keep RACK enabled by default because disabling it is several times
slower on paths that reorder packets (#20893).

Test the default, explicitly enabled, and explicitly disabled cases.

Updates #21946
Fixes #22045

Signed-off-by: Aditya Merchant <adityamerchant08@gmail.com>
Signed-off-by: James Tucker <james@tailscale.com>
Co-authored-by: James Tucker <james@tailscale.com>
Change-Id: I7d8012beed8a5722fb026203625299b5597fdadc
2026-10-29 14:15:48 -07:00
Jordan Whited 5c83ce1b8c wgengine/wgcfg: add wireguard metrics for hybrid handshakes
Updates tailscale/corp#50068
Updates tailscale/corp#48747

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-10-29 13:31:12 -07:00
David Anderson 8af1b389ff go.mod: update wireguard-go
Also includes a minor change to a packet identification helper in magicsock,
to make it correctly identify retried hybrid handshake packets.

Updates tailscale/corp#50068

Signed-off-by: David Anderson <danderson@tailscale.com>
Change-Id: I6204b8f6b60342fe3c0b7b63c4b90c8c6a6a6964
2026-10-29 12:55:25 -07:00
Alex Valiushko ea3b9f2472 wgengine/magicsock: implement conn.MultiSocketBind (#21971)
* wgengine/magicsock: implement conn.MultiSocketBind

Bind a group of UDP sockets sharing one local port per address family. A
flow index is plumbed through the send path to pick which socket a batch
leaves from.

Group size is fixed at Options.UDPSockets for the life of the Conn. In
case rebind comes short, the group is padded out with conns that block
until closed.

No caller sets Options.UDPSockets yet, every group holds a single socket
and this is inert.

Additionally, add test coverage for the now exercised net/sockopts paths.

Updates tailscale/corp#48168

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
Change-Id: I253646c753fa46e2efd0ad2158d24c416a6a6964
2026-10-28 11:09:56 -07:00
Claus Lensbøl 7a3650494f wgengine/magicsock: ensure discoKey nodeKey match (#21956)
The check for finding unambiguous disco pings was not comparing the
disco key and node keys correctly. Fix the error and add a test that
asserts the ping is not seen as unambiguous when the node key does not
match the disco key, and the disco key has multiple matches.

Fixes tailscale/corp#50343

Thank you to Brody Franks for reporting this issue.

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-10-26 14:21:52 -04:00
Alex Valiushko 12e619a070 wgengine/magicsock: apply peer MTU sockopts through net/sockopts (#21855)
And merge the repeated don't fragment codepaths between Linux and Darwin.

Updates tailscale/corp#48168

Change-Id: I5aef408f037c8def99522b4d820c853a6a6a6964
Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-10-26 10:09:21 -07:00
James Tucker 3c0508fa28 net/tstun,wgengine/netstack: avoid deadlocks on full outbound queues
Make link queue writes nonblocking: gVisor can generate replies on the
reader needed to drain the queue. A full queue drops only the rejected
packet and reports ErrNoBufferSpace, which gVisor counts and TCP handles
by arming its retransmit timer. UDP sockets do not see the error unless
IP_RECVERR is set, matching Linux. Count WireGuard-bound drops as
reason="queue_full" in tailscaled_outbound_dropped_packets_total, and
per-queue drops in debug and client metrics.

Add Wrapper.TryInjectOutbound for best-effort replies generated while
processing packets (TSMP rejects and pongs, conn25 rejects, fake-mode
echo replies), so those paths drop instead of stalling behind the
injection queue's reader. Its drops are also counted as queue_full.

Make outbound injection waits cancellable and close link queues before
abort-generated replies. Stop the WireGuard injection pump on ErrClosed.
Release injected packets still queued at Close, outside closeOnce to
allow reentrant release callbacks; a sender whose send races with Close
releases the packet itself. Release temporary capture buffers.

Copy injected payload views directly into the WireGuard slab, avoiding
an intermediate allocation and copy.

Test queue saturation, multiqueue progress, cancellation, concurrent
shutdown, reference ownership, fragmented payloads, and GSO.

Performance versus the parent commit, on a 4-core Xeon 6975P-C:

  inject+read 64B / 1280B       245ns / 479ns -> 187ns / 195ns, 0 allocs
  4 injectors, 1 reader         843ns -> 451ns
  64KiB GSO inject+split        10.9us -> 4.4us, 64KiB/op -> 0
  link queue write+read         81.4ns -> 81.5ns
  tsnet TCP, 1 / 8 streams      448 / 580 MB/s -> 492 / 674 MB/s

BenchmarkTCPQueueBottleneck shows that dropping on full queues keeps
throughput at the bottleneck rate (117 MB/s at 100k packets/s, as with
blocking writes), at the cost of more retransmits with many streams.

Fixes #21631
Updates #21638
Updates #21632
Updates #21639

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-13 15:55:43 -07:00
toppk 8b94ec2d96 wgengine/magicsock: log initial contacts at verbose level (#21766)
Repeated CallMeMaybe messages over DERP can each log a new contact for
the same peer, flooding stderr. Mark the DERP, direct, and control-netmap
latency messages [v1] so they remain in diagnostic uploads but only
reach stderr with --verbose=1.

Updates #12639
Updates #21765

Change-Id: Ic7ee47c699e5698f61358a72c6fd7776e1caf8dc
Signed-off-by: kenneth topp <toppk@bllue.org>
2026-10-13 15:32:58 -07:00
Alex Valiushko e0d21b8a52 health: register arbitrary magicsock receive funcs (#21705)
Health tracker no longer holds a pre-set number of receive func health
items. Magicsock can spawn an arbitrary number of new health items
ad-hoc instead.

A tracker that no magicsock registered with no longer reports a missing
receive func, including builds without UDP transport.

Updates tailscale/corp#48168

Change-Id: I1f929db5a9f056d41a590cebd87a5cfdecf55510

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-10-09 09:57:11 -07:00
Alex Valiushko 76648a00fd net/tstun: implement independent injection queue (#21681)
Injected packets now arrive on a queue of their own rather than being
multiplexed onto queue 0. Wrapper.Read is deprecated. It still reads the
first kernel-backed queue, but from now on no single queue of a Wrapper
sees every packet.

Deletes the poll goroutine, the bufferConsumed token, the vectorOutbound
channel, tunVectorReadResult, a full-size slab, and the copy of every
accepted packet between the two slabs.

wireguard-go now runs an extra TUN reader goroutine, a net wash compared
to running our poll routine.

Measured on c8gn.2xlarge, Ubuntu 24.04, Kernel v6.14, tailscaled under
load from two peers, each running iperf3 -P8.

            tx mean   rx mean   cores
  main       7.00      8.84      3.49
  branch     8.94      8.86      3.95

Bumps wireguard-go pin to tailscale/wireguard-go@e61b6b7811.

Updates tailscale/corp#37878

Change-Id: Ide0e8a9f345371b2747fad59dd2bf963c8af5b63

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-10-08 13:13:11 -07:00
James Tucker 852c3d1f0a feature/exitnodehealth: refine exit node warnings based on responsiveness
Report when an active selected exit node has not answered disco messages for a minute. Include the node name and last response time in the warning.

Evaluate responsiveness from existing magicsock heartbeats and clear the warning when the peer becomes idle or responds. Use changes to nonzero WireGuard RX and TX counters to suppress warnings when traffic is working. Keep observation history and warning evaluation in the exitnodehealth feature. WireGuard-only peers are excluded.

Updates #tailscale/corp#33007

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-07 15:38:22 -07:00
Nick Khyl ab2380a631 net/dns,util/coalescedop: serialize and coalesce DNS flush calls on Windows
Previously, ipconfig /flushdns and ipconfig /registerdns were called
from multiple sites without coordination: the DNS manager's SetDNS
(in a fire-and-forget goroutine), the router's winRouter.Set (synchronously
on every route change), the link change handler, and session unlock.
These could fire concurrently or in rapid succession, spawning multiple
ipconfig processes and putting excessive pressure on the Dnscache service.

In this PR, we introduce a new util/coalescedop.CoalescedOp type that provides
"one in-flight, at most one pending" semantics: Do() is non-blocking,
at most one execution runs at a time, and concurrent/rapid successive
requests coalesce into a single follow-up execution.

We then use CoalescedOp for both ipconfig /flushdns and ipconfig /registerdns,
replacing the fire-and-forget goroutine in windowsManager.SetDNS
and in the session unlock handler in tailscaled.

We also remove the redundant dns.Flush() call from winRouter.Set, since every
meaningful router.Set is followed by dns.Set in Reconfig, which already
flushes.

Updates #21001

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2026-09-30 10:44:22 -07:00
Jordan Whited 3fb612f598 go.mod,wgengine/wgcfg: export wireguard-go replay drops as clientmetric
tailscale/wireguard-go@417aef3 is the only wireguard-go commit
introduced by this dep update.

Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-29 15:30:53 -07:00
Brad Fitzpatrick fe91043315 wgengine/magicsock, net/netcheck, feature/featuretags: add udptransport and nattraversal features
Contining the trend of making everything be optional that could
possibly be optional, this makes the magicsock UDP underlay transport
be optional. (mostly replacing a bunch of GOOS != "js" checks in the process)

Omitting udptransport (ts_omit_udptransport) makes magicsock DERP-only: no
UDP sockets, no advertised endpoints, and netcheck measures DERP latency
over HTTPS. Omitting only nattraversal keeps disco ping/pong to peers'
advertised endpoints but drops STUN, call-me-maybe, the peer relay client,
the endpoint tracker, and UDP lifetime probing.

Both are plain linker dead-code elimination via buildfeatures
constants. (as oppposed to moving the code all over into feature
packages and indirecting through hooks) The minimal linux/amd64
tailscaled shrinks by 258 KB without nattraversal and 561 KB without
both.

Updates #12614

Change-Id: Ife10804b48a168d9481e4321c18ed8fc85fa71f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-29 06:09:17 -07:00
Brad Fitzpatrick 5d35baeb87 wgengine/netstack: don't half-close the backend's read side after EOF
After copying from the backend to the client until EOF, forwardTCP shut
down the backend socket's read side. That does nothing on the wire, and
once the backend's FIN has arrived, macOS rejects shutdown(SHUT_RD) with
ENOTCONN (Linux does too once the socket reaches TIME_WAIT), so every
forwarded connection logged "backend -> client close connection: ...
socket is not connected". Keep only the CloseWrite calls, which are what
propagate the half-close.

The regression was introduced by 04d24cdbd4 (#16462), merged 2025-07-07.

Tested on Linux and on macOS 27 (arm64): the wgengine/netstack tests
pass on both. The same ENOTCONN failure was reproduced on macOS via the
identical pattern in net/socks5 (#21522).

Updates #21522
Updates #16462

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I3e9b1d7f4a2c8e6b0f5d9a3c7e1b4f8d2a6c0e5b
2026-09-29 05:29:48 -07:00
Martin Minkus 27b2a7d1a1 wgengine/router/osrouter: leave pf and forwarding alone in FreeBSD netstack mode
FreeBSD handles subnet routes in netstack by default, but the router
still installed its pf NAT rules whenever SNATSubnetRoutes was set,
which is the default. Every FreeBSD node, subnet router or not, loaded
and enabled pf, inserted anchor references into the host's main
ruleset and loaded NAT rules that netstack never needed, since
netstack dials subnet destinations from the host's own addresses.
It also flipped the forwarding sysctls for any advertised route.

Only do either when the kernel path is opted into with
TS_DEBUG_NETSTACK_SUBNETS=false and routes are advertised.

TestSubnetRouterFreeBSDManyFlows ran in netstack mode since the default
changed, so it no longer exercised pf at all. Opt it into the kernel
path and assert the anchor holds NAT rules, and have
TestSubnetRouterFreeBSD assert the default mode leaves pf untouched.

Also drop the stale claim in handleSubnetsInNetstack that the pf NAT
rule never matches; that was the (self) pool bug, since fixed.

Updates #21450

Change-Id: Ibf91a676a026cebb29f64e39a64fe8e75488360b
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-09-25 12:10:53 -07:00
Jordan Whited 7f51756f2e go.mod,wgengine/wgcfg: add wireguard cookie reply metric
This also pulls in tailscale/wireguard-go@cbffc0a.

Updates tailscale/corp#48747

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-25 11:10:40 -07:00
Nick Khyl d229a06f9a wgengine/router/osrouter: use unspecified NextHop for on-link routes instead of interface IP
On Windows, we used to create all routes using the interface IP address as the next hop.
Notably, on Windows 8 and later, the system normalizes that next hop to 0.0.0.0,
resulting in an on-link route.

In #12847, we stopped creating on-link subnet routes because doing so made
the last IP address in the range unreachable. However, we missed a related issue.
As a result, we continued specifying the interface IP address as the next hop
for Tailscale IP routes.

Because Windows normalizes those routes to use an unspecified next hop,
the routes we read back from the system did not match the routes we expected
to see. This caused unnecessary churn, with the same routes being repeatedly
deleted and recreated.

In this PR, we start creating on-link routes with a normalized, unspecified (all-zeroes)
next hop, as required by the API. This ensures that the routes we read back match
the desired routes, preventing unnecessary churn.

https://web.archive.org/web/20260924154040/https://learn.microsoft.com/en-us/windows-hardware/drivers/network/mib-ipforward-row2

Fixes #21438

Reported-by: Caleb Crome <caleb.crome@flyzipline.com>
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2026-09-24 11:56:24 -07:00
Jordan Whited 8665988efb go.mod,wgengine/wgcfg: add wireguard-go handshake metrics
Updates tailscale/corp#48747

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-23 15:59:28 -07:00
Claus Lensbøl 3f3e56f412 wgengine/netstack: avoid returning from sender goroutines on error (#21332)
Returning from injectToHost and injectToWireGuard leaves the two
goroutines dead and the host without having a way to send traffic.

This is especially relevant for android where the tundev is torn down
and recreated for every call to updateTUN() from a route table change or
roaming between networks.

Fixes #21155

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-09-21 13:56:52 -04:00
Brad Fitzpatrick 1c778640ea net/netmon, ipn/ipnlocal, wgengine: don't lose a network change during startup
If the network comes up during the few milliseconds NewLocalBackend
takes, tailscaled starts with its control client paused and never
unpauses: the interface state snapshot was taken before the netmon
subscription (kept late since #17252), so a change published in between
reached nobody. #21261 hit it on fast-booting NixOS microVMs, and it was
behind the natlab TestEasyEasy CI flake, where a gokrazy node's DHCP
lease landed in that window and "tailscale up" hung at "awaiting
unpause".

Re-read netmon's state after subscribing rather than subscribing before
the snapshot (as #21281 proposed), which would bring back the #17252
data race and let a fresh delta be overwritten by the stale snapshot.
netmon.New and the userspace engine had the same shape of gap between
their snapshot and their subscription; close those too.

Under CPU pressure the hang reproduced in 1 of 22 TestEasyEasy runs
before and 0 of 120 after.

Thanks to @c-vigo for the detailed diagnosis in #21261 and to @zzz-yu
for pointing out this problem and proposing a fix in #21281!

Fixes #21261
Updates #14902
Updates #19126
Updates #deflake

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ifa21f8055a85321a5afda7800140fc5045c6ce08
2026-09-19 10:46:50 -07:00
Naman Sood 35848427ce types/nettype: add HalfCloser type
We have multiple situations where we have a `net.Conn` representing a
TCP connection for a proxy and we need access to the underlying
`CloseWrite()` and `CloseRead()` functions to properly pass through
half-closes (see #16462, #20883). Add an interface we can cast to in
order to get access to these functions.

Updates #20883.

Signed-off-by: Naman Sood <mail@nsood.in>
2026-09-17 17:22:32 -04:00
James Tucker 3ec674bc86 go.mod,wgengine/netstack,tstest/natlab/vnet: enable RACK and cubic with gVisor clock fix
Update gVisor to include its fix for RACK loss detection with coarse
monotonic clocks. Configure netstack with the 500 microsecond clock
resolution used on Windows so RACK accounts for timestamp quantization.

Remove the TCP recovery override that disabled RACK, enabling gVisor's
default RACK behavior on all platforms.

Switch netstack to cubic congestion control. The int overflow in CUBIC
sender cwnd arithmetic that required pinning reno has since been reworked
upstream into float arithmetic with RFC 9438 target clamping.

Align the natlab vnet stack with netstack: enable cubic, and drop the
now-redundant explicit SACK and receive-buffer moderation sets, both of
which are gVisor defaults.

Fixes #9707

Signed-off-by: James Tucker <jftucker@gmail.com>
2026-09-15 17:15:35 -07:00
Brad Fitzpatrick 2a4ce5ba3b wgengine/magicsock, net/udprelay: reject zero relay disco keys
A tailnet peer can remotely crash tailscaled by sending a DERP-sealed
disco CallMeMaybeVia message with an all-zero ServerDisco key. The
decoder accepts the zero key, and the relay manager later hands it to
DiscoPrivate.Shared, which panics on zero keys. The sender only needs
to be a relay-capable peer in the victim's netmap.

Auditing the other DiscoPrivate.Shared call sites reachable from
decoded messages turned up the same bug on the relay server side.
AllocateUDPRelayEndpointRequest.ClientDisco is attacker-chosen: one
slot must match the sender's disco key, and the other can be zero. It
flows unchecked into udprelay.Server.AllocateEndpoint, which calls
Shared on both client keys and panics in its eventbus subscriber
goroutine. AllocateEndpoint now rejects zero client keys with an error,
which its only caller already handles by logging.

Thanks to Ben Carman for the report!

Updates tailscale/corp#48187

Reported-by: Ben Carman
Change-Id: Ifc0f64d8f63270100b22c06e9f759dce624ab811
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-14 12:05:26 -07:00
Jordan Whited 960cb50469 wgengine/wgcfg: size crypto & per-peer queues by CPU factor
To better balance throughput vs backlogged packet memory consumption.

These changes reduce peak RSS in UDP TX benchmarks that previously
regressed in 8fc6dca. All other benchmarks remain largely unchanged.
Netstack benchmarks are new, with back-tested results from 1e69418.

The following throughput and peak RSS benchmarks were performed with
iperf3 between two Intel i5-12400 nodes running Ubuntu 24.04 (Linux 6.8).

The UDP benchmarks did not use UDP GSO on the sender, so they were
roughly equivalent to single packet I/O through wireguard-go.

TCP/1 signifies one TCP stream; TCP/128 signifies 128 parallel TCP
streams. -NS signifies netstack, and -RX/-TX signify the side netstack
was running on, with the other being a natural TUN.

Change column is calculated against the oldest baseline (1e69418).

Throughput (Mb/s)
  Test            1e69418      8fc6dca  queue-size   Change
  TCP/1            10,371       11,354      11,407   +9.98%
  TCP/1-NS-TX       9,225       10,553      10,481  +13.61%
  TCP/1-NS-RX       8,573        9,097       9,002   +5.00%
  TCP/128           7,886        8,404       8,427   +6.85%
  TCP/128-NS-TX     4,613        6,408       5,448  +18.10%
  TCP/128-NS-RX     6,730        8,524       8,279  +23.02%
  UDP/1             2,111        2,853       2,751  +30.33%
  UDP/1-NS-TX       2,022        1,941       1,904   -5.85%
  UDP/1-NS-RX       2,087        2,444       2,261   +8.35%
  UDP/128           1,747        2,235       2,216  +26.87%
  UDP/128-NS-TX     1,325        1,615       1,424   +7.47%
  UDP/128-NS-RX     1,743        2,219       2,240  +28.50%

Peak memory (VmHWM, kB)
  Test           Side    1e69418      8fc6dca  queue-size   Change
  TCP/1          TX       98,240       52,596      52,508  -46.55%
                 RX      287,748       73,384      65,404  -77.27%
  TCP/1-NS-TX    TX      254,196       85,116      76,076  -70.07%
  TCP/1-NS-RX    RX      434,952       70,420      70,916  -83.70%
  TCP/128        TX      101,196       52,812      51,740  -48.87%
                 RX      290,420       63,620      62,716  -78.41%
  TCP/128-NS-TX  TX    1,499,200    1,199,304     821,740  -45.19%
  TCP/128-NS-RX  RX    3,945,960      475,276     153,516  -96.11%
  UDP/1          TX       58,864      160,840      58,060   -1.37%
                 RX      137,516       49,900      51,732  -62.38%
  UDP/1-NS-TX    TX      214,924      282,400      59,956  -72.10%
  UDP/1-NS-RX    RX      112,152       49,004      49,108  -56.21%
  UDP/128        TX       66,148      116,096      57,344  -13.31%
                 RX      154,384       56,556      57,572  -62.71%
  UDP/128-NS-TX  TX      187,096      225,080      88,628  -52.63%
  UDP/128-NS-RX  RX      156,108       74,556      75,952  -51.35%

Updates tailscale/corp#46716
Updates tailscale/corp#22467
Updates tailscale/corp#36989
Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-09 15:35:16 -07:00
Jordan Whited 2c30be2a4a wgengine/netstack: split outbound traffic into destination queues
Route packets to dedicated WireGuard, host, and loopback queues when
gVisor writes them. Drain each queue in a separate goroutine, preparing
the outbound path for batching and multiqueue WireGuard delivery.

Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-09 13:42:48 -07:00
James Tucker 85c1efb466 net/{connreject,tstun},feature/connreject,wgengine,client/local: add connection-rejection diagnostics
Adds an opt-in, in-memory aggregator of recent connection-rejection
events (TSMP rejects received from peers, outbound TSMP rejects we emit
on ACL-blocked inbound flows, and pendopen timeouts) keyed by
(direction, proto, peer-address, reason). The aggregated data is exposed
over a new debug-rejects LocalAPI endpoint and a GET /debug/rejects c2n
endpoint, intended for future GUI/CLI consumption when diagnosing why a
connection failed.

Architecture:
 - net/connreject holds the data types and a per-LocalBackend
   Aggregator (LRU-bounded, default 256 entries on desktop / 32 on
   mobile, per direction).
 - feature/connreject is a self-registering ipnext.Extension that owns
   one Aggregator per LocalBackend, installs note callbacks on the
   tundev and engine, subscribes to OnSelfChange to flip the runtime
   gate, and serves the LocalAPI/c2n endpoints.
 - wgengine.Engine and *tstun.Wrapper each gain a SetConnRejectNote
   setter; data-plane sites use a single atomic.Pointer load + nil
   check, so the cost when no consumer is installed is one MOV.

Gating:
 - Compile-time: ts_omit_connreject build tag (standard
   feature/buildfeatures + condregister plumbing). Trims ~41 KB.
 - Runtime: nodecap.ConnReject node attribute, off by default
   at the control plane. May be removed once the feature is enabled
   by default.

Updates CapabilityVersion to 146 (clients understand nodecap.ConnReject
and can serve GET /debug/rejects).

Adds Proto/Src/Dst accessors on flowtrack.Tuple (used by pendopen to
construct events without exposing the tuple's internals to the
aggregator).

Updates #1094
Updates #14802

Change-Id: I83e8f24a7e66fa2d158d128bd25fbe851134941b
Signed-off-by: James Tucker <james@tailscale.com>
2026-09-08 16:22:43 -07:00
Jordan Whited 8fc6dca15e all: use ~128KiB packet buf for batched i/o
This commit bumps the wireguard-go dependency to incorporate changes to
the packet memory model and the tun.Device.Read and conn.ReceiveFunc I/O
interfaces. It updates their implementations accordingly.

These changes improve throughput in all measured benchmarks and reduce
peak RSS in six of eight cases. The two regressions will be addressed in
a follow-up commit that reduces peak RSS below the baseline measured at
1e69418. That work is kept separate to simplify review.

The following throughput and peak RSS benchmarks were performed with
iperf3 between two Intel i5-12400 nodes running Ubuntu 24.04 (Linux 6.8).

The UDP benchmarks did not use UDP GSO on the sender, so they were
roughly equivalent to single packet I/O through wireguard-go.

TCP/1 signifies one TCP stream; TCP/128 signifies 128 parallel TCP
streams.

Throughput (Mb/s)
  Test     1e69418  After  Change
  TCP/1    10,371  11,354   +9.5%
  TCP/128   7,886   8,404   +6.6%
  UDP/1     2,111   2,853  +35.1%
  UDP/128   1,747   2,235  +28.0%

Peak memory (VmHWM, kB)
  Test     Side   1e69418    After   Change
  TCP/1    TX      98,240   52,596   -46.5%
           RX     287,748   73,384   -74.5%
  TCP/128  TX     101,196   52,812   -47.8%
           RX     290,420   63,620   -78.1%
  UDP/1    TX      58,864  160,840  +173.2%
           RX     137,516   49,900   -63.7%
  UDP/128  TX      66,148  116,096   +75.5%
           RX     154,384   56,556   -63.4%

Updates tailscale/corp#46716
Updates tailscale/corp#22467
Updates tailscale/corp#36989
Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-04 09:32:55 -07:00
Claus Lensbøl 33cc45a32a wgengine/magicsock: fix logging for changing disco keys (#21105)
Upates to controlClient learned keys was logged as coming from TSMP.

Also, nil changes on nil keys were not filtered.

Updates #20494

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-09-04 08:03:40 -04:00
Brad Fitzpatrick 31d8badb3b wgengine: support per-peer WireGuard PSKs
Replace the allowed-IP-only peer callback result with wgcfg.PeerConfig.
It carries allowed IPs and an optional pre-shared key through lazy peer
creation and active peer synchronization.

Update wireguard-go for the new peer PSK APIs.

Updates tailscale/tailcat#84

Change-Id: Iacd9d2c74b0b64d690f3cbdf93918686ac6076d7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-03 20:04:09 -07:00
Martin Minkus 16dacb0c50 wgengine/router/osrouter: don't destroy the FreeBSD tun from Close
freebsdRouter.Close ran "ifconfig tailscale0 destroy" while this process
still holds the tun open: the engine closes the router before the tun
device, and FreeBSD's tun(4) makes SIOCIFDESTROY sleep until the last
descriptor closes. tailscaled therefore deadlocks against its own child
ifconfig on every shutdown, and "service tailscaled restart" hangs
forever:

  51709  -  I   /usr/local/bin/tailscaled -port 41641 -tun tailscale0 ...
  52455  -  I   ifconfig tailscale0 destroy
  52403  1  I+  /bin/sh /usr/local/etc/rc.d/tailscaled restart

Do only the PF cleanup at Close time. The interface left behind at
process exit is destroyed by the next startup's cleanUp hook
(router.HookCleanUp), which exists for exactly that and runs when
nothing holds the device.

Updates #5573

Change-Id: I7b739d948718bfb460240d2c0dd74658fc40ac82
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-09-03 12:40:44 -07:00
Claus Lensbøl f53c28101a control/controlclient,ipn/ipnlocal,wgengine/{,magicsock}: route disco keys learned over TSMP directly to magicsock (#21016)
Previously, the new disco keys entering the client via TSMP was routed
into controlClient to allow for deduplication and filtering of disco
keys and avoid control overwriting an active TSMP learned key with a
stale key.

A system supporting multiple disco keys was introduced to allow these
keys living side by side, along side a system for selecting an active
egress key with a bias towards keys learned via TSMP. On ingress any
known key is accepted.

This PR makes the switch to key routing, by sending new TSMP learned
disco keys directly into the userspace engine and in turn magicsock,
removing the need for a full layer of deduplication in the
controlClient.

Additionally, the system that previously fully reconfigured clients on
disco key updates coming from control is now using an optimistic
handshake througha newly introduced wireguard-go method,
ScheduleHandshakeOnUserSend implemented in:
https://github.com/tailscale/wireguard-go/pull/81

What this PR does not do is revert the mapSession back to being single
writer. Bringing the mapSession back to this state is desired, however
to ensure the plumbing is done right and easier to reason about, that
will be done in a separate PR.

Updates #20590

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-09-03 09:38:28 -04:00
Mike Jensen 0188b38666 fuzz: refactor fuzzing to new layout for oss-fuzz support (#21069)
Prior to this change there were two problems with our fuzzing for oss-fuzz:
  1. There was an issue if the fuzzing spanned two files (mingled with the testing).
  2. The fuzzing needs to be part of the implementation package (no _test packages).

This change fixes that by moving all package fuzzing into a common `fuzz_test.go` within the package.

Updates https://github.com/tailscale/corp/issues/46608

Change-Id: I0b95edcd0df946f723eea32f575c679214c0b202

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-01 13:15:45 -06:00
Claus Lensbøl e1d17a6b99 wgengine/magicsock: dynamically switch active disco key on recv (#20984)
When receiving disco traffic from a node, mark that node as having been
seen. Should that disco key not be the active use disco key, switch to
that one as being the active key. Additionally, clear states on the
magicsock connection and prepare for sending a new WG handshake whenever
user data is transmitted.

Sets up for:
 - Routing TSMP keys directly into magicsock
 - Switching the active connection reset mechanism to the optimistic
   handshake
 - Cleaning up paths into controlClient

Updates #20494

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-09-01 11:18:25 -04:00
Martin Minkus 4e80553a9d wgengine/router/osrouter: only remove PF anchor refs this process added
removePFAnchorRef ran at shutdown (and from the startup cleanup hook) and
unconditionally rewrote the main PF ruleset to strip the "tailscale"
anchor references. That is wrong twice over for an operator who
configured those references statically in /etc/pf.conf -- the durable
setup the ensurePFAnchorRef error message recommends:

  - it drifts the running ruleset away from /etc/pf.conf on every
    tailscaled shutdown, and
  - the rewrite reconstructs the ruleset from "pfctl -s" output, which
    names PF tables but never their contents, so any table the operator
    populates out-of-band is silently emptied (same failure mode
    ensurePFAnchorRef now refuses).

Track whether this process inserted the references and only remove what
we added, and even then leave them alone if the ruleset now references
tables. Skipped removal is harmless: callers flush the anchor's contents
first, and a reference to an empty anchor has no effect on traffic. The
flag is process-local by design, so the cross-process startup cleanup
hook never removes references it cannot prove are tailscaled's.

The only-remove-what-we-added semantics were first identified and
implemented by Ross Williams (@overhacked) on a fork of this branch;
this is an independent implementation of the same idea alongside the
table guard.

Updates #5573

Change-Id: Idf579a8b4190c37211782feac4e5bfd057c3ace0
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 20:31:01 -07:00
Martin Minkus 4c5862376c wgengine/router/osrouter: don't clobber PF tables when adding anchor refs
ensurePFAnchorRef reconstructs the main ruleset from "pfctl -sn" and
"pfctl -sr" output and reloads it with "pfctl -f -". That output names any
table a rule references but never prints its contents, so the reconstructed
ruleset re-declares every table as empty. Reloading it silently drops the
addresses of any table the operator populates out-of-band -- a "persist file"
table, "pfctl -T add", pfctl's own automatic tables for interface groups --
and every rule referencing that table then matches nothing.

That is a quiet, security-relevant failure: a ruleset whose "pass ... from
<trusted>" rules still exist but match no addresses looks fine in "pfctl -sr".

There is no way to insert an anchor reference into a running ruleset without
a reload, so detect the case and refuse, pointing the operator at the durable
fix (putting the anchor references in /etc/pf.conf, where they survive reboots
and pf reloads anyway).

Boxes that already have the anchor references configured are unaffected:
ensurePFAnchorRef returns early before reaching this check.

Updates #5573

Change-Id: I6efa9a63e1322af2ec8fd986a774ed76c5d5766e
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 20:31:01 -07:00
Martin Minkus 827c6fe50d wgengine/router/osrouter: SNAT to the egress interface address, not (self)
The FreeBSD subnet-router NAT rule translated with "-> (self)":

  nat on ! tailscale0 inet from 100.64.0.0/10 to any -> (self)

In pf, "(self)" is a round-robin pool of every address on the machine,
including tailscale0's own address and loopback, and pf deals each new
state the next address in the pool. Only flows that happen to draw the
egress interface's address work; a flow translated to any other address
gets replies the far end cannot route, and hangs at SYN. With N usable
addresses on the box, roughly (N-1)/N of connections through the subnet
router silently fail.

Observed in a natlab vmtest against a FreeBSD 15.0 subnet router with
four addresses (WAN, LAN, QEMU debug NIC, tailscale0): exactly half of
8 HTTP requests hung, alternating, and the pf state table showed the
failed flows translated to the debug NIC's address and to tailscale0's
own address:

  10.0.0.102:51100 (100.64.0.1:35132) -> 10.0.0.103:8080  ESTABLISHED
  10.0.2.15:56553  (100.64.0.1:35148) -> 10.0.0.103:8080  SYN_SENT:CLOSED
  100.64.0.2:52655 (100.64.0.1:54106) -> 10.0.0.103:8080  SYN_SENT:CLOSED

On a production FreeBSD firewall running this branch, the equivalent
IPv6 rule shows 55 state creations totalling 117 packets (about two
packets per state): SYNs whose replies never came back.

Emit one rule per up, non-loopback, non-Tailscale interface instead,
translating to that interface's own address, per address family only
where the interface holds a usable address of that family:

  nat on vtnet0 inet from 100.64.0.0/10 to any -> (vtnet0)
  nat on vtnet1 inet from 100.64.0.0/10 to any -> (vtnet1)

which is also the rule form FreeBSD firewall operators write by hand.
With this, the same 8-request test passes 8/8, and the LAN interface's
rule shows 8 states with healthy packet counts (56 packets total).

The interface set is sampled when SNAT is enabled; interfaces added
later are not covered until SNAT is toggled or tailscaled restarts.

Updates #5573

Change-Id: Ife3367124737ce5c8785ca7f920eafca593ec705
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 20:31:01 -07:00
Brad Fitzpatrick 1293b4f67b wgengine/router/osrouter: add native FreeBSD routing, no-snat support
And add site-to-site natlab vmtests for Linux + FreeBSD.

Fixes #5573
Updates #18897

Change-Id: I9629f46fdd99137f0ae96431487d3a7410e2a4cc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-31 20:31:01 -07:00
Brad Fitzpatrick 2a4d74356b wgengine/netstack: fix data race on test logger in makeHangDialer
The dialer returned by makeHangDialer runs on netstack-owned goroutines
that can outlive the test, so calling tb.Logf from it raced with the
test completing. Use tstest.WhileTestRunningLogger, which stops logging
once the test is done, as makeNetstack already does.

Fixes #21052

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ibbf4288eff48f3ae0bb5812204a1cdfd567322a3
2026-08-31 09:19:31 -07:00
Claus Lensbøl da1fc4fc8c wgengine/magicsock: add facilities to support multiple disco keys (#20927)
Instead of accessing the key and string directly, put it behind a method
to make it easier to use that as a proxy when we add multiple keys
(control and TSMP origin).

Instead of only updating a single origin for disco keys, teach peermap
how to work with multiple disco key sources so a key can originate from
either control or TSMP.

This sets the work up for switching dynamically between disco keys
later. As of this commit, the keys still arrive for the most part via
the controlClient, but this commit sets us up for:
 - Switching dynamically between received keys.
 - Route TSMP keys directly into magicsock, not via controlClient.
 - Potentially revert controlClient to the state before any TSMP
   changes, making it single threaded.
 - Use switching keys as trigger for optimistic WG handshakes, instead
   of tearing down the full connection and setting it up again.

Updates #20494

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-08-25 09:55:48 -04:00
Alex Valiushko 65d2226742 wgengine/magicsock: reset endpoint trust on rekey (#20368)
Invalidate the trust window so we re-run path discovery
instead of coasting on the dead path until trust lapses on its own.

Partially reverts 85bb5f8 removal of resetLocked within updateFromNode.

Fixes #20268

Change-Id: Ib18b31e4329513294b682e698d1a9f426a6a6964
Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-08-24 14:25:25 -07:00
Simon Law 0e84b4a3a0 tailcfg: replace int with DERPRegionID for additional type safety (#20646)
Historically, when DERP regions were switched away from strings to
numeric identifiers in PR #14641, tailcfg.Node.HomeDERP was declared
as an int instead of its own type.

This PR declares a new tailcfg.DERPRegionID type, represented by an
int64, and converts the following fields to use this type:

- netcheck.Report.PreferredDERP
- netcheck.Report.RegionLatency
- netcheck.Report.RegionV4Latency
- netcheck.Report.RegionV6Latency
- tailcfg.DERPHomeParams.RegionScore
- tailcfg.DERPMap.Regions
- tailcfg.DERPNode.RegionID
- tailcfg.DERPRegion.RegionID
- tailcfg.NetInfo.PreferredDERP
- tailcfg.Node.HomeDERP
- tailcfg.PeerChange.DERPRegion
- tailcfg.PingResponse.DERPRegionID

Note that the original field was an int, while the new field is backed
by an int64. This change makes DERPRegionID the same size on both
32-bit and 64-bit architectures.

Fixes: #20165

Change-Id: Ic6f795a6d791dd16f756f246d5a02085443e212f

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-08-19 16:25:50 -07:00
Mike Jensen 90ed0bcf4b wgengine/netstack: block 4via6 forwards to host-scoped targets (#20866)
The packet filter only sees a via address's outer ULA, so netstack unconditionally dialed whatever IPv4 was embedded in it.

This change refuses TCP, UDP, and ping relays to host-scoped destinations after UnmapVia.

Credit to the Anthropic infrastructure security team for finding and reporting.

Fixes https://github.com/tailscale/corp/issues/46646

Change-Id: I93d8e27a2eddbce7eb3f8c5fa4677f8de3a8ed9e

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-08-19 09:16:08 -06:00
Alex Chan 468241a13f envknob: add SetenvForTest helper for safe environment variable handling
Our tests are inconsistent in how they set environment variables and cleanup.
Missing cleanup logic can leak environment variable state across tests and
change the behaviour of subsequent tests.

This recently caused flakes in feature/acme, where `TestGetCertPEMWithValidity`
leaked `TS_CERT_SHARE_MODE` and `TestAsyncRenewalDedup` to fail inconsistently.

To fix the immediate flake and prevent future leaks, introduce a `SetenvForTest`
helper that handles setting and cleaning up environment variables in tests.

Fixes #20902

Change-Id: I9f8ef45ec875d66034cfd0054311bd69b67e2243
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-08-19 12:22:26 +01:00
James Tucker 0d66cde14f tstest/membudget: add live-heap budget regression tests
Static size checks (iossize) catch binary dirty-page growth but nothing
covered runtime heap cost, which is what actually consumes the iOS
Network Extension's 50 MiB jetsam budget. Bring up a tsnet backend
(with the full condregister feature set, matching shipping clients)
against an in-process testcontrol server and assert live post-GC heap
budgets for (a) backend startup with zero peers and (b) marginal cost
per netmap peer.

The startup test measures 1.3 MiB today and fails loudly on the
conn25 flow-table pre-allocation regression (17 MiB) that jetsam-killed
the iOS extension on large tailnets.

Budgets are deliberately generous (6-12x current measurements) to stay
flake-free while still catching the multi-MiB regressions that matter
for mobile.

A new debugknob enables us to constrain the GSO/GRO batch size to 1 for
these tests so as to avoid the memory allocation associated with those
buffers, which are a known issue with their own work stream.

Updates tailscale/corp#46408
Updates tailscale/corp#18514

Signed-off-by: James Tucker <james@tailscale.com>
2026-08-14 16:45:04 -07:00
Jordan Whited ab0489912f go.mod,wgengine/wgcfg: bump wireguard-go for device.Option
Move away from mutating global, exported variables in wireguard-go. Use
device.Option's passed to device.NewDevice, instead. No functional
changes, just API cleanup in preparation of future changes.

Updates tailscale/corp#22467
Updates tailscale/corp#46396
Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-08-13 15:05:36 -07:00