|
|
@@ -31,11 +31,16 @@ import (
|
|
|
"github.com/creack/pty"
|
|
|
"github.com/pkg/sftp"
|
|
|
"github.com/u-root/u-root/pkg/termios"
|
|
|
+ "go4.org/mem"
|
|
|
gossh "golang.org/x/crypto/ssh"
|
|
|
"golang.org/x/sys/unix"
|
|
|
"tailscale.com/cmd/tailscaled/childproc"
|
|
|
+ "tailscale.com/hostinfo"
|
|
|
"tailscale.com/tempfork/gliderlabs/ssh"
|
|
|
"tailscale.com/types/logger"
|
|
|
+ "tailscale.com/util/lineread"
|
|
|
+ "tailscale.com/util/strs"
|
|
|
+ "tailscale.com/version/distro"
|
|
|
)
|
|
|
|
|
|
func init() {
|
|
|
@@ -59,7 +64,14 @@ var maybeStartLoginSession = func(logf logger.Logf, ia incubatorArgs) (close fun
|
|
|
//
|
|
|
// If ss.srv.tailscaledPath is empty, this method is equivalent to
|
|
|
// exec.CommandContext.
|
|
|
-func (ss *sshSession) newIncubatorCommand() *exec.Cmd {
|
|
|
+//
|
|
|
+// The returned Cmd.Env is guaranteed to be nil; the caller populates it.
|
|
|
+func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
|
|
|
+ defer func() {
|
|
|
+ if cmd.Env != nil {
|
|
|
+ panic("internal error")
|
|
|
+ }
|
|
|
+ }()
|
|
|
var (
|
|
|
name string
|
|
|
args []string
|
|
|
@@ -292,7 +304,7 @@ func (ss *sshSession) launchProcess() error {
|
|
|
} else {
|
|
|
return err
|
|
|
}
|
|
|
- cmd.Env = append(cmd.Env, envForUser(ss.conn.localUser)...)
|
|
|
+ cmd.Env = envForUser(ss.conn.localUser)
|
|
|
for _, kv := range ss.Environ() {
|
|
|
if acceptEnvPair(kv) {
|
|
|
cmd.Env = append(cmd.Env, kv)
|
|
|
@@ -567,7 +579,64 @@ func envForUser(u *user.User) []string {
|
|
|
fmt.Sprintf("SHELL=" + loginShell(u.Uid)),
|
|
|
fmt.Sprintf("USER=" + u.Username),
|
|
|
fmt.Sprintf("HOME=" + u.HomeDir),
|
|
|
+ fmt.Sprintf("PATH=" + defaultPathForUser(u)),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func defaultPathForUser(u *user.User) string {
|
|
|
+ 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 := strs.CutPrefix(rest, "DEFAULT="); ok {
|
|
|
+ if path, err := strconv.Unquote(quoted); err == nil {
|
|
|
+ path = strings.NewReplacer(
|
|
|
+ "@{HOME}", u.HomeDir,
|
|
|
+ "@{PAM_USER}", u.Username,
|
|
|
+ ).Replace(path)
|
|
|
+ if !strings.Contains(path, "@{") {
|
|
|
+ // If no more expansions, use it. Otherwise we fail closed.
|
|
|
+ return path
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+ return ""
|
|
|
}
|
|
|
|
|
|
// updateStringInSlice mutates ss to change the first occurrence of a
|