Commit Graph
11408 Commits
Author SHA1 Message Date
Brad Fitzpatrick bcc7e46f42 tstest/integration/testcontrol: add experiment.reco implementation
Select the published reco-backed test server with a build tag while
preserving the default implementation and existing caller API. Pin the
public module with its proxy-verified checksums; no workspace is needed.

Accept incremental peer messages in the TSP test. Exercise legacy map
semantics with the default server and version-floor rejection with reco,
including current-version streaming, without manual test skips.

Document GOFLAGS usage so child Go builds inherit the experiment tag.

Updates tailscale/corp#49097

Change-Id: I9b3e086c4c6a9a57a076e76b883422875c843f20
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-29 10:25:02 -07:00
Kristoffer Dalby cf7d4c6015 ipn/ipnlocal: report peers dropped by a full netmap as PeersRemoved
Peer-change watchers upsert PeersChanged, so peers missing from a full
netmap were never removed from their view.

Updates #15660

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-09-29 19:23:51 +02:00
Kristoffer Dalby c4a66d1ce1 ipn/ipnlocal, tstest/largetailnet: test removals bundled with full netmaps
A peer removal sent with any MapResponse field that forces a full netmap
rebuild never reaches IPN bus watchers; these tests fail.

Updates #15660

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-09-29 19:23:51 +02:00
Brad Fitzpatrick 5758b2aa57 derp/derpserver: add app filter, connected-time sort, and JSON output to /debug/clients/
Add three things to the connected clients debug page:

app=NAME narrows any of the existing filters to connections that
advertised that app name, and may be repeated to match any of several
(app=tailcat-server&app=tailcat-client). On its own it applies to all
connections. An empty app= matches connections that sent no app name.
App names in the table link to their filter, and the next-page and
sort links carry the app filter along.

sort=connected walks by connection time, ascending being longest
connected first, with -connected for newest first. The next-page
links use the connection time in Unix nanoseconds as the cursor; by
hand, after= also accepts a duration such as 30m, meaning connections
that have been up that long, which is the natural way to ask for
"everything older than half an hour".

format=json returns the page as a JSON object with the filter
description, the matching connection and key counts, how many
connections remain after the page, the next page's relative URL, and
the client rows, so the page can be walked from curl or a script the
same way a browser follows the next links. Rows gain a connectedAt
timestamp alongside the rounded connected duration.

Updates tailscale/corp#48933

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ib5d2e7c40a9f13e8a6d7c2b5f9e0a4d3c8b17e62
2026-09-29 09:18:15 -07:00
Brad Fitzpatrick b8dc4061b0 net/socks5: don't half-close the read side after EOF, tear down on error
Shutting down a socket's read side after its peer's FIN has arrived does
nothing on the wire and fails with ENOTCONN on macOS (and on Linux once
the socket reaches TIME_WAIT), so every cleanly half-closed connection
was logged as a failure and TestTCPHalfClose failed on macOS. Only
half-close the destination after EOF.

When one direction fails, one of the two sockets is dead. If the failure
was writing to it, the other direction is reading from that dead socket
and finishes on its own once it has drained what arrived before the
failure, so leave it alone rather than truncate the data. If the failure
was reading from it, the other direction may be blocked reading the live
peer, so close both connections to unblock it; nothing deliverable is
lost in that case.

Telling the two apart means wrapping the destination, which disables the
kernel splice fast path when both ends are bare TCP connections (about a
third of relay throughput on Linux loopback). That is accepted here: the
common userspace-networking and proxymux configurations wrap the
connections anyway, so they never had the fast path.

The regression was introduced by 027e249fcf (#21359), merged 2026-09-17.

Tested on Linux and on macOS 27 (arm64): the old test failed 49 of 50
runs on macOS; the new tests pass 200 iterations there and pass under
the race detector on Linux.

Fixes #21522
Updates #20883

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I7c2e4a9d1b8f5e3c6a0d2f4b8e1c9a7d3f5b6e2c
2026-09-29 08:50:22 -07:00
Michael Ben-Ami (on Claude) a0e471a35b feature/conn25: split client and connector code into their own files
conn25.go had grown to hold the extension, the Conn25 type, the config,
the DNS rewriting, and both the client and connector implementations.
Move the client struct and its methods to client.go, and the connector
struct and its methods to connector.go.

appAddr, transitIPExpiryEntry, and connectorTransitIPExpiry are used
only by the connector, so they move to connector.go. addrs moves to
client.go because it is a client-side concept, even though the
extension's send path in conn25.go and addrAssignments.go also refer
to it. Everything else stays in conn25.go.

Test functions covering client and connector behavior move to
client_test.go and connector_test.go correspondingly.

This is a pure code move: no declaration is added, removed, or edited,
and the relative order of everything is unchanged.

Updates #cleanup

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-09-29 10:21:45 -04:00
chaosinthecrd 06e3480a85 cmd/k8s-operator: clean up HA Service when proxy-group annotation removed
When the tailscale.com/proxy-group annotation is removed from a Service
that was exposed on a ProxyGroup, the HA Service reconciler returned early
on the empty annotation before reaching its cleanup path. The Tailscale
Service was left advertised and the operator's finalizer was never removed,
so the Tailscale Service leaked and the Kubernetes Service wedged forever in
Terminating once deleted.

The annotation is the only place the ProxyGroup name was recorded, and it is
gone by the time cleanup needs it. This commit encodes the ProxyGroup name in
the finalizer, which survives any modification of the resources annotations.
On reconcile, an empty annotation with our finalizer present now recovers the
ProxyGroup name from the finalizer and runs cleanup. Services carrying the bare
legacy finalizer are migrated to the encoded form while the annotation is still
present.

Updates #19922

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-09-29 14:55:00 +01: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
chleeandJordan Whited dca60dd77d net/batching: preserve IPv6 zone in WriteBatchTo
WriteBatchTo copied the destination IP and port into the pooled
net.UDPAddr handed to sendmmsg(2), but not the zone. For an IPv6
link-local destination the sockaddr then had no scope ID, and Linux
failed the send with "cannot assign requested address". Disco goes
through WriteToUDPAddrPort, which keeps the zone, so magicsock could
pick a link-local path whose WireGuard packets, sent via WriteBatchTo,
never left the host.

Set the zone on every write, so that a destination without one also
clears the zone left over from an earlier write that reused the
pooled address.

Fixes #21411

Co-Authored-By: Jordan Whited <jordan@tailscale.com>
Signed-off-by: chlee <sourcehatchery@gmail.com>
Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-28 20:48:51 -07:00
Brad Fitzpatrick b4e3a5299c cmd/tailscaled, safesocket: add --windows-mode=dev
This adds a tailscaled string flag "--windows-mode" which accepts two
possible values: the empty string (default) to get the normal behavior
(tailscaled running as an admin, usually as a service), and "dev", to
make the safesocket named pipe path be at a location that regular
users (non-admins) can create.

It then modifies the safesocket client side (as used by the CLI) to
try the dev mode path too on failure.

This lets people work on tailscaled.exe+tailscale.exe in a terminal
easily during development, as either an admin or non-admin. (This
used to work prior to the move away from TCP localhost to named pipes
for safesocket on Windows)

Updates #2791

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I9c1e3a5b7d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c
2026-09-28 13:26:52 -07:00
Brad Fitzpatrick 48ec6284d3 safesocket: let a non-administrator listen on the Windows named pipe
The pipe's security descriptor named the Administrators group as the
pipe's owner (O:BA) and primary group (G:BA). Only administrators may
assign that SID as an owner, so a tailscaled run by a regular user (in
userspace-networking mode with its own --socket) died at startup with
"This security ID may not be assigned as the owner of this object". The
tests already had to swap in an empty descriptor to run unelevated.

Neither part did anything for us. The primary group is never consulted by
Windows access checks; it exists only for POSIX and NFS compatibility. The
owner grants exactly one thing beyond the DACL: the right to read and
rewrite the pipe's DACL. Whoever creates the pipe already has that in
practice. For the default pipe name under ProtectedPrefix\Administrators,
the creator must be an administrator (only administrators may create pipes
with that prefix), so naming Administrators as owner gave them nothing
new. A regular user creating a pipe of some other name runs the process
that owns it and controls its DACL anyway. Who may connect is decided only
by the DACL, which is unchanged: read and write for Users and LocalSystem.

So drop the owner and group and let the creator own the pipe. While here,
drop the AI, OI, and CI flags too: they concern inheritance to child
objects, which a named pipe doesn't have, and were copied from a file
system descriptor. The tests now use the real descriptor. Also give a
pointer when listening on the default ProtectedPrefix\Administrators pipe
fails with access denied.

Updates #2791

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I8a1c3e5f7b9d2e4a6c8b0d2f4e6a8c0b2d4f6a8c
2026-09-28 13:26:52 -07:00
Basavaraj S mandMike Jensen a40d145b21 derp/derphttp: reject invalid DERP node hostname before proxy CONNECT (#21038)
* derp/derphttp: reject invalid DERP node hostname before proxy CONNECT

When a DERP client reaches a node through an HTTP(S) proxy,
dialNodeUsingProxy writes the CONNECT request by hand and puts
net.JoinHostPort(n.HostName, port) into both the request line and the
Host header. n.HostName comes from the control-supplied DERP map and
net.JoinHostPort does no sanitizing, so a hostname carrying CR/LF was
written verbatim into the plaintext request sent to the proxy. That let
whoever populated the DERP map inject extra headers, or a second
pipelined request, into the connection to the operator's proxy.

Validate n.HostName with httpguts.ValidHostHeader at the top of
dialNodeUsingProxy, before the proxy is dialed, and also reject the
empty hostname, which ValidHostHeader accepts. DNS names and IP
literals continue to work.

Add a table-driven test that runs accepted and rejected hostnames
against a fake proxy and checks the CONNECT target that goes out.

Fixes tailscale/corp#48122

Signed-off-by: basavaraj-sm05 <basavaraj@digiscrypt.com>
Co-authored-by: Mike Jensen <mikej@tailscale.com>
Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-28 12:56:36 -06:00
Mazdak Nasab ecedffff5b feature/conn25: unmap addresses in TransitIPRequest
Unmap both transit and destination IPs in
TransitIPRequest to prevent unpredictable mappings.

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

Change-Id: I5aea2bf4e5ff5240c66f20c71cfa1ceba1722622
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-28 11:11:52 -07:00
Mazdak Nasab 8a119e1ec0 feature/conn25: skip AAAA records with IPv4-in-IPv6 addresses
Skip destination IPs from AAAA records that are
in ipv4-in-ipv6 format.

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

Change-Id: I5eeaa9c998c79250d2e6a943fa9f26ed7feb5ec6
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-28 11:11:52 -07:00
Brad Fitzpatrick 7bb87d3614 derp/derpserver, cmd/derper: add /debug/clients/ page listing connected clients
The derper debug pages had no way to see which clients were connected.
The expvar gauges only give counts, /debug/check only says whether the
counts agree, and /debug/traffic only reports connections that moved
bytes since its last tick, and only if ss is installed.

Add /debug/clients/, which by default serves an index page with a form
to pick one of four filters: ?all lists every connection, ?ip=1.2.3.4
and ?cidr=1.2.0.0/16 list connections from an address or prefix, and
?key=nodekey:... lists the connection(s) for one node key. Each row
shows the connection number, key, remote address, connection age,
flags (home, mesh, prober, notideal, dup/active/disabled), protocol
version, app name, per-connection rx/tx packet and byte counts, and
the estimated unique sender count.

Big derpers have far too many connections for one page, so results
are paginated with keyset cursors rather than page numbers: sort=key,
ip, conn, rx, tx, rxpkts, or txpkts (with a leading - for descending)
picks the walk order, limit=N the page size, and after=X resumes after
that value of the sort field. The next-page links add afterconn=N so a
page boundary that falls among connections sharing a value (duplicate
keys, one IP with many ports, equal counters) resumes exactly. Column
headers link to the other sort orders.

The walk under Server.mu does only a filter match, a cursor comparison,
and at most a bounded-heap operation per connection, so connections
before the cursor are discarded without being copied and at most limit
entries are ever kept. Only the summary counts (matching connections
and keys) look at every connection. Snapshots are taken and the page
rendered after the lock is released, so a slow debug client can't
stall the server. A benchmark with 100k connections takes about 10ms
per page.

There were no per-connection traffic counters before, only the
server-wide ones, so sclient gains four atomic.Uint64 counters (rx/tx
packets and bytes, counting data packets like the server-wide ones)
bumped alongside them. That's 32 bytes per connection. For the counter
sorts, the value is loaded once per connection during the walk and
used for both the cursor test and the heap order, so the order stays
consistent while the counters keep changing.

The sclient preferred field becomes an atomic.Bool so the page can
report which connections are the client's home DERP; it was previously
only touched by the run goroutine.

Updates tailscale/corp#48933

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I4e9b7c2d5a83f61b0e7d2c94a5f8b3e16d7c0a29
2026-09-28 10:14:36 -07:00
Dep Updater 48c7fbcc56 go.mod: bump 4 modules
* github.com/gokrazy/firmware: https://github.com/gokrazy/firmware/compare/527ce0ed43cf...f966a2e0e4b0
* github.com/gokrazy/kernel.amd64: https://github.com/gokrazy/kernel.amd64/compare/21771250d660...5a347b66f180
* github.com/gokrazy/kernel.arm64: https://github.com/gokrazy/kernel.arm64/compare/ac43a676b0b4...3998e71b7b1d
* github.com/gokrazy/rpi-eeprom: https://github.com/gokrazy/rpi-eeprom/compare/b80c62cf428d...6a92fdda349c
* github.com/gokrazy/breakglass: already current
* github.com/gokrazy/gokrazy: already current
* github.com/gokrazy/kernel.rpi: already current
* github.com/gokrazy/serial-busybox: already current

Triggered by the gokrazy-bump workflow's schedule.

Updates #1866

Signed-off-by: Dep Updater <noreply+dep-updater@tailscale.com>
2026-09-28 07:07:05 -07:00
Brendan Creane 2d4379386a net/dns: don't take over DNS when the OS has no upstream resolvers (#20794)
On backends that can't do OS-level split DNS, Tailscale forwards the default
route to the system's own resolvers, which it reads out of the OS config. At
boot that config may not be populated yet, because NetworkManager or
systemd-resolved haven't run, and Tailscale took over regardless: it pointed
the OS at 100.100.100.100 but compiled an empty "." route, so every
non-Tailscale name failed to resolve for the rest of the process's life.

Fail compileConfig instead, which leaves the OS resolvers in place, and
retry the last config with a bounded backoff until resolvers appear. A
health warning explains why MagicDNS is inactive in the meantime.

Sandboxed macOS and iOS are exempt. There the network extension reapplies
the config itself when the OS nameservers change, and quad-100 as the
primary resolver is what keeps tailnet names resolving while the base config
is still empty. Dropping the exemption is tracked in tailscale/corp#48962.

This also changes the outcome on an openresolv host with no snippets
registered. Since the fix for #20825, openresolv reports an empty base config
there and tailscaled took over with no upstream, so every public name got
SERVFAIL. Such a host now keeps its resolv.conf and shows the health warning
instead. TestOpenresolvDNS checks that outcome.

TestSplitDNSEmptyBaseConfig covers the boot race end to end in natlab: it
empties resolv.conf on a guest using the "direct" backend, checks that
tailscaled leaves it alone, then adds a resolver and checks that both tailnet
and public names resolve.

Fixes #20341

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-09-27 10:20:42 -07:00
Murad Rabadanov fe0d7d1dc5 tstun: skip netstack checksum handling for raw injections
Updates #18013

Signed-off-by: Murad Rabadanov <the21.21@mail.ru>
2026-09-26 14:58:38 -07:00
Dep Updater 6b3a45f14e go.mod: bump github.com/go4org/hashtriemap
* github.com/go4org/hashtriemap: https://github.com/go4org/hashtriemap/compare/45fcf11fca0e...44e5305f85d9

Triggered by @bradfitz via the bumpdep workflow.

Updates #21485

Signed-off-by: Dep Updater <noreply+dep-updater@tailscale.com>
2026-09-25 16:03: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
Brad Fitzpatrick d4f2bb4cff ipn/ipnlocal: skip the kernel peerapi listener on FreeBSD when netstack is present
FreeBSD is a weak-host stack like Linux, so a LAN-adjacent machine can
send a SYN for a node's Tailscale IP to the node's NIC and get the
kernel's SYN-ACK from the peerapi port, confirming which tailnet
identity that MAC address belongs to. Linux closes this with
SO_BINDTODEVICE and macOS/iOS with IP_BOUND_IF, but FreeBSD has no
per-socket interface bind (only IP_RECVIF and SO_SETFIB).

Instead, treat FreeBSD like Android: when netstack is present, don't
create a kernel-level peerapi listener at all and use the fake listener,
since netstack already intercepts peerapi connections from peers in
userspace and the kernel socket only ever served the local host. When
netstack is compiled out (ts_omit_netstack), the kernel listener is the
only way to serve peers, so keep it and log that it must be restricted
with pf.

Along the way, tailscaled's own pf source NAT rule on FreeBSD turned out
to rewrite the source of such a SYN-ACK to the LAN IP and a random port,
so a plain connect() from the LAN never completes. The SYN-ACK still
leaks, though, so the natlab test now watches for SYN-ACKs on the
attacker's NIC with tcpdump instead of checking for a completed
handshake, and gains a FreeBSD variant. With the fix disabled, that
variant fails on the leaked SYN-ACK from the peerapi port.

Fixes #21419
Updates tailscale/corp#48248

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I0d2de4ebc9bae28450d500bd7b6ddbfa70f9052f
2026-09-25 12:10:40 -07:00
Mike Jensen 94ea82ab55 ipn/localapi: restrict debug-log access to PermitWrite (#21447)
The debug-log endpoint was gated on PermitRead, which any local user connecting to the world-writable tailscaled socket passes. Although forged log entries are always possible, we should limit the ability for local users to influence that flow with respects to a given node.

This change requires PermitWrite for debug-log, matching the trust level of every other mutating debug endpoint. Also add a buildfeatures.HasDebug guard to debug-dial-types for parity with serveDebug.

Fixes tailscale/corp#48143

Change-Id: I0c0044b6b44fe7cbfb6734ac18bca3dc36eaffbf

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-25 12:29:56 -06: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
Brad Fitzpatrick ec5d51098a derp/derpserver: bound dup client sendHistory growth
When two connections share a node key, they form a dup client set and
noteClientActivity records each sending connection in the set's
sendHistory. It appended on every frame whenever the sender was not the
immediately previous one, and nothing trimmed the slice while both
connections stayed alive. Two connections taking turns sending therefore
grew sendHistory by one *sclient per frame without bound.

Under the default lastWriterIsActive policy nothing ever stops that
growth, so a malicious client (which controls its own node key) or a
buggy one that keeps two connections alive and both sending could leak
server memory, roughly 8 bytes per frame, for the life of the
connection pair.

Record the sender by moving it to the end of sendHistory and dropping any
earlier occurrence, so each connection appears at most once and the slice
stays bounded by the number of connections in the set. This preserves the
existing behavior: the fighting check under disableFighters still runs
before the move and still disables everyone on the first repeat, and
removeClient still promotes the previous speaker from the slice tail.

Fixes tailscale/corp#48884

Change-Id: I06198178e6ab0d7e2f04c1dc4c09eafcb16ace46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-25 09:51:11 -07:00
Brad Fitzpatrick cbcca9dc15 derp/derpserver: bound the size of unknown frames the server will discard
The per-client receive rate limiter charges at most
minRateLimitTokenBucketSize tokens per frame, on the assumption that any
frame larger than that would fail validation and close the connection.
That held for every known frame type, but not for unknown ones:
handleUnknownFrame discarded exactly the declared length, up to 4 GiB,
and returned nil, keeping the connection open.

A client could therefore send unknown frame types with huge declared
lengths and have the server read the bytes off the socket while being
charged only 64 KiB of tokens per frame, bypassing the operator's
configured per-client rate limit by a factor of about 65,000.

Reject unknown frames whose declared length exceeds the largest frame a
regular client can send today, which makes the limiter's assumption true
for all frame types while still tolerating small, reasonably sized
frames from newer clients. Add a test that the server still tolerates
a small unknown frame and closes the connection on a huge one.

Fixes tailscale/corp#48889
Updates tailscale/corp#40171

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I3f6a2c9d8e1b4a7f5c0d2e9b8a6f4c1d7e3b5a90
2026-09-25 09:51:00 -07:00
Brad Fitzpatrick 19855780f7 derp/derpserver: don't hold s.mu while writing debug traffic to the network
ServeDebugTraffic held s.mu while JSON-encoding each record straight
to the ResponseWriter, so a debug client on a slow connection could
block the network write with the server mutex held and stall the
whole DERP server.

Encode into a private bytes.Buffer instead and, once it passes a
threshold size, release s.mu, write the buffer to the network, and
re-take the lock before continuing. That keeps the lock off the
network path without toggling it around every record.

Fixes tailscale/corp#48890

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I7c3e91a4d2f58b06e1a9c4f7d3b28e5a61f09c4d
2026-09-25 09:50:40 -07:00
Mazdak Nasab 72fa821ed4 feature/conn25: clean up formatting in conn25*.go files
Improving formatting in conn25.go and conn25_test.go.

Updates #cleanup

Change-Id: Ic597e8e3e9284d0cc244de538abd6a563f783f32
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-25 09:46:53 -07:00
Mazdak Nasab 5af313f0aa feature/conn25: validate ConnectorTransitIPRequest contains TransitIPs in the pool
Validate ConnectorTransitIPRequest messages contain
TransitIPs in the configured pool.

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

Change-Id: I35dfcaa95a08b4347b1164df905cc59de47e4d96
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-25 09:46:53 -07:00
Francois Marier ef50433800 tstest/natlab/vnet: tear down gVisor network stacks at Server shutdown
Resolves #21442

Change-Id: I98f220d1a0ddc1a15cd9ebe7a8aaa745d17a43b2
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-09-25 09:39:50 -07:00
chaosinthecrd 7aa70218d2 cmd/k8s-operator: opt serve out of the tun bind for cluster-traffic ingress
An Ingress annotated with
`tailscale.com/experimental-forward-cluster-traffic-via-ingress` forwards
cluster traffic to the proxy's Pod IP, which is DNATed to the node's Tailscale IP
where serve answers it. On Linux the serve listener is bound to the tunnel
interface and drops that traffic, so set TS_SERVE_ALLOW_ALL_INTERFACES on the
proxy when this annotation is used, which makes serve answer it again.

Document on the annotation how the traffic reaches serve and that it bypasses
tailnet ACLs, and regenerate the CRD and operator manifests.

Updates tailscale/corp#48248

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-09-25 16:35:59 +01:00
chaosinthecrd 3d62394afc ipn/ipnlocal: add TS_SERVE_ALLOW_ALL_INTERFACES to opt serve out of the tun bind
On Linux, serve's kernel listeners are now bound to the tunnel interface, which
stops LAN-adjacent hosts completing a handshake with the listener (see #21420).
That bind drops packets that arrive on another interface addressed to the node's
Tailscale IP, which breaks the Kubernetes operator's
`tailscale.com/experimental-forward-cluster-traffic-via-ingress` ingress feature.

Add `TS_SERVE_ALLOW_ALL_INTERFACES`, which makes serve's listener skip the
interface bind so those packets are answered again. It applies only to serve
listeners, not the web client listener, which shares the same code but stays
bound. It only affects serve; the peerapi listener stays bound too. This is a
targeted opt-out for the operator to set on the affected proxies. Enabling it
re-exposes the serve listener to the local network.

Updates tailscale/corp#48248

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-09-25 16:35:59 +01: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
Brad Fitzpatrick 506f261427 log/filelogger, logpolicy: put Windows disk logs next to the log config
filelogger always wrote under %ProgramData%\Tailscale\Logs, even though
logpolicy already picks %LocalAppData%\Tailscale for its config when
tailscaled isn't privileged enough for %ProgramData%. A tailscaled run by
a regular user therefore logged "failed to create local log directory" and
kept no disk logs. Take the directory from the caller instead: logpolicy
passes the Logs subdirectory of whichever directory it chose, which is the
same %ProgramData% path as before for the service.

Updates #2791

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I3b5d7f9a1c3e5b7d9f1a3c5e7b9d1f3a5c7e9b1d
2026-09-24 11:52:30 -07:00
Brad Fitzpatrick e87076a378 ipn/auditlog: store the audit log in the backend's state directory
The store path was hardcoded to %ProgramData%\Tailscale\audit-log.json on
Windows, so a tailscaled run by a regular user with its own --statedir
logged "[unexpected] failed to create audit log store ... Access is denied"
on every profile switch. Use the backend's TailscaleVarRoot when it is
known, falling back to the platform default otherwise. For the Windows
service the state directory is %ProgramData%\Tailscale, so its path is
unchanged; an explicit SetStoreFilePath still wins.

Updates #2791

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I5d7f9b1c3e5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e
2026-09-24 11:52:21 -07:00
Brad Fitzpatrick 6612bed24d ipn/desktop: skip the extension when sessions can't be enumerated
Registering the session state callback enumerates the existing desktop
sessions, which fails for a tailscaled run by an unprivileged user
(WTSEnumerateSessions returns "No more data is available"). That was
reported as "init failed", which reads like a bug. Such a tailscaled has
no other users' sessions to track, so report it as skipping the extension,
the same way an unavailable session manager already is.

Updates #2791

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I7f9b1d3e5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b
2026-09-24 11:51:51 -07:00
kari-ts 9020fdcd8a logtail: stop retrying uploads after logger is disabled (#21459)
Logger.SetEnabled(false) prevents new log entries from being buffered, but a batch that has already been drained into uploading can continue retrying indefinitely after logging is disabled.  This was seen on a client where log.tailscale.com was blocked and remote client logging had been disabled.
Here we add a check on the disabled state before upload attempts so that existing failed batches are abandoned after logging is disabled.

Updates tailscale/tailscale#21088

Signed-off-by: kari-ts <kari@tailscale.com>
2026-09-24 10:43:53 -07:00
License Updater b1664580dc licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2026-09-23 18:02:11 -07:00
Patrick O'Doherty 9cdbc3ad3e ssh/tailssh: record stderr on non-PTY sessions (#21369)
On non-PTY exec sessions stdout was wrapped in the recording writer but
stderr was copied to the client raw, so any command output written to
stderr never appeared in the session recording. PTY sessions were
unaffected because they fold stdout and stderr into a single stream that
already flows through the recording writer.

Wrap stderr in rec.writer too, recording it under the "o" (output)
direction, matching how PTY sessions record combined output. The
asciinema cast format has no separate stderr channel, so "o" is the
correct code for players and tsrecorder to render it.

Strengthen TestSSHRecordingNonInteractive to run a command that writes to
both stdout and stderr and assert both appear in the recorded cast lines.

Updates tailscale/corp#48187

Change-Id: I1dc36073722677a85f2b23b331fd55b8df4d342e
Reported-by: Ben Carman <benthecarman@live.com>
Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2026-09-23 16:13:52 -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
Mazdak Nasab b6c6c59dde feature/conn25: extend datapath test to verify full packets (#21422)
Extend datapath tests to not only assert headers but
also the full expected packets.

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

Change-Id: I0e9a1c2f61f2d63eafa430d8e6436908fe927cf4

Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-23 13:52:21 -07:00
Francois Marier 8d43ba6737 tstest/natlab/vmtest: close unused pipe read FD to avoid leak
There are two copies of the read file descriptor (parent & child).
Since the parent copy is unused, we can close it after fork.

Updates #cleanup

Change-Id: Id789e0a04970c9fb8eaeed1f299aa25f29fcf48c
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-09-23 13:04:11 -07:00
François Marier 02de76ba62 tstest/natlab: ignore transport errors during gokrazy update (#21441)
In my testing using TCG, I hit non-EOF network errors that made
this test fail. Since we already poll and check the image version
later, we can treat this response as best-effort.

Resolves #21440

Change-Id: Ia923fc2c199aae7a543d00e06d25a375512daa01
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-09-23 10:50:34 -07:00
Brad Fitzpatrick 0931824b5f feature/androiddns: fall back to getaddrinfo on Android 9 and older
The resnsend dnsproxyd command this package relays raw DNS messages
through was added in Android 10. On Android 9 and older the daemon
answers it with FrameworkListener's text "500 Command not recognized",
which we read as binary: "500 " passed the negative errno check and
"Comm" became the answer length, so every lookup failed with
"androiddns: bogus answer length 1131375981". That's what a tailcat
user hit on a Fire TV stick, which runs Android 9 under Fire OS 7.

Detect that text reply (a binary reply never starts with an ASCII
digit) and switch the process over to the daemon's older getaddrinfo
command, the one bionic's getaddrinfo proxies through. Parse the
single A or AAAA question out of the query, send the command with the
matching address family, and synthesize a DNS answer from the addrinfo
list that comes back, mapping EAI_NONAME to NXDOMAIN and EAI_NODATA to
an empty answer so Go's resolver produces its usual errors. Other
query types return an error saying the Android version can't answer
them.

The reply layout is the field-by-field one netd has used since
Android 6.0 (a 64-bit netd may serve a 32-bit client, so it stopped
sending the raw struct); Android 5.x's raw struct layout is detected
and rejected rather than guessed at. The format was verified against a
32-bit Fire OS 7.7.1.3 device (Android 9, API 28), where a tailcat
build with this change resolves names, fetches its DERP map over
HTTPS, and accepts a connection from another machine.

Updates tailscale/tailcat#126

Change-Id: I7062984c7ec8ce6caf737089e220ef2ca439dcc5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-23 10:45:09 -07:00
Francois Marier a35aff2895 tstest/natlab: move running instructions to a README
Updates #cleanup

Change-Id: I9580fbfeeef2edf0c678c8ab1ec70131fd20f0b5
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-09-23 10:19:34 -07:00
YewFence 7122baac1b ipnlocal: fix portlist service event type mismatch (#21433)
The portlist extension publishes a PortlistServices event, but the
LocalBackend subscriber accepted the underlying slice type instead. The
event bus matches event types exactly, so endpoint updates were dropped
before reaching Hostinfo.Services.

Add a regression test covering the event-to-Hostinfo path.

Fixes #20192

Signed-off-by: YewFence <hello@yewfence.dev>
2026-09-23 11:55:33 -04:00
Brad Fitzpatrick 63f625397b misc/bumpdeps, .github/workflows: unify the bumpdeps tool and bumpdep workflow
The bumpdep workflow grew 170 lines of inline shell that duplicated
much of what misc/bumpdeps already did in Go (resolving versions,
special branches, running go get), and disagreed with it in small ways
(the workflow fetched wireguard-go with GOPROXY=direct; the tool asked
the proxy for the branch head). It was also untestable except by
extracting the run block and running it by hand.

Move all of that logic into misc/bumpdeps and make the workflow a thin
wrapper that runs it with -github. The tool now:

  - accepts the workflow's argument forms: "go" for the toolchain (via
    ./pull-toolchain.sh), the "wireguard-go" and "gvisor" aliases,
    exact module paths, "path@version", and modules not yet in go.mod,
    alongside its existing substring filters; comma-separated arguments
    are split, so the workflow input passes straight through;
  - finds branch heads with git ls-remote and then asks the proxy for
    the pseudo-version of that commit, so a just-pushed commit is seen
    without ever cloning (GOPROXY=direct on github.com/gokrazy/kernel.*
    hung the workflow for hours);
  - refuses downgrades by diffing go.mod before and after go get,
    rather than grepping go get's output;
  - runs "make tidy" and "make updatedeps" itself (-tidy=false to skip);
  - renders the PR title, body (GitHub compare links derived from the
    module path, including subdirectory-tagged modules), and commit
    message, printing a suggested commit message locally and, with
    -github, writing step outputs and the step summary. -issue accepts
    the issue URL or "#N" form and becomes the "Updates" line.

The branch name changes from a slug of every module path, which
produced names like actions/bumpdep/github.com-gokrazy-kernel.amd64-
main-github.com-gokrazy-kernel.arm64-main-github, to
actions/<workflow>/<actor>/<yyyymmdd-hhmmss> (no actor for scheduled
runs). Each run is a fresh PR; delete-branch cleans up after merge or
close.

The bumpdep workflow gains an optional exclude-newer-than-days input for
the cooldown. All of the formerly-inline logic now has unit tests,
including a fake module proxy.

Also add a gokrazy-bump workflow that runs the same tool every Sunday
night on the direct github.com/gokrazy/* modules (kernels, firmware,
gokrazy itself) and opens a PR labeled run-natlab-tests, so the natlab
VM tests boot the new kernels before merge. Substring filters now skip
indirect dependencies unless -indirect is set, so that "github.com/
gokrazy/" doesn't drag in gokapi; naming an indirect module exactly
still selects it.

Updates tailscale/corp#48312
Updates #8043
Updates #1866

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I7c3e91a4d2f58b0e6a1c9d47f3b2e8a5c6d0f1e2
2026-09-23 06:42:52 -07:00
Raj Singh 5f0cf87429 cmd/containerboot: recover from IPN watch closure (#21374)
When containerboot falls behind on the IPN bus, tailscaled closes the
watch. containerboot treated the EOF as fatal and SIGTERMed a healthy
tailscaled, which is easy to hit on large, churny tailnets.

Instead, reconnect and rebuild state from the new watch's initial
status, and only request peer changes in modes that use them. If the
watch can't be reopened for a minute, exit so a dead tailscaled still
restarts the container.

Fixes #21373

Change-Id: Iad7749e4fd0f43eabdb471d6e64bb43f37ff70ff

Signed-off-by: Raj Singh <raj@tailscale.com>
2026-09-23 10:36:01 +01:00
Dep Updater 610b05c58e go.mod: bump github.com/gokrazy/kernel.amd64, github.com/gokrazy/kernel.arm64, github.com/gokrazy/kernel.rpi, github.com/gokrazy/rpi-eeprom, github.com/gokrazy/gokrazy
* github.com/gokrazy/kernel.amd64: v0.0.0-20260705070735-de680abf072b to v0.0.0-20260922084445-21771250d660
* github.com/gokrazy/kernel.arm64: v0.0.0-20260705071517-37841c4d6ff1 to v0.0.0-20260922084859-ac43a676b0b4
* github.com/gokrazy/kernel.rpi: v0.0.0-20251127164438-9778ec0261de to v0.0.0-20260911133309-2cbf751e3f2a
* github.com/gokrazy/rpi-eeprom: v0.0.0-20260518070910-95f7328a8228 to v0.0.0-20260913082024-b80c62cf428d
* github.com/gokrazy/gokrazy: v0.0.0-20260703061218-a4a45a20149d to v0.0.0-20260916140236-39fe3e5557b8

Triggered by @bradfitz via the bumpdep workflow.

Updates #1866

Signed-off-by: Dep Updater <noreply+dep-updater@tailscale.com>
2026-09-22 18:46:04 -07:00
David Bond 8af8f861c0 cmd/k8s-operator/e2e: add tests for kube-apiserver ProxyGroups (#20993)
The e2e suite covered the operator's in-process API server proxy but
not the ProxyGroup-based one. Add tests for both proxy modes that
drive a ConfigMap through its lifecycle via the proxy, verify a
forbidden request is rejected, and check that deleting the ProxyGroup
cleans up its StatefulSet and Tailscale Service.

Fixes tailscale/corp#38009

Change-Id: Ifc0be47ce32dd96f8daa748af6785a8b82ec19a7

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-09-22 22:39:20 +01:00