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
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>
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>
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
Launch incubator sessions through the per-connection monitor, passing
stdio and the optional environment pipe via SCM_RIGHTS. tailscaled
retains the parent I/O descriptors. Keep direct launch when no
tailscaled binary is available, and on Plan 9.
Wait for dispatched exec replies even after session cancellation so
the session retains ownership of any child that starts. Publish exit
status through persistent completion state. Identify monitor children
by PID and exec request ID, and validate both when signaling. Remove
raw-PID signaling after monitor teardown.
Normalize incubator identity flags and reject alternate spellings
that could override them. The monitor RPC caller remains trusted to
supply the executable and other arguments; this is not a privilege
boundary against tailscaled.
Pass default TERM and locale values through the environment pipe,
preserving last-value-wins defaults and policy-forwarded precedence.
Use a separately owned close-on-exec PTY descriptor for resizing.
Add regression tests for launch cancellation, exit delivery, signal
identity, descriptor ownership, and environment handling.
Updates tailscale/corp#50380
Signed-off-by: James Tucker <james@tailscale.com>
Route tailssh's forwarding through the per-connection ssh-monitor, so
on unix forwarded sockets are dialed and bound by children with the
authenticated local user's credentials, never by root tailscaled.
- unix -L, direct-streamlocal: the lexical allowlist check stays in
tailscaled as coarse policy. The allowlist is home, /tmp, and
/run/user/<uid>. Relative and abstract paths are rejected. The dial
then runs as the user through the monitor. The EvalSymlinks
re-validation is dropped, and the kernel's DAC is the boundary. The
allowlist is capped. A home directory that cleans to "/" or is empty
would prefix-match every absolute path, and such an entry is dropped
instead. Paths at or beyond the sun_path capacity reject lexically
rather than dying at bind or dial with a generic EINVAL.
- unix -R, streamlocal-forward: the listener is created by a user-cred
child with umask-at-bind 0600 and unlink-if-owned. Closing it does
not unlink the socket, and the stale file on cancel is accepted.
- TCP -R, tcpip-forward: a tailssh-owned handler replaces gliderssh's
ForwardedTCPHandler. The bind goes through the monitor's listen op
with the local user's credentials, so privileged ports fail by
kernel construction for non-root-mapped users. Root-mapped users are
allowed, as in OpenSSH. Bind addresses are loopback-only unless
TS_SSH_FORWARD_GATEWAY_PORTS is set, the equivalent of sshd's
GatewayPorts.
- agent forwarding: the socket and its containing directory are
created by a user-cred child, a 0600 socket in a 0700 user-owned
directory, instead of a root-run MkdirTemp, Chmod, and Chown
sequence. Session-end cleanup is unchanged.
The monitor is spawned lazily on the first forwarding or agent need,
from the tailscaled binary, and closed when the SSH connection ends.
closeFwdMonitor latches a closed flag under the fwd lock, so a caller
racing the teardown is refused by fwdMonitor, or reads the current
state without spawning through fwdMonitorIfAny, instead of respawning
a monitor nobody ever closes.
plan9 keeps today's behavior through a build-tagged seam, fwdmonitor.go
and fwdplan9.go. There is no monitor on plan9. Unix forwarding is
rejected there, and reverse TCP forwarding keeps the gliderssh
handler with the mayReversePortForwardTo gate.
Tests cover conn-level callbacks against a real monitor and real
one-shot fwd children: allowlist rejections, user-owned 0600 sockets, a
foreign sticky-dir socket not unlinked, loopback, gateway-ports, and
privileged-port TCP binds, agent directory hygiene and cleanup, and
TS_SSH_DISABLE_FORWARDING coverage, plus refused respawn after
teardown. The unix-fwd root-dial regressions cover a static
in-allowlist symlink to a root-owned 0600 canary, which must fail
permission denied as the bound user, and a dir-swap race, which must
never serve the protected canary.
Updates tailscale/corp#50380
Signed-off-by: James Tucker <james@tailscale.com>
New self-contained code for the per-connection privileged ssh-monitor.
No existing behavior or wiring changes. Later commits wire the
forwarding and session paths onto it.
rpcFrame is the newline-delimited JSON wire struct with request IDs.
It covers dial, listen, exec, signal, and ping requests, ok and err
replies, and unsolicited exit notifications. rpcConn is the framing
codec over one socketpair end. Reads go only through ReadMsgUnix with
a CmsgSpace(3*sizeof(int)) OOB buffer. A plain Read or an undersized
buffer silently drops ancillary data, and the kernel closes the dropped
descriptors. A stream read never returns data beyond an
SCM_RIGHTS-carrying message, so a descriptor arrives with the first
chunk of a split line and survives reassembly. Writes attach
descriptors to the first byte of a line and never to an empty payload.
A short write resends the remainder as plain data, because resending
the ancillary data would duplicate the descriptors. Received
descriptors are marked close-on-exec at parse time, by the codec rather
than by the stdlib's platform-dependent handling. Herald attribution
validates a whole read's herald batch before queueing any set for a
frame, so a failed batch closes its sets exactly once and never
double-closes a set already queued for a frame. Lines are capped at
256 KiB, and NUL bytes and frames carrying more descriptors than their
op declares are rejected on receive. A read timeout returns
os.ErrDeadlineExceeded and preserves any partially received line, so
the monitor's poll loop can sweep between reads without losing the
frame. A read timeout is the only error that preserves state. Every
other error closes all held descriptors and clears the buffer.
sendFds, recvFds, and wrapUnixConn are shared by the monitor and client
sides. wrapUnixConn consumes its fd, because os.NewFile's finalizer
cannot be neutralized from outside.
Tests exercise ping and fd round-trips with liveness and FD_CLOEXEC
checks, multi-fd frames, kernel framing pins for first-chunk ancillary
delivery and undersized-OOB MSG_CTRUNC rejection, 64 KiB-plus
split-line reassembly under a shrunken SO_SNDBUF, oversized and NUL
rejection, the short-write resend path, the read-timeout
state-preservation contract, and 50-way multiplexed request/reply
traffic.
The monitor scrubs every descriptor from 3 up to close-on-exec at
entry and again before each exec. Go's exec closes nothing beyond the
descriptors it passes, so a single non-close-on-exec fd anywhere in the
spawner's table would otherwise reach every child the monitor execs.
close_range(CLOSE_RANGE_CLOEXEC) does the sweep in one syscall on
kernels 5.11+; older kernels scan the self-fd directory. A new test
injects a non-close-on-exec pipe at spawn and pins the clean-child
contract.
spawnRPCChild takes the child's environment explicitly: a nil env
gives the child an empty environment, never the spawner's. The monitor
child is spawned with the daemon's environment; user-identity children
are spawned with none, so the daemon's environment never reaches a
child running as the connection's user.
Updates tailscale/corp#50380
Signed-off-by: James Tucker <james@tailscale.com>