mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-22 03:25:16 -04:00
The main server file mixed portable session handling with Unix details: sending SIGHUP to end a session, decoding exec.ExitError, the euid check for whether the process can switch users, agent forwarding's chown of the socket, and reading /etc/ssh host keys as root. Those now sit behind small functions (hangupProcess, waitProcess, canSwitchToLocalUser, handleSSHAgentForwarding, systemHostKeyFile, isRootUser) in the new process_unix.go, along with the incubator's forwarded-environment pipe helpers, and the session's *exec.Cmd moves into an embedded osSessionState struct defined there, so that the portable code no longer refers to the process representation at all. user.go keeps only the portable userMeta and userLookup; the login shell and default PATH logic moves to user_unix.go. The SFTP child entrypoint and its stdio adapter, which incubator.go and incubator_plan9.go each had a copy of, move to sftp.go. The c2n usernames handler gains a hook for platforms that list users some other way than /etc/passwd. The agent socket's uid and gid are parsed as 31-bit rather than 32-bit unsigned values so that the conversion to int for os.Chown cannot overflow on 32-bit platforms, which is the pattern CodeQL flags. Updates #cleanup Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I4c7e2b9a0d3f5e1c8b6a4d2f0e9c7b5a3d1f8e6c
151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd || plan9
|
|
|
|
package tailssh
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"syscall"
|
|
|
|
gliderssh "github.com/tailscale/gliderssh"
|
|
)
|
|
|
|
// osSessionState holds the platform-specific state of an [sshSession]'s
|
|
// process. It is embedded in [sshSession] and initialized by launchProcess.
|
|
type osSessionState struct {
|
|
cmd *exec.Cmd // the session's incubator or shell process
|
|
}
|
|
|
|
// forwardedEnvChildFD is the fd the incubator child reads the forwarded environment from, sent via
|
|
// --env-fd. It must match the payload file's index in launchProcess's ExtraFiles (fd = 3 + index).
|
|
const forwardedEnvChildFD = 3
|
|
|
|
// forwardedEnvFile returns the read end of a pipe holding the JSON-encoded forwarded pairs.
|
|
// The read end is passed to the incubator child via exec.Cmd.ExtraFiles to communicate
|
|
// secrets and config; the payload only ever exists in memory, never on any filesystem. A
|
|
// goroutine writes the payload and closes the write end. Caller must close the read end
|
|
// after the child starts.
|
|
func forwardedEnvFile(forwardedEnv []string) (*os.File, error) {
|
|
if len(forwardedEnv) == 0 {
|
|
return nil, errors.New("no forwarded environment")
|
|
}
|
|
b, err := json.Marshal(forwardedEnv)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshaling forwarded environment: %w", err)
|
|
}
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating forwarded environment pipe: %w", err)
|
|
}
|
|
go func() {
|
|
defer w.Close()
|
|
// A short read fails the session child-side
|
|
_, _ = w.Write(b)
|
|
}()
|
|
return r, nil
|
|
}
|
|
|
|
// canSwitchToLocalUser reports whether this process can run a session as lu.
|
|
// A non-root tailscaled can only run sessions as itself.
|
|
func canSwitchToLocalUser(lu *userMeta) error {
|
|
if euid := os.Geteuid(); euid != 0 && runtime.GOOS != "plan9" {
|
|
if lu.Uid != fmt.Sprint(euid) {
|
|
return fmt.Errorf("can't switch to user %q from process euid %v", lu.Username, euid)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// waitProcess waits for the session's process to exit and returns its exit
|
|
// code. A non-nil error means the exit code could not be determined.
|
|
func (ss *sshSession) waitProcess() (exitCode int, err error) {
|
|
err = ss.cmd.Wait()
|
|
if err == nil {
|
|
return 0, nil
|
|
}
|
|
if ee, ok := errors.AsType[*exec.ExitError](err); ok {
|
|
return ee.ProcessState.ExitCode(), nil
|
|
}
|
|
return 1, err
|
|
}
|
|
|
|
// hangupProcess asks the session's process to terminate because the session
|
|
// is over.
|
|
func (ss *sshSession) hangupProcess() {
|
|
// SIGHUP = POSIX terminal-disconnect semantics; OpenSSH gets it
|
|
// implicitly via PTY-master close (session.c:2246), we send it
|
|
// explicitly because non-PTY sessions use pipes.
|
|
ss.cmd.Process.Signal(syscall.SIGHUP)
|
|
}
|
|
|
|
// systemHostKeyFile returns the path of the system's OpenSSH host key of the
|
|
// given type ("rsa", "ecdsa", "ed25519") for tailssh to reuse, or "" to
|
|
// generate its own key instead. Only root can read the system keys.
|
|
func systemHostKeyFile(typ string) string {
|
|
if os.Geteuid() != 0 {
|
|
return ""
|
|
}
|
|
return "/etc/ssh/ssh_host_" + typ + "_key"
|
|
}
|
|
|
|
// handleSSHAgentForwarding starts a Unix socket listener and in the background
|
|
// forwards agent connections between the listener and the gliderssh.Session.
|
|
// On success, it assigns ss.agentListener.
|
|
func (ss *sshSession) handleSSHAgentForwarding(s gliderssh.Session, lu *userMeta) error {
|
|
if !gliderssh.AgentRequested(ss) || !ss.conn.finalAction.AllowAgentForwarding {
|
|
return nil
|
|
}
|
|
if sshDisableForwarding() {
|
|
// TODO(bradfitz): or do we want to return an error here instead so the user
|
|
// gets an error if they ran with ssh -A? But for now we just silently
|
|
// don't work, like the condition above.
|
|
return nil
|
|
}
|
|
ss.logf("ssh: agent forwarding requested")
|
|
ln, err := gliderssh.NewAgentListener()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err != nil && ln != nil {
|
|
ln.Close()
|
|
}
|
|
}()
|
|
|
|
// 31 bits so that the values fit an int on every platform.
|
|
uid, err := strconv.ParseUint(lu.Uid, 10, 31)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gid, err := strconv.ParseUint(lu.Gid, 10, 31)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
socket := ln.Addr().String()
|
|
dir := filepath.Dir(socket)
|
|
// Make sure the socket is accessible only by the user.
|
|
if err := os.Chmod(socket, 0600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chown(socket, int(uid), int(gid)); err != nil {
|
|
return err
|
|
}
|
|
// Make sure the dir is also accessible.
|
|
if err := os.Chmod(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
go gliderssh.ForwardAgentConnections(ln, s)
|
|
ss.agentListener = ln
|
|
return nil
|
|
}
|