mirror of
https://github.com/tailscale/tailscale.git
synced 2026-11-02 23:54:39 -05:00
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
124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd
|
|
|
|
package tailssh
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// connMonitorState owns the connection's process monitor. It is started
|
|
// on the first session or forwarding request and closed at connection teardown.
|
|
// Its local user, UID, GID, and supplementary groups are fixed at startup
|
|
// from the identity selected by clientAuth.
|
|
type connMonitorState struct {
|
|
mu sync.Mutex
|
|
mon *monitorClient
|
|
|
|
// closed is set by closeConnMonitor, before the lock is dropped. Once
|
|
// the conn's teardown has run, connMonitor refuses to spawn.
|
|
// connMonitorIfAny is unaffected.
|
|
closed bool
|
|
|
|
// exits routes notifications by child handle. Its callback is installed
|
|
// at monitor spawn.
|
|
exits monExitRegistry
|
|
}
|
|
|
|
// connMonitor returns the conn's monitor, spawning it on first use with
|
|
// the spawn-bound identity documented on connMonitorState.
|
|
func (c *conn) connMonitor() (*monitorClient, error) {
|
|
c.monitor.mu.Lock()
|
|
defer c.monitor.mu.Unlock()
|
|
if c.monitor.mon != nil {
|
|
return c.monitor.mon, nil
|
|
}
|
|
if c.monitor.closed {
|
|
return nil, errors.New("monitor closed at connection teardown")
|
|
}
|
|
uid, gid, groups, err := c.monitorCreds()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The monitor is exec'd from the tailscaled binary; in tests
|
|
// os.Executable() is the test binary. The incubator spawn takes the
|
|
// same path from srv.tailscaledPath.
|
|
exe := c.srv.tailscaledPath
|
|
if exe == "" {
|
|
exe, err = os.Executable()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ssh-monitor: executable: %w", err)
|
|
}
|
|
}
|
|
mc, err := spawnMonitor(exe, uid, gid, groups, c.localUser.Username)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("spawning ssh-monitor for %q: %w", c.localUser.Username, err)
|
|
}
|
|
// Route the monitor's async exit notifications to the conn's
|
|
// sessions in sessmonitor.go.
|
|
mc.SetExitCallback(c.monitor.exits.dispatchExit)
|
|
c.monitor.mon = mc
|
|
return mc, nil
|
|
}
|
|
|
|
// connMonitorIfAny returns the connection's monitor without starting one.
|
|
// It returns nil before startup and after closeConnMonitor.
|
|
func (c *conn) connMonitorIfAny() *monitorClient {
|
|
c.monitor.mu.Lock()
|
|
defer c.monitor.mu.Unlock()
|
|
return c.monitor.mon
|
|
}
|
|
|
|
// monitorCreds returns the monitor's spawn identity, parsing like the
|
|
// incubator's argv does. The ids are carried at their 32-bit ABI width.
|
|
func (c *conn) monitorCreds() (uid, gid uint32, groups []uint32, err error) {
|
|
u64, err := strconv.ParseUint(c.localUser.Uid, 10, 32)
|
|
if err != nil {
|
|
return 0, 0, nil, fmt.Errorf("ssh-monitor: parsing local user uid %q: %w", c.localUser.Uid, err)
|
|
}
|
|
g64, err := strconv.ParseUint(c.localUser.Gid, 10, 32)
|
|
if err != nil {
|
|
return 0, 0, nil, fmt.Errorf("ssh-monitor: parsing local user gid %q: %w", c.localUser.Gid, err)
|
|
}
|
|
for _, g := range c.userGroupIDs {
|
|
v, err := strconv.ParseUint(g, 10, 32)
|
|
if err != nil {
|
|
return 0, 0, nil, fmt.Errorf("ssh-monitor: parsing local user group %q: %w", g, err)
|
|
}
|
|
groups = append(groups, uint32(v))
|
|
}
|
|
return uint32(u64), uint32(g64), groups, nil
|
|
}
|
|
|
|
// closeConnMonitor closes the connection monitor, if any.
|
|
// If monitorKillAll is set, teardown must terminate all session children.
|
|
// Subsequent calls have no effect.
|
|
func (c *conn) closeConnMonitor() {
|
|
c.monitor.mu.Lock()
|
|
mc := c.monitor.mon
|
|
c.monitor.mon = nil
|
|
c.monitor.closed = true
|
|
c.monitor.mu.Unlock()
|
|
if mc == nil {
|
|
return
|
|
}
|
|
if c.monitorKillAll.Load() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
if err := mc.KillAll(ctx); err != nil {
|
|
c.logf("ssh: arming monitor kill-all: %v", err)
|
|
}
|
|
cancel()
|
|
}
|
|
if err := mc.Close(); err != nil {
|
|
c.logf("ssh: closing connection monitor: %v", err)
|
|
}
|
|
}
|