Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154// 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) }}