mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-29 14:39:16 -04:00
main
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
79d5abe043 |
ssh/tailssh: launch sessions via the monitor
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> |
||
|
|
eba00ab363 |
ssh/tailssh: route SSH forwarding through the privileged monitor
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> |
||
|
|
de16943cbf |
ssh/tailssh: add user-credential forwarding children and monitor dial/listen ops
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> |
||
|
|
ed7903582b |
ssh/tailssh: add monitor RPC codec and SCM_RIGHTS fd passing
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> |