Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd
// monitorClient is tailscaled's side of the ssh-monitor RPC channel.// It spawns the per-connection monitor process, re-exec'ing the// current binary. It multiplexes request IDs, dispatches unsolicited// exit notifications, and serializes writes.//// Dial, Listen, and ListenAgent serve the conn's forwarding paths,// and Exec serves the session path.
package tailssh
import ( "context" "errors" "fmt" "net" "os" "os/exec" "slices" "strings" "sync" "syscall" "time"
"tailscale.com/hostinfo")
// monitorSpawnPrefix is the argv prefix following the executable path,// used to spawn the monitor process: "be-child ssh-monitor" in// tailscaled. Tests override this seam to re-exec the test binary and// dispatch to beMonitor.var monitorSpawnPrefix = []string{"be-child", "ssh-monitor"}
// spawnMonitor spawns the per-connection ssh-monitor process and returns// the connected client. uid/gid/groups/localUser carry the connection's// bound identity, as documented on monitorArgs.func spawnMonitor(exe string, uid, gid uint32, groups []uint32, localUser string) (*monitorClient, error) { gids := make([]string, len(groups)) for i, g := range groups { gids[i] = formatID(g) } argv := append(slices.Clone(monitorSpawnPrefix), "--uid="+formatID(uid), "--gid="+formatID(gid), "--groups="+strings.Join(gids, ","), "--local-user="+localUser, "--rpc-fd=3", ) // The monitor is its own session leader. It is a tailscaled // re-exec, not a service instance. Give it the daemon's environment // without the systemd service variables, so neither the monitor // nor any helper it execs can notify the tailscaled unit. See // [hostinfo.EnvironWithoutSystemdServiceVars]. cmd, parentConn, err := spawnRPCChild("ssh-monitor", exe, argv, hostinfo.EnvironWithoutSystemdServiceVars(nil), &syscall.SysProcAttr{Setsid: true}) if err != nil { return nil, err }
mc := &monitorClient{ c: parentConn, rpc: newRPCConn(parentConn), pending: make(map[int64]chan *rpcResult), cmd: cmd, done: make(chan struct{}), } go mc.readLoop() return mc, nil}
// rpcResult is one inbound reply. It holds the frame and, for the// socket ops, the descriptor the reply carried. Ownership of the// descriptors passes to the waiting call. If there is none, they are// closed.type rpcResult struct { f rpcFrame fds []int}
// monitorClient is tailscaled's connection to one ssh-monitor process.type monitorClient struct { c *net.UnixConn rpc *rpcConn cmd *exec.Cmd
// writeMu serializes writeFrame, so the client has one logical // writer. writeMu sync.Mutex
mu sync.Mutex // guards the fields below nextID int64 pending map[int64]chan *rpcResult onExit func(monitorProcess, int) closed bool
// done is closed when the read loop exits, on RPC EOF or a read // error. The monitor is gone or the channel broke. done chan struct{}}
// Pid returns the monitor process's pid for diagnostics and tests.func (mc *monitorClient) Pid() int { return mc.cmd.Process.Pid}
func (mc *monitorClient) readLoop() { defer close(mc.done) // A protocol/read failure must also trigger monitor teardown. defer mc.c.Close() for { f, fds, err := mc.rpc.readFrame() if err != nil { return } // Acknowledge receipt of any relayed descriptors so the monitor // releases its hold on its copies. The recvmsg in readFrame is // what transfers ownership; see the monitor_rpc.go header for // the darwin kernel behavior the acknowledgment covers. A write // failure means the channel is going away; the monitor's // teardown closes its parked copies either way. if len(fds) > 0 { mc.writeMu.Lock() _ = mc.rpc.writeFrame(rpcFdAckReq(f.ID)) mc.writeMu.Unlock() } mc.dispatch(f, fds) }}
// dispatch routes one inbound frame. Replies, with any carried// descriptors, go to their waiting call; a reply whose caller gave up// has its descriptors closed here. Unsolicited exit notifications go// to the registered callback.func (mc *monitorClient) dispatch(f rpcFrame, fds []int) { if f.ID == 0 { closeFds(fds) // notifications never carry descriptors if f.Op == opExit { mc.mu.Lock() h := mc.onExit mc.mu.Unlock() if h != nil { h(monitorProcess{PID: f.Pid, ID: f.ChildID}, f.Code) } } // Other unsolicited frames are ignored. return } mc.mu.Lock() ch, ok := mc.pending[f.ID] delete(mc.pending, f.ID) if ok { ch <- &rpcResult{f: f, fds: fds} } else { // The caller gave up, on ctx done or client death, and nobody // will consume the descriptors. Close them here. closeFds(fds) } mc.mu.Unlock()}
// call sends a request and waits for its reply, cancellation, or channel death.// Dispatched exec requests ignore cancellation so their child handles cannot be// lost. Descriptors not handed to the caller are closed here or by dispatch.func (mc *monitorClient) call(ctx context.Context, f rpcFrame, fds ...int) (rpcFrame, []int, error) { var zero rpcFrame if err := ctx.Err(); err != nil { return zero, nil, err } mc.mu.Lock() if mc.closed { mc.mu.Unlock() return zero, nil, errors.New("ssh-monitor: client closed") } mc.nextID++ id := mc.nextID f.ID = id ch := make(chan *rpcResult, 1) mc.pending[id] = ch mc.mu.Unlock()
mc.writeMu.Lock() err := ctx.Err() if err == nil { err = mc.rpc.writeFrame(f, fds...) } mc.writeMu.Unlock() if err != nil { mc.mu.Lock() delete(mc.pending, id) mc.mu.Unlock() return zero, nil, fmt.Errorf("ssh-monitor: write %q: %w", f.Op, err) }
done := ctx.Done() if f.Op == opExec { // Once dispatched, exec must return its child handle even if the // session is canceled. The caller then owns termination and waiting. // Monitor death also ends this wait and tears down its children. done = nil } select { case r := <-ch: if !r.f.OK { closeFds(r.fds) // an error reply should carry none; be safe return r.f, nil, errors.New("ssh-monitor: " + r.f.Err) } return r.f, r.fds, nil case <-done: // Delete the entry under the lock: after this, dispatch finds no // waiter and closes a late reply's descriptors itself. A reply // already sitting in the channel is drained here and closed. mc.mu.Lock() delete(mc.pending, id) mc.mu.Unlock() select { case r := <-ch: closeFds(r.fds) default: } return zero, nil, fmt.Errorf("ssh-monitor: op %q: %w", f.Op, ctx.Err()) case <-mc.done: mc.mu.Lock() delete(mc.pending, id) mc.mu.Unlock() select { case r := <-ch: closeFds(r.fds) default: } return zero, nil, errors.New("ssh-monitor: RPC channel closed") }}
// SetExitCallback registers h for unsolicited child exit notifications.// nil disables delivery.func (mc *monitorClient) SetExitCallback(h func(monitorProcess, int)) { mc.mu.Lock() mc.onExit = h mc.mu.Unlock()}
// Gone returns a channel closed when the RPC read loop exits.func (mc *monitorClient) Gone() <-chan struct{} { return mc.done}
// Ping round-trips a ping request.func (mc *monitorClient) Ping(ctx context.Context) error { _, _, err := mc.call(ctx, rpcPingReq(0)) return err}
// execRequest builds an exec frame and descriptor list. envFile, when// present, follows stdio and becomes the child's fd 3.func execRequest(argv, env []string, dir string, ctty bool, envFile *os.File, stdio []*os.File) (rpcFrame, []int) { fds := make([]int, 0, len(stdio)+1) for _, f := range stdio { fds = append(fds, int(f.Fd())) } if envFile != nil { fds = append(fds, int(envFile.Fd())) } f := rpcExecReq(0, argv, env, dir, ctty, envFile != nil, len(fds)) return f, fds}
// execOnMonitor sends an exec frame and returns the child handle.func (mc *monitorClient) execOnMonitor(ctx context.Context, f rpcFrame, fds []int) (monitorProcess, error) { r, _, err := mc.call(ctx, f, fds...) if err != nil { return monitorProcess{}, err } if r.Pid <= 0 || r.ChildID == 0 || r.ChildID != r.ID { mc.c.Close() return monitorProcess{}, errors.New("ssh-monitor: invalid exec child handle") } return monitorProcess{PID: r.Pid, ID: r.ChildID}, nil}
// Exec launches trusted argv with the given environment and directory.// Once dispatched, it waits for the child handle or monitor death even if// ctx is canceled; the caller owns termination of a successfully launched child.func (mc *monitorClient) Exec( ctx context.Context, argv, env []string, dir string, ctty bool, envFile *os.File, stdio ...*os.File,) (monitorProcess, error) { f, fds := execRequest(argv, env, dir, ctty, envFile, stdio) return mc.execOnMonitor(ctx, f, fds)}
// ExecIncubator launches a trusted incubator invocation with its identity// flags normalized to the monitor's bound identity. Like Exec, it returns// the child handle even if ctx is canceled after dispatch.func (mc *monitorClient) ExecIncubator( ctx context.Context, argv, env []string, dir string, ctty bool, envFile *os.File, stdio ...*os.File,) (monitorProcess, error) { f, fds := execRequest(argv, env, dir, ctty, envFile, stdio) f.Incubator = true return mc.execOnMonitor(ctx, f, fds)}
// Signal asks the monitor to signal a child process group.func (mc *monitorClient) Signal(ctx context.Context, child monitorProcess, sig string) error { _, _, err := mc.call(ctx, rpcSignalReq(0, child, sig)) return err}
// Dial dials a unix socket through a relay child running with the// connection's bound identity. The returned data socket and lifetime pipe// are acknowledged to the monitor on receipt, releasing its held copies.// See the monitor_rpc.go header.func (mc *monitorClient) Dial(ctx context.Context, addr string) (*monitorDialConn, error) { _, fds, err := mc.call(ctx, rpcDialReq(0, "unix", addr)) if err != nil { return nil, err } if len(fds) != 2 { closeFds(fds) return nil, fmt.Errorf("ssh-monitor: dial reply carried %d fds, want 2", len(fds)) } lifetime := os.NewFile(uintptr(fds[1]), "ssh-fwd-lifetime") uc, err := wrapUnixConn(fds[0]) // consumes the descriptor if err != nil { lifetime.Close() return nil, err } return &monitorDialConn{UnixConn: uc, lifetime: lifetime}, nil}
// DialTCP resolves and dials addr through a one-shot child running with the// connection's bound identity. The kernel socket owner is preserved when the// connected TCP socket is handed back to tailscaled.func (mc *monitorClient) DialTCP(ctx context.Context, addr string) (*net.TCPConn, error) { _, fds, err := mc.call(ctx, rpcDialReq(0, "tcp", addr)) if err != nil { return nil, err } if len(fds) != 1 { closeFds(fds) return nil, fmt.Errorf("ssh-monitor: TCP dial reply carried %d fds, want 1", len(fds)) } f := os.NewFile(uintptr(fds[0]), "ssh-fwd-tcp") defer f.Close() nc, err := net.FileConn(f) if err != nil { return nil, err } tc, ok := nc.(*net.TCPConn) if !ok { nc.Close() return nil, fmt.Errorf("ssh-monitor: TCP dial returned %T", nc) } return tc, nil}
// monitorDialConn preserves UnixConn's half-close methods, but a full Close// also ends the relay even if the service remains open and silent. Discarding// an unclaimed RPC reply closes the lifetime fd too, without needing a wrapper.type monitorDialConn struct { *net.UnixConn lifetime *os.File}
func (c *monitorDialConn) Close() error { err := c.UnixConn.Close() c.lifetime.Close() return err}
// Listen asks the monitor for a listener bound with the connection's// bound identity; see fwdchild.go for the umask-at-bind construction// and the optional if-owned pre-unlink. Closing the returned listener// does not unlink a unix socket file; a later bind with unlink-if-owned// removes stale socket files.func (mc *monitorClient) Listen(ctx context.Context, network, addr string, unlink bool) (net.Listener, error) { r, fds, err := mc.call(ctx, rpcListenReq(0, network, addr, unlink, false)) if err != nil { return nil, err } if len(fds) != 1 { closeFds(fds) return nil, fmt.Errorf("ssh-monitor: listen reply carried %d fds, want 1", len(fds)) } return wrapListenerFD(fds[0], r.Path)}
// ListenAgent asks the monitor for the agent-forwarding socket: a// user-owned 0700 temp dir and, inside it, a 0600 socket at// listener.sock, created by the bound-identity child. It returns the// listener and the socket path. Removing the containing directory is// the caller's job.func (mc *monitorClient) ListenAgent(ctx context.Context) (ln net.Listener, path string, err error) { r, fds, err := mc.call(ctx, rpcListenReq(0, "", "", false, true)) if err != nil { return nil, "", err } if r.Path == "" { closeFds(fds) return nil, "", errors.New("ssh-monitor: agent listen reply carried no path") } if len(fds) != 1 { closeFds(fds) return nil, "", fmt.Errorf("ssh-monitor: agent listen reply carried %d fds, want 1", len(fds)) } ln, err = wrapListenerFD(fds[0], r.Path) if err != nil { return nil, "", err } return ln, r.Path, nil}
// wrapListenerFD wraps a raw listener descriptor in a net.Listener. fd is// consumed. net.FileListener dups it into the returned listener with// close-on-exec set, and the original is closed here exactly once.func wrapListenerFD(fd int, name string) (net.Listener, error) { f := os.NewFile(uintptr(fd), name) ln, err := net.FileListener(f) f.Close() // exactly-once close of the raw descriptor. The dup in ln is unaffected if err != nil { return nil, err } return ln, nil}
// Close tears the connection down by closing the RPC channel, which// triggers the monitor's teardown. Close waits for the monitor to// exit. A wedged monitor is killed after the forwarding-op deadline,// both teardown reap sweeps, and a margin for scheduling and child reap.func (mc *monitorClient) Close() error { mc.mu.Lock() if mc.closed { mc.mu.Unlock() return nil } mc.closed = true mc.mu.Unlock()
mc.c.Close() <-mc.done waitErr := make(chan error, 1) go func() { waitErr <- mc.cmd.Wait() }() select { case err := <-waitErr: return err case <-time.After(monFwdOpDeadline + 2*monTeardownReap + 5*time.Second): mc.cmd.Process.Kill() return <-waitErr }}