Add the one-shot "be-child ssh-fwd" children that perform forwarded
socket work, and the monitor and client plumbing for the socket ops.
The conn and session flows do not use the ops yet. The next commit
routes them.
- fwdchild.go: a child re-execed by the monitor, born unprivileged at
clone. The monitor applies the connection's bound identity as
SysProcAttr.Credential, so the kernel's DAC sees the connection
user's permissions. It dials a unix socket, or creates one with
umask-at-bind mode and optional unlink-if-owned, or binds a TCP
listener, or creates the agent-forwarding socket in a user-owned
directory. The result returns to the monitor over an inherited
socketpair as one status line with at most one descriptor attached.
- monitor.go: handleDial and handleListen run the ops on one-shot
children with a spawn-to-result deadline, monFwdOpDeadline. On
expiry the child's group is SIGKILLed. killChild reaps such a child
with a bounded non-blocking wait. A child left in uninterruptible
sleep is abandoned in its own group rather than blocking the loop
goroutine. The dial and listen placeholder replies are gone, and
their not-implemented test with them.
- monitor_client.go: Dial, Listen, and ListenAgent over the existing
request-ID plumbing.
- incubator.go, monitor_linux.go, monitor_other.go: registration and
per-platform spawn attributes for the fwd children.
Tests drive real children across uid boundaries. They cover dial and
bind round trips, unix and TCP listens, agent-listen directory
hygiene, unlink-if-owned, peer-credential assertions, and the
no-identity-keys-on-the-wire shape of every request.
Forwarding children spawn with an empty environment through
spawnRPCChild: they take everything from argv and the result socket,
and the daemon's environment must not reach a child running as the
connection's user.
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>