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
139 lines
3.8 KiB
Go
139 lines
3.8 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 (
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"go4.org/mem"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/hostinfo"
|
|
"tailscale.com/util/lineiter"
|
|
"tailscale.com/util/osuser"
|
|
"tailscale.com/version/distro"
|
|
)
|
|
|
|
// GroupIds returns the list of group IDs that the user is a member of.
|
|
func (u *userMeta) GroupIds() ([]string, error) {
|
|
return osuser.GetGroupIds(&u.User)
|
|
}
|
|
|
|
// isRootUser reports whether u is the superuser.
|
|
func isRootUser(u *userMeta) bool {
|
|
return u.Uid == "0" || u.Username == "root"
|
|
}
|
|
|
|
func (u *userMeta) LoginShell() string {
|
|
if runtime.GOOS == "plan9" {
|
|
return "/bin/rc"
|
|
}
|
|
if u.loginShellCached != "" {
|
|
// This field should be populated on Linux, at least, because
|
|
// func userLookup on Linux uses "getent" to look up the user
|
|
// and that populates it.
|
|
return u.loginShellCached
|
|
}
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
// Note: /Users/username is key, and not the same as u.HomeDir.
|
|
out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", u.Username), "UserShell").Output()
|
|
// out is "UserShell: /bin/bash"
|
|
s, ok := strings.CutPrefix(string(out), "UserShell: ")
|
|
if ok {
|
|
return strings.TrimSpace(s)
|
|
}
|
|
}
|
|
if e := os.Getenv("SHELL"); e != "" {
|
|
return e
|
|
}
|
|
return "/bin/sh"
|
|
}
|
|
|
|
// defaultPathTmpl specifies the default PATH template to use for new sessions.
|
|
//
|
|
// If empty, a default value is used based on the OS & distro to match OpenSSH's
|
|
// usually-hardcoded behavior. (see
|
|
// https://github.com/tailscale/tailscale/issues/5285 for background).
|
|
//
|
|
// The template may contain @{HOME} or @{PAM_USER} which expand to the user's
|
|
// home directory and username, respectively. (PAM is not used, despite the
|
|
// name)
|
|
var defaultPathTmpl = envknob.RegisterString("TAILSCALE_SSH_DEFAULT_PATH")
|
|
|
|
func defaultPathForUser(u *user.User) string {
|
|
if s := defaultPathTmpl(); s != "" {
|
|
return expandDefaultPathTmpl(s, u)
|
|
}
|
|
if runtime.GOOS == "plan9" {
|
|
return "/bin"
|
|
}
|
|
isRoot := u.Uid == "0"
|
|
switch distro.Get() {
|
|
case distro.Debian, distro.Crostini:
|
|
hi := hostinfo.New()
|
|
if hi.Distro == "ubuntu" {
|
|
// distro.Get's Debian includes Ubuntu. But see if it's actually Ubuntu.
|
|
// Ubuntu doesn't empirically seem to distinguish between root and non-root for the default.
|
|
// And it includes /snap/bin.
|
|
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
|
|
}
|
|
if isRoot {
|
|
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
}
|
|
return "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
|
|
case distro.NixOS:
|
|
return defaultPathForUserOnNixOS(u)
|
|
}
|
|
if isRoot {
|
|
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
}
|
|
return "/usr/local/bin:/usr/bin:/bin"
|
|
}
|
|
|
|
func defaultPathForUserOnNixOS(u *user.User) string {
|
|
for lr := range lineiter.File("/etc/pam/environment") {
|
|
lineb, err := lr.Value()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if v := pathFromPAMEnvLine(lineb, u); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {
|
|
if !mem.HasPrefix(mem.B(line), mem.S("PATH")) {
|
|
return ""
|
|
}
|
|
rest := strings.TrimSpace(strings.TrimPrefix(string(line), "PATH"))
|
|
if quoted, ok := strings.CutPrefix(rest, "DEFAULT="); ok {
|
|
if path, err := strconv.Unquote(quoted); err == nil {
|
|
return expandDefaultPathTmpl(path, u)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func expandDefaultPathTmpl(t string, u *user.User) string {
|
|
p := strings.NewReplacer(
|
|
"@{HOME}", u.HomeDir,
|
|
"@{PAM_USER}", u.Username,
|
|
).Replace(t)
|
|
if strings.Contains(p, "@{") {
|
|
// If there are unknown expansions, conservatively fail closed.
|
|
return ""
|
|
}
|
|
return p
|
|
}
|