mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-28 22:19:12 -04:00
Move lazy monitor startup, credentials, and cleanup into connmonitor.go and rename the connection state and accessors. The monitor owns session processes as well as forwarding children. Move the session exit registry alongside session monitor handling and keep the Plan 9 stub in a matching file. Update callers, tests, and diagnostic prefixes without changing monitor lifecycle behavior. Updates tailscale/corp#50380 Change-Id: I760fa009176491da426639615907d129b68026fc Signed-off-by: James Tucker <james@tailscale.com>
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd
|
|
|
|
// Session processes are launched by the connection's privileged monitor.
|
|
// tailscaled keeps the parent I/O descriptors and passes the child's stdio
|
|
// and optional environment pipe via SCM_RIGHTS.
|
|
|
|
package tailssh
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// hangupEscalation is the delay between SIGHUP and SIGKILL on session teardown.
|
|
const hangupEscalation = 5 * time.Second
|
|
|
|
// monExitRegistry matches exit notifications to child handles, including
|
|
// notifications received before the session registers its waiter.
|
|
type monExitRegistry struct {
|
|
mu sync.Mutex
|
|
waiters map[monitorProcess]*monExitStatus
|
|
pending map[monitorProcess]int // exits received before registration
|
|
}
|
|
|
|
// dispatchExit is the monitorClient exit callback, called from the
|
|
// client's read-loop goroutine.
|
|
func (s *monExitRegistry) dispatchExit(pid monitorProcess, code int) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if ch, ok := s.waiters[pid]; ok {
|
|
delete(s.waiters, pid)
|
|
ch.complete(code)
|
|
return
|
|
}
|
|
if s.pending == nil {
|
|
s.pending = make(map[monitorProcess]int)
|
|
}
|
|
s.pending[pid] = code
|
|
}
|
|
|
|
// registerSessionExit delivers and removes a pending exit, or registers
|
|
// a waiter for a future notification.
|
|
func (s *monExitRegistry) registerSessionExit(pid monitorProcess, ch *monExitStatus) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if code, ok := s.pending[pid]; ok {
|
|
delete(s.pending, pid)
|
|
ch.complete(code)
|
|
return
|
|
}
|
|
if s.waiters == nil {
|
|
s.waiters = make(map[monitorProcess]*monExitStatus)
|
|
}
|
|
s.waiters[pid] = ch
|
|
}
|
|
|
|
func newMonExitStatus() *monExitStatus {
|
|
return &monExitStatus{done: make(chan struct{})}
|
|
}
|
|
|
|
func (s *monExitStatus) complete(code int) {
|
|
s.code = code
|
|
close(s.done)
|
|
}
|
|
|
|
// launchViaMonitor passes cmd and the child's descriptors to the monitor.
|
|
// After dispatch, cancellation does not abandon the exec reply: a successful
|
|
// launch always registers the child so run can terminate it and wait for exit.
|
|
func (ss *sshSession) launchViaMonitor(cmd *exec.Cmd, envFile *os.File, stdio []*os.File, ctty bool) error {
|
|
if len(stdio) != 3 {
|
|
return fmt.Errorf("internal error: monitor exec needs 3 stdio fds, got %d", len(stdio))
|
|
}
|
|
if err := ss.ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
mc, err := ss.conn.connMonitor()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
child, err := mc.ExecIncubator(ss.ctx, cmd.Args, cmd.Env, cmd.Dir, ctty, envFile, stdio...)
|
|
if err != nil {
|
|
return fmt.Errorf("exec on session monitor: %w", err)
|
|
}
|
|
exit := newMonExitStatus()
|
|
ss.conn.monitor.exits.registerSessionExit(child, exit)
|
|
ss.monChild = child
|
|
ss.monExit = exit
|
|
ss.monDead = mc.Gone()
|
|
return nil
|
|
}
|
|
|
|
// waitMonitorProcess returns the child's exit code, or an error if its monitor
|
|
// dies without reporting an exit. ok is false for directly launched sessions.
|
|
func (ss *sshSession) waitMonitorProcess() (code int, ok bool, err error) {
|
|
if ss.monExit == nil {
|
|
return 0, false, nil
|
|
}
|
|
select {
|
|
case <-ss.monExit.done:
|
|
return ss.monExit.code, true, nil
|
|
case <-ss.monDead:
|
|
// The read loop delivers notifications before closing monDead.
|
|
// Recheck here because both cases may have become ready together.
|
|
select {
|
|
case <-ss.monExit.done:
|
|
return ss.monExit.code, true, nil
|
|
default:
|
|
return 1, true, errors.New("session monitor exited before the session process")
|
|
}
|
|
}
|
|
}
|
|
|
|
// hangupMonitorProcess sends SIGHUP, then SIGKILL after hangupEscalation if the
|
|
// child has not exited. It returns false for directly launched sessions.
|
|
func (ss *sshSession) hangupMonitorProcess() bool {
|
|
if ss.monChild.PID == 0 {
|
|
return false
|
|
}
|
|
mc := ss.conn.connMonitorIfAny()
|
|
if mc == nil {
|
|
// Monitor teardown owns cleanup. Its reaped PIDs may already have
|
|
// been reused, so tailscaled must not signal them directly.
|
|
return true
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := mc.Signal(ctx, ss.monChild, "SIGHUP"); err != nil {
|
|
ss.logf("ssh: SIGHUP session process group: %v", err)
|
|
}
|
|
time.AfterFunc(hangupEscalation, func() { ss.escalateMonitorProcess(mc) })
|
|
return true
|
|
}
|
|
|
|
func (ss *sshSession) escalateMonitorProcess(mc *monitorClient) {
|
|
select {
|
|
case <-ss.monExit.done:
|
|
return
|
|
default:
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := mc.Signal(ctx, ss.monChild, "SIGKILL"); err != nil {
|
|
ss.vlogf("ssh: SIGKILL session process group: %v", err)
|
|
}
|
|
}
|