11580 Commits
Author SHA1 Message Date
James Tucker 1334899864 ssh/tailssh: stop killing session processes on graceful disconnects
Monitor teardown SIGHUPed and SIGKILLed every remembered child group
when the transport closed. sshd signals nothing at a non-PTY session
end; background jobs with redirected stdio survive and reparent to
init. Clients rely on that: VS Code Remote-SSH starts its remote
server as a plain background job in the session's group and expects
it to outlive connection drops, and tailscale upgrades run as
non-PTY execs whose work should survive a daemon restart.

Use an explicit graceful monitor close to preserve session jobs on
ordinary disconnect and daemon shutdown. Close PTY masters normally,
letting the terminal driver signal the foreground group, but do not
signal non-PTY jobs. Remove Linux Pdeathsig from session children;
forwarding children retain it and always die with the connection.

Commit duration and recording-failure termination to the monitor
before publishing cancellation or writing the termination message.
The monitor owns per-session HUP-to-KILL escalation even after the
leader exits or the connection closes. Revocation arms connection-wide
cleanup. Pin every termination target's leader until after SIGKILL,
including during teardown, so group signaling cannot hit reused PIDs.
Select teardown targets explicitly and leave graceful survivors alone.

Unexpected RPC loss fails closed. If a termination mandate cannot be
delivered, close the RPC rather than subsequently requesting graceful
teardown; this may also kill sibling jobs on a broken channel. Keep
both the escalation and monitor shutdown waits bounded.

Add coverage for default-HUP graceful survivors, actual duration and
recording-failure termination followed by immediate disconnect, sibling
survival, HUP-exiting leaders with immune group members, broken RPCs,
and leader pinning through teardown's SIGKILL pass.

Fixes #22076

Signed-off-by: James Tucker <james@tailscale.com>

Change-Id: I70b4ef40035330dd542d256ba79d889a12426a74
2026-10-30 16:39:06 -07: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
François Marier 1b68f62be8 tstest/natlab: add a new portmapper VM test (#22071)
This test encodes the current behaviour of the Tailscale client.
It only covers the creation of new mappings.

Updates #21713

Change-Id: Ia44877036569660f4129484d5059ed22ffbf9cda
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-10-30 10:19:14 -07:00
Fran Bull eb298524ee feature/conn25: create a secret for TransitIPRequest tokens
Create a secret that can be used for token minting and verification.

Updates tailscale/corp#46169

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-10-30 09:36:17 -07:00
Mazdak Nasab b6fc02d501 feature/conn25: block private destinations (#22073)
Similar to 7304dd4 for host-scoped destinations, conn25 needs to
block private destinations: a connector forwards to app destinations,
and the packet filter only ever sees the transit IP, never the
destination, so a private destination would let a client reach into the
connector's own network.

Updates https://github.com/tailscale/corp/issues/50118
Updates https://github.com/tailscale/corp/issues/46169

Change-Id: I700b0b5a270723f5a77907e35c52d729f88c44b6

Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-30 08:55:05 -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
James Tucker 5a05ac11d5 ssh/tailssh: bound session teardown so shutdown can't wedge on it
Bound the server shutdown wait and the output drain after a session
process exits. Descendants can keep stdio open after the session
process exits, so neither wait can depend on EOF alone. Abort the SSH
channel on drain timeout without sending EOF concurrently with an
outstanding output write.

Close all PTY master descriptors on cancellation and let the terminal
driver send SIGHUP. Duplicate the masters with CLOEXEC and O_NONBLOCK
before wrapping them with os.NewFile so the runtime poller can
interrupt pending I/O on Close. Keep the PTY child's blocking mode intact.
Use RawConn.Control for duplication and window changes to synchronize
with Close and avoid restoring blocking mode on the original master
through File.Fd.

Non-PTY sessions receive SIGHUP explicitly. Keep SIGKILL escalation
for monitor sessions and add it for directly launched sessions. Use
os.Process.Signal on the direct path to synchronize signaling with
reaping. Make the escalation delay configurable alongside the
shutdown and I/O drain timeouts.

Use a process-completion channel to stop the cancellation worker on
normal exit, and join the worker before sending exit status so any
cancellation message is written before channel teardown. Cover normal
completion and cancellation through live monitored SSH sessions.

Fixes tailscale/corp#50380
Fixes #22059
Updates #20628
Updates #21827

Signed-off-by: James Tucker <james@tailscale.com>
Change-Id: I2a66741ba156a607dc3df70a79e140f19737b7a5
2026-10-29 15:43:08 -07:00
James Tucker 96467f7d16 tsnet,net/tsdial: resolve outgoing names using tailnet DNS policy
Resolve Server.Dial and Server.HTTPClient names using the tailnet's DNS
configuration, including MagicDNS, search domains, split DNS routes, and
global resolvers. Preserve the dialer's local MagicDNS fast path.

Add Server.LookupIP for direct lookups returning netip.Addr values. Keep
the resolver implementation private to tsnet. Search expansion uses
ndots=1, includes the MagicDNS suffix when enabled, and respects trailing
dots. Each lookup uses the current DNS configuration.

Use the internal resolver before considering host fallback. Configured
upstream failures and authoritative local denials must not fall back to
the host resolver; the host's hosts file is consulted only during fallback.
Distinguish configured routes from ambient cloud-host fallback resolvers.

Cover search expansion, split and global DNS, MagicDNS-disabled operation,
configuration updates, hosts-file precedence, and dialing in tests.

Fixes #5840
Updates #7984
Updates #15260
Updates #4677

Change-Id: I832bc4c68658d2d25c5dc29b79acf611fc11e59e
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-29 15:29:43 -07: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
Adrian Dewhurst 7304dd42c1 feature/conn25, ipn/ipnlocal: use the new net/routelimits package for blocking host-scoped destinations
Similar to #20866 for 4via6, conn25 needs to block host-scoped
destinations. Due to the similarity between the two situations, the
matching is no longer in a 4via6-specific location. Since there is no
backwards compatibility to satisfy, there is no exception list for
conn25.

ipn/ipnlocal (for 4via6) and feature/conn25 now both use the new
package.

Updates tailscale/corp#50118
Updates tailscale/corp#46169

Change-Id: Ic3f9cccd2f5823a2ca48bd78607792b0fd6d7db9
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2026-10-29 13:57:37 -04:00
Aarnav Tale 6630facd78 cmd/tsconnect/wasm: hold back partial UTF-8 sequences in termWriter
termWriter converted each chunk of the SSH output to a JS string on its
own which incorrectly handles multi-byte characters across chunk
boundaries. Move it to a separate cmd/tsconnect/termwriter package with
tests, and hold back incomplete sequences for the next write.

Fixes #22007

Signed-off-by: Aarnav Tale <git@tale.me>
2026-10-29 09:57:11 -07:00
Mazdak Nasab 3ad81d1908 feature/conn25: return pool addresses when reserveAddresses fails
Addresses handed out by ippool.next() are marked in use, and the only path
back to the pool is the popExpired loop in reserveAddresses, which can
only see an addrs once it's in the client's assignments. So a reservation
that fails after taking an address leaks it from the pool
for the lifetime of the process.

The leak is the result of interaction between the magic and transit pools.
First an address from magic pool is picked. Then another address from
the transit pool is picked. If an error happens during picking an address
from transit pool, the address from the magic pool needs to be returned,
otherwise that address is leaked, as the address in not in the client
assignments and won't be released.

Fixes https://github.com/tailscale/tailscale/pull/22019

Change-Id: I3d98282762576cc57c135761859902a1d0a63c31
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-29 09:10:47 -07:00
James Tucker 7ad13349f1 ipn/ipnlocal: randomize DNS query IDs
Allocate a fresh 16-bit ID for each QueryDNS request instead of using the
constant ID 1.

Updates tailscale/corp#48187

Change-Id: I9b47a3fbbef70f0462f1da4f3b29eb77cf31f9f0
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-29 08:53:10 -07:00
James Tucker 7623e71e9c ipn/ipnlocal,ipn/localapi: propagate context through DNS queries
Accept a caller context in LocalBackend.QueryDNS and pass it to the
internal resolver. Use the HTTP request context for localapi DNS queries
so cancellation and deadlines bound the lookup.

Updates #8338

Change-Id: I1bb8ec0b779c883ec40d52063adf25ff36cf43e3
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-29 08:52:50 -07:00
Alex Chan d064d751fb go.mod: bump to golang.org/x/net v0.62.0
These are subject to vulnerability report GO-2026-6860:
https://pkg.go.dev/vuln/GO-2026-6860

We don't use the affected functions anywhere in oss and there's only one
fairly minor non-test use in corp, but a vulnerability scanner in corp
is failing on this package.

Updates #cleanup

Change-Id: I2b441c5f5624090e6bf2d3cc014c9dd4663cf2fc
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-10-29 13:09:55 +00:00
James Tucker f7c1b8e526 hostinfo,ssh/tailssh: don't leak systemd service env to child processes
tailscaled runs under a Type=notify unit, so NOTIFY_SOCKET is in its
environment. The desktop probe ran loginctl with that environment
inherited. loginctl sends sd_notify(3) messages on exit and error, and
lives in the daemon's cgroup, so systemd took each message as coming
from the tailscaled unit and rejected it:

  tailscaled.service: Got notification message from PID 1234, but
  reception only permitted for main PID 1000

One failing exec sends two messages, ERRNO=... and EXIT_STATUS=... That
is the pair of rejected lines seen at each SSH session on Debian-family
hosts. The per-connection ssh-monitor child also received the daemon's
full environment.

Helpers exec'd by tailscaled now get the daemon's environment without
the systemd service-instance variables: NOTIFY_SOCKET, WATCHDOG_PID,
WATCHDOG_USEC, LISTEN_FDS, LISTEN_PID, LISTEN_FDNAMES, INVOCATION_ID,
JOURNAL_STREAM. The scrub covers the loginctl and getenforce probes,
the direct-exec fallback probe, and the ssh-monitor spawn.

Session children still never receive the daemon's environment. The
monitor still gives children running as the connection's user no daemon
variables. The daemon's own environment is unchanged.

Updates tailscale/corp#50380

Change-Id: Ic686726473e851e2855e75eea11d7b3f75440311
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-28 15:17:19 -07:00
James Tucker b101cb8bef ssh/tailssh: keep sessions when a prompt-free rule expires
Check mode grants recently approved users a prompt-free Accept rule
with a short RuleExpires. When that rule expired, the policy recheck
matched the base HoldAndDelegate rule instead. The recheck read that as
a policy edit and closed the connection. Live sessions died when the
prompt-free window lapsed.

Remember the rule that matched at authentication. If it has expired and
a hold rule matches instead, keep the connection. Only new connections
re-authenticate.

The expiry timer now fires at least 1ms past a lapsed deadline. A
zero-delay refire lands at the deadline, where the rule is not yet
expired, and re-arms. The real clock moves on between refires; a stopped
clock, such as a synctest bubble, does not.

The fallback test runs in a synctest bubble, so the expiry cannot race
the handshake on a slow runner. A bubble cannot hold child processes:
the test opens a session channel but runs no command, and the empty PATH
keeps the conn lifecycle free of subprocesses. The session-kill side of
expiry stays pinned on the real clock.

A policy edit that swaps a still-valid accept rule for a hold rule still
closes the connection. Recorder, identity, and accept-env checks still
run on the fallback path.

Updates tailscale/corp#50380

Change-Id: I46fa7d42142e90d00843744427c91ba29d68a467
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-28 14:47:00 -07:00
François Marier 8ed43f1f2a tstest/natlab: add basic SSDP and UPnP support (#21823)
This bare-bones implementation of a UPnP server covers only what
the Tailscale client needs in order to successfully create a
new mapping.

A new flag is introduced for the test control server so that UPnP
can continue to be disabled in all tests except natlab.

Updates #21713

Change-Id: I57779e07c6a628a12bb30dccabd94f7bb21270c4
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-10-28 11:58:07 -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 41d25e0c8f cmd/natlab-test-matrix,tstest/natlab/vmtest: add test matrix tool (#21818)
Add experimental tool for generating a matrix of tests to be run by CI.
This tool will parse the test files in the vmtest folder, and find
candidates that can be with with a set of variations. For now that is
expressed as different flavours of Linux, but it can be expanded across
OS'es but also other things like NAT configurations and portmapper
extensions available.

Updates #21817

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-10-28 14:05:30 -04:00
Sam JeanandJames Tucker 3a38840ccc ipn/ipnlocal: check the tailscaled socket on every serve unix dial
The unix-socket reverse proxy checked its backend against the tailscaled
socket only when the handler was built, but each backend connection
re-resolved the path from scratch. A backend that was benign at config
time could later be replaced with a symlink to the tailscaled socket by
anyone able to write to its directory, and the next dial followed it.
The proxy dials as root, so the same swap could redirect a serve or
funnel backend at any root-only unix socket on the host.

Move the check to the dial itself, in a dialServeUnixSocket method
shared by the HTTP transport, the h2c transport and the TCP-forward
handler, all of which previously dialed unix on their own. It refuses
tailscaled's socket with ErrProxyToTailscaledSocket immediately before
connecting.

On Linux 5.6 and newer, the dial pins the resolved socket inode with
openat2(2): an O_PATH descriptor opened with RESOLVE_NO_MAGICLINKS,
and RESOLVE_BENEATH under the socket's parent directory where the path
allows it, or by whole path otherwise. The pinned file is checked
against the tailscaled socket, and the connection is made to the pinned
file through /proc/self/fd, so replacing the path cannot retarget the
connect. On kernels without openat2(2), and on other platforms, the
path is dialed directly.

The connected peer is re-checked after the dial on every platform. The
peer address of a path-bound listener is the path the socket was bound
to, not the path that was dialed, so a symlink swapped in before the
dial is identified after the connect. This check is best-effort:
connections that have no determinable peer name are allowed.

Fixes tailscale/corp#50481

Change-Id: Iae5db207519db9c139a512c0427dce3dfa3ca672
Co-authored-by: James Tucker <james@tailscale.com>
Signed-off-by: Sam Jean <samj@tailscale.com>
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-27 13:08:08 -07:00
kari-ts 822061d89c ipn/ipnlocal: reset alwayson override before reconciling prefs (#21998)
https://github.com/tailscale/tailscale/commit/09982e1918f27c740320a4879bbafdd9f379733a
introduced an alwayson override reset before applying policy settings
during Start.
A prefs reconciliation refactor in
https://github.com/tailscale/tailscale/commit/cb7b49941eae3a933c4c5b7dc56398bce24d7e08
changed the ordering so that Start reconciled prefs before clearing the
override. If the user disconnected with a reason, reconciliation would
preserve WantRunning=false, and Start could retain that stale value
after the override was cleared.
https://github.com/tailscale/tailscale/commit/f4aea70f7a199e5ac155e225fe7ad4de374e9dea
added cached netmap startup, so processing the cached netmap can
reconcile the current prefs to WantRunning=true while Start still holds
a local prefs snapshot with WantRunning=false. Start can then notify the
frontend with the stale value even though the backend is transitioning
to Running with WantRunning=true.

This resets the alwayson override before reconciling prefs, restoring
the original Start ordering and ensuring that the prefs used throughout
Start reflect the effective alwayson policy

Updates tailscale/corp#44094
Updates tailscale/corp#44095

Signed-off-by: kari <kari@tailscale.com>
2026-10-27 12:42:35 -07:00
Michael Ben-Ami de7245d25b feature/conn25,net/dns{,/resolver}: thread PeerAPI DoH metadata in DNS
response when DNS request goes to PeerAPI DoH

To support feature/conn25 and other extensions or consumers needing to
know which PeerAPI node handled a DNS request, e.g. for app connectors
or exit nodes, and needing extra data from the server that can be
transmitted in the headers.

DNS responses from other upstream resolvers do not get any metadata
populated, even if those servers are DoH.

Add a new Response type in net/dns/resolver that includes the raw
response bytes and the optional metadata, and replace the bare bytes
with it in some places.

Pass this new type to to net/dns.ResponseMapper instead of the bare
bytes to the extensions can see the metadata.

Updates tailscale/corp#46169

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-10-27 15:25:12 -04:00
BeckyPauley b3766d4fea cmd/k8s-operator/e2e: add 4via6 egress tests (#21854)
Add end-to-end tests for egress to a 4via6 target (an address behind a
subnet router advertising a 4via6 route). Tests include:

- TestConnector4via6Egress/HAEgress: egress to an in-cluster Connector
subnet router. Test requires a dual stack cluster.

- TestHostSubnetRouter4via6Egress/HAEgress: egress to a subnet router that
runs on the test host as a tsnet node. Use RegisterFallbackTCPHandler to
mock the backend response without requiring forwarding on to a backend node
(for test simplicity).

Also update end-to-end test acl for 4via6 routes, and install a dedicated
ProxyClass to accept routes.

Updates tailscale/corp#45732

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-10-27 16:41:10 +00:00
Brendan Creane ce19ee0b47 net/routemanager: keep the coarse CGNAT and ULA routes when a peer advertises them (#21901)
A peer can advertise 100.64.0.0/10 or fd7a:115c:a1e0::/48 as a subnet
route. Those prefixes are also the coarse routes that stand in for peers'
individual addresses, so one OS route entry has two reasons to exist.
applyDirty set that entry from one reason at a time, so removing the
advertising peer deleted the /10 while coarsening still wanted it, and
turning coarsening off deleted the /10 while a peer still advertised it.
Set each coarse prefix once, from both inputs, as rebuildAll already does.

Fixes #21890

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-10-27 08:04:50 -07:00
Samuel Blais-DowdyandCursor c9e36203f4 cmd/k8s-operator: give kube-apiserver ProxyGroups the configured log level (#21888)
The operator set each kube-apiserver ProxyGroup's LogLevel from its own
logger's Level(). Unless TS_NO_LOGS_NO_SUPPORT is set, that logger is
teed into the log upload core at debug, so Level() is always debug and
every k8s-proxy logged at debug whatever OPERATOR_LOGGING said. In our
fleet that is millions of debug lines an hour from the proxies.

Read the level before adding the tee and hand it to the ProxyGroup
reconciler. Log uploads are unchanged.

Fixes #21887

Change-Id: I3dd791549c60ba2a4f3c2651da6716623da571a6

Signed-off-by: sugarraysam <sbd@fireworks.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 14:42:44 +00:00
Francois Marier 2b62983a4a tstest/natlab/vnet: fix TCPConn leak
Explicitly close the other end of the losing connection.

This is a follow-up to 31baee90c0.

Updates #cleanup

Change-Id: I5bf36a7451da05545e7242ee5c1bff02fa44b362
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-10-26 21:49:04 -07:00
Francois Marier e8ebe6aba1 ssh/tailssh: parse UIDs consistently in isRootUser()
Since the incubator parses the UID in base 0 via flags.IntVar()
and isRootUser() uses base 10, it was possible to get inconsistent
results between the two using non-base 10 numbers.

Updates tailscale/corp#50380

Change-Id: I9b2fb16bc37153df7b695533cc73a3499cb774eb
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-10-26 20:06:51 -07:00
Sam Jean 713be260c3 ipn/ipnlocal: refuse serving TCP to the tailscaled socket (#21966)
tailscale serve --tcp=<port> unix:<path> dialed the target socket with
no check, so pointing it at tailscaled's own LocalAPI socket proxied a
tailnet peer straight into the LocalAPI. The LocalAPI authorizes a
unix-socket caller by the peer credentials of the dialing process --
tailscaled itself, as root -- so the peer got a write-capable LocalAPI
and could change the node's settings.

The HTTP proxy path already refused this via isTailscaledSocket; the
TCP-forward path, added later, did not. Reject it at config-set time in
setServeConfigLocked so "tailscale serve" fails up front, and keep a
dial-site guard returning ErrProxyToTailscaledSocket as a backstop for
configs set by other means.

RELNOTE: tailscale serve no longer proxies to tailscaled's own socket

Fixes tailscale/corp#50425

Change-Id: I5ed02e83d32bdfb09c8164907bf38cabcc406169

Signed-off-by: Sam Jean <samj@tailscale.com>
2026-10-26 21:52:39 -03:00
Pawel Rutkowski 73b4c49b88 derp/derphttp: bound response body read on non-101 upgrade response (#21980)
connect read a non-101 response body with an unbounded io.ReadAll and
inlined it into the returned error.

* Cap the read, format resp.Status instead of the always-nil err, and use %q.
* Add a regression test for the bound

Updates: tailscale/corp#50441

Change-Id: Ie35fada982ccda4328fd4e9adb795947dee3a283
Signed-off-by: Pawel Rutkowski <pawel@tailscale.com>
2026-10-26 19:47:59 -05:00
James Tucker 055c809d5f ssh/tailssh: release reverse-forward cancellation callbacks
Close the specific forwarded listener on connection cancellation and stop its callback when the accept loop exits. Canceling a forward no longer retains a connection-lifetime waiter or looks up a replacement listener by address.

Test repeated cancel and rebind operations on one connection, along with callback cleanup when the listener closes independently. Make the existing connection-cancellation test fail if its listener stays open.

Updates tailscale/corp#50380

Change-Id: Ife7fb0acde012b0abf1a8d804b9c6a8994ee5703
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 17:42:07 -07:00
Mazdak Nasab ce20d24a2b feature/conn25: add test coverage for DNS optional sections (#21782)
Additionals and authorities are two optional sections
in a DNS response. Conn25 treats DNS responses differently,
based on the response type. Adding test coverage for these
optional sections to assert the expected behavior.

Updates #cleanup

Change-Id: I778af5b368f53f6209fe2934aa98d9009c7fa395
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-26 16:44:37 -07:00
Percy Wegmann b1582deed4 ssh/tailssh: set $LOGNAME to same value as $USER in incubator
When the ssh incubator runs the shell itself instead of using `su` or `login`,
it needs to set the `$LOGNAME` environment variable. Just like OpenSSH `sshd`,
it sets this to the same value as `$USER`.

Updates tailscale/corp#50380

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2026-10-26 17:25:45 -05:00
James Tucker f5fdb18b2e ssh/tailssh: give delegated action fetches one timeout owner
Let fetchSSHAction own the per-fetch deadline and pass the connection cancellation context directly from clientAuth. Remove the duplicate timeout and its deferred cancellation inside the action loop. Derive the longer transport watchdog from the fetch timeout, keeping both durations unchanged.

Use virtual time to test the fetch deadline across retries, earlier parent deadlines, and parent cancellation. Verify that a successful fetch releases its request context.

Updates tailscale/corp#50380

Change-Id: I185958ca19566f04cbd68f33e4b1dad3d18aebb7
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 15:11:58 -07:00
James Tucker e3a4c41d38 ssh/tailssh: name the monitor for its connection-wide role
Move lazy monitor startup, credentials, and cleanup into connmonitor.go and rename the connection state and accessors. The monitor owns session processes as well as forwarding children. Move the session exit registry alongside session monitor handling and keep the Plan 9 stub in a matching file.

Update callers, tests, and diagnostic prefixes without changing monitor lifecycle behavior.

Updates tailscale/corp#50380

Change-Id: I760fa009176491da426639615907d129b68026fc
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 15:09:58 -07:00
James Tucker 2c1cea51e4 ssh/tailssh: clarify lifecycle and protocol comments
Describe exit statuses, output ordering, and descriptor ownership directly. Keep the Darwin descriptor acknowledgment rationale in the protocol documentation instead of repeating it at each handoff.

Clarify that the exec spawn limit is checked after Start returns, not an interrupting deadline. Remove claims that session exit statuses are unique or that unreaped children must be uninterruptible.

Updates tailscale/corp#50380

Change-Id: I613b83ac2ae92d7bf8a582c4e4b326ebbe55852d
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 15:09:10 -07:00
James Tucker 617c38d523 ssh/tailssh: register connections atomically with shutdown
Check shutdown and register initialized connections under the same lock. Reject a connection if shutdown ran during its construction, stopping its login-grace timer and closing the transport. Also close the transport when connection construction fails.

Cover shutdown before and during connection construction without starting an SSH handshake.

Updates tailscale/corp#50380

Change-Id: I634036a7e073b24d7ff80d6a6d0ae991c40f3c69
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 15:08:03 -07:00
Mario Minardi 4a435a7b42 client,tool: bump node to latest 22.x
Bump node to latest 22.x release.

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

Signed-off-by: Mario Minardi <mario@tailscale.com>
2026-10-26 14:29:40 -06:00
James Tucker 87f3b61f31 ssh/tailssh: dial TCP forwards as the bound user
Route direct-tcpip channels, used by TCP -L and SOCKS -D forwards, through the per-connection monitor after the existing policy checks. Resolve names and create/connect TCP sockets in a child running with the authenticated local user credentials instead of the root daemon.

Hand the connected TCP descriptor back through the existing acknowledged fd protocol. TCP socket ownership survives the handoff, so UID-based firewall policy sees the mapped user. TCP has no per-message SCM_CREDENTIALS and needs no persistent relay: the child exits after handing off the socket. Bound resolution/dialing by the monitor forwarding deadline.

Share the channel-close-aware forwarding copy loop with Unix forwards, retaining bidirectional half-closes and bounded draining on full close. Plan 9 retains its existing in-process forwarding path.

Add root-run IPv4/IPv6 socket-owner regression tests, hostname and half-close coverage, policy/disable gates, invalid/refused dial handling, and a pin that the child is reaped while its socket remains usable. Update the TCP policy revocation fixture for child spawning and synchronize its assertion on transport shutdown.

Updates tailscale/corp#50380

Change-Id: I89159fd31e75c22446096587471afcd2bb4e78d4
Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 13:10:45 -07:00
James Tucker 123232260e ssh/tailssh: retain the latest session window size
Drain any stale buffered size before sending the latest window update. The adapter is the only sender to a one-slot channel, so the final resize is retained even if the consumer races with the drain.

Cover late and slow readers over a real SSH session with deterministic synctest synchronization. Keep the final two resizes unread to exercise replacement of the buffered size.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
Change-Id: I8c077114a6d4765c4208d42a22f384c82f73f75c
2026-10-26 12:11:07 -07:00
James Tucker 6072693e91 ssh/tailssh: relay Unix forward payloads as the bound user
Keep the dialed service socket in the credentialed forwarding child and return a private socketpair endpoint instead. Passing the service socket to tailscaled preserves connect-time peer credentials but causes payload SCM_CREDENTIALS to identify the root daemon.

Keep dial children alive for bidirectional forwarding. Return a lifetime
pipe alongside the data socket so full closure or a discarded dial reply
ends the relay independently of data EOF. Preserve half-closes and the
monitor's descriptor acknowledgment/teardown ownership for both fds.
Listener children retain their existing descriptor handoff behavior.

Handle local Unix forwarding channel closure independently of the SSH
connection: the channel request stream closes on CHANNEL_CLOSE, not
CHANNEL_EOF. Stop waiting for service output while giving queued client
payload a bounded drain in each forwarding stage, so a silent or
non-reading service cannot retain the relay indefinitely.

Test relay exit/reaping with silent services, discarded replies, queued
payload and backpressure, including end-to-end SSH channel closure while
the transport stays connected. Fix a canary test's unsynchronized marker
assertion to wait for the reporting goroutine.

Add a root-run cross-UID SO_PASSCRED regression test and lifecycle coverage for both half-close directions and monitor shutdown with an idle relay.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
Change-Id: I54b8ddf02b9d82434e474fea2ec283c0e39fab3e
2026-10-26 11:34:49 -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
James Tucker 84ca98e518 ssh/tailssh: enforce recording failure before notifying control
Cancel fail-closed sessions before sending recording upload failure
notifications. Bound each notifyControl request to five seconds, including
recorder connection failures on the synchronous session startup path.
An unresponsive control request must not keep an unrecorded session running
or indefinitely delay rejection or fail-open startup.

Keep the timeout local to each notification, independent of the session
and upload lifetime, without adding another goroutine. Test blocked
startup notifications in both rejection and fail-open modes, and use
virtual time to cover earlier and simultaneous cancellation/deadlines.

Send SFTP recording rejection diagnostics to stderr, preserving the binary protocol on stdout and exit status 254. Add real SSH/recorder tests for fail-closed and fail-open behavior with blocked notifications, and an SFTP client regression test.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
Change-Id: I270d624975a6a9115502a4915ee5ff28a67d714b
2026-10-26 10:44:30 -07: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 5cb033d61e ssh/tailssh: initialize username hint exclusions
Allocate the exclusion set before adding client-supplied usernames. A true Exclude entry otherwise panics when SSH is enabled.

Exercise the c2n handler with true, false, absent, and unknown exclusions, and cover the result cap and disabled-SSH behavior.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
Change-Id: I1d9faf36e5809ccff888fc402329652e8b7d88c8
2026-10-26 10:00:23 -07:00
James Tucker d75f04c927 ssh/tailssh: enforce SSH policy at the connection level
Re-evaluate authenticated connections on policy updates and at session
and forwarding gates. Close connections whose grants are revoked or
narrowed, including forwarding permissions, user identity, duration,
AcceptEnv, and effective recording requirements. Recheck rule-derived
AcceptEnv and recording configuration for delegated connections too.
Compare recorder lists with duplicate counts preserved so duplicate
entries cannot hide a change of recording destination.

Treat recording-failure changes in either direction as material,
including fail-open to fail-closed transitions. An inconclusive user
lookup retains the previous identity but does not skip other policy
checks, including for `"*": "="` mappings. An absent account or a changed
username, UID, or primary GID revokes the connection.

Preserve explicit getent failures when the os/user fallback cannot find
the account, instead of treating every lookup failure as account deletion.
Numeric UID mappings use LookupId in the fallback, not Lookup by name.
Known limitation: NSS can report a directory-only account as not found
(getent exit status 2) during an sssd/LDAP outage. If the fallback also
reports the account missing, this is indistinguishable from deletion and
still revokes the connection. This availability tradeoff remains accepted
for now; explicit command failures, timeouts, and malformed output do not
by themselves establish that an account was deleted.

Reject forwarding requests using the latched Allow bit before doing a
policy recheck. Forwarding gates still re-evaluate policy, but use the
latched account identity rather than spawning getent for every forwarded
connection (including each SSH -D request). Account identity is rechecked
at authentication completion, policy updates, rule expiry, and session
gates.

Re-arm rule-expiry timers under the policy recheck lock and prevent
re-arming after teardown. Add connection-level login grace, extending
it during delegation and stopping it only on SSH authentication
success, not on unsigned key queries or partial authentication.
Close the raw transport and cancel delegated authentication on teardown.

Add regression tests for these transitions, wait for the auth-completion
recheck before test policy flips, and release stalled recorder handlers
even on test failure.

Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 08:48:36 -07:00
James Tucker df83989647 go.mod: bump gliderssh for window resize fix
Updates tailscale/corp#50380

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-26 08:13:06 -07:00