@ -20,7 +20,6 @@ import (
"log/syslog"
"log/syslog"
"os"
"os"
"os/exec"
"os/exec"
"os/user"
"path/filepath"
"path/filepath"
"runtime"
"runtime"
"sort"
"sort"
@ -31,16 +30,12 @@ import (
"github.com/creack/pty"
"github.com/creack/pty"
"github.com/pkg/sftp"
"github.com/pkg/sftp"
"github.com/u-root/u-root/pkg/termios"
"github.com/u-root/u-root/pkg/termios"
"go4.org/mem"
gossh "golang.org/x/crypto/ssh"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/exp/slices"
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
"golang.org/x/sys/unix"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/logger"
"tailscale.com/types/logger"
"tailscale.com/util/lineread"
"tailscale.com/version/distro"
"tailscale.com/version/distro"
)
)
@ -83,7 +78,7 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
case "sftp" :
case "sftp" :
isSFTP = true
isSFTP = true
case "" :
case "" :
name = loginShell ( ss . conn . localUser )
name = ss . conn . localUser . LoginShell ( )
if rawCmd := ss . RawCommand ( ) ; rawCmd != "" {
if rawCmd := ss . RawCommand ( ) ; rawCmd != "" {
args = append ( args , "-c" , rawCmd )
args = append ( args , "-c" , rawCmd )
} else {
} else {
@ -688,113 +683,15 @@ func (ss *sshSession) startWithStdPipes() (err error) {
return nil
return nil
}
}
func loginShell ( u * userMeta ) string {
if u . LoginShell != "" {
// 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 . LoginShell
}
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"
}
func envForUser ( u * userMeta ) [ ] string {
func envForUser ( u * userMeta ) [ ] string {
return [ ] string {
return [ ] string {
fmt . Sprintf ( "SHELL=" + loginShell ( u ) ) ,
fmt . Sprintf ( "SHELL=" + u . LoginShell ( ) ) ,
fmt . Sprintf ( "USER=" + u . Username ) ,
fmt . Sprintf ( "USER=" + u . Username ) ,
fmt . Sprintf ( "HOME=" + u . HomeDir ) ,
fmt . Sprintf ( "HOME=" + u . HomeDir ) ,
fmt . Sprintf ( "PATH=" + defaultPathForUser ( & u . User ) ) ,
fmt . Sprintf ( "PATH=" + defaultPathForUser ( & u . User ) ) ,
}
}
}
}
// 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 )
}
isRoot := u . Uid == "0"
switch distro . Get ( ) {
case distro . Debian :
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/bn/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 {
var path string
lineread . File ( "/etc/pam/environment" , func ( lineb [ ] byte ) error {
if v := pathFromPAMEnvLine ( lineb , u ) ; v != "" {
path = v
return io . EOF // stop iteration
}
return nil
} )
return path
}
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
}
// updateStringInSlice mutates ss to change the first occurrence of a
// updateStringInSlice mutates ss to change the first occurrence of a
// to b.
// to b.
func updateStringInSlice ( ss [ ] string , a , b string ) {
func updateStringInSlice ( ss [ ] string , a , b string ) {