mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-01 05:52:24 +00:00
ssh/tailssh: set default Tailscale SSH $PATH for non-interactive commands
Fixes #5285 Co-authored-by: Andrew Dunham <andrew@tailscale.com> Change-Id: Ic7e967bf6a53b056cac5f21dd39565d9c31563af Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
350aab05e5
commit
56f7da0cfd
@ -31,11 +31,16 @@ 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/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
"tailscale.com/cmd/tailscaled/childproc"
|
"tailscale.com/cmd/tailscaled/childproc"
|
||||||
|
"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/util/strs"
|
||||||
|
"tailscale.com/version/distro"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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
|
// If ss.srv.tailscaledPath is empty, this method is equivalent to
|
||||||
// exec.CommandContext.
|
// 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 (
|
var (
|
||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
@ -292,7 +304,7 @@ func (ss *sshSession) launchProcess() error {
|
|||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cmd.Env = append(cmd.Env, envForUser(ss.conn.localUser)...)
|
cmd.Env = envForUser(ss.conn.localUser)
|
||||||
for _, kv := range ss.Environ() {
|
for _, kv := range ss.Environ() {
|
||||||
if acceptEnvPair(kv) {
|
if acceptEnvPair(kv) {
|
||||||
cmd.Env = append(cmd.Env, kv)
|
cmd.Env = append(cmd.Env, kv)
|
||||||
@ -567,9 +579,66 @@ func envForUser(u *user.User) []string {
|
|||||||
fmt.Sprintf("SHELL=" + loginShell(u.Uid)),
|
fmt.Sprintf("SHELL=" + loginShell(u.Uid)),
|
||||||
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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// 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) {
|
||||||
|
@ -44,6 +44,7 @@ import (
|
|||||||
"tailscale.com/util/lineread"
|
"tailscale.com/util/lineread"
|
||||||
"tailscale.com/util/must"
|
"tailscale.com/util/must"
|
||||||
"tailscale.com/util/strs"
|
"tailscale.com/util/strs"
|
||||||
|
"tailscale.com/version/distro"
|
||||||
"tailscale.com/wgengine"
|
"tailscale.com/wgengine"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -755,3 +756,43 @@ func TestAcceptEnvPair(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPathFromPAMEnvLine(t *testing.T) {
|
||||||
|
u := &user.User{Username: "foo", HomeDir: "/Homes/Foo"}
|
||||||
|
tests := []struct {
|
||||||
|
line string
|
||||||
|
u *user.User
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", &user.User{}, ""},
|
||||||
|
{`PATH DEFAULT="/run/wrappers/bin:@{HOME}/.nix-profile/bin:/etc/profiles/per-user/@{PAM_USER}/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin"`,
|
||||||
|
u, "/run/wrappers/bin:/Homes/Foo/.nix-profile/bin:/etc/profiles/per-user/foo/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin"},
|
||||||
|
{`PATH DEFAULT="@{SOMETHING_ELSE}:nope:@{HOME}"`,
|
||||||
|
u, ""},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
got := pathFromPAMEnvLine([]byte(tt.line), tt.u)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("%d. got %q; want %q", i, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathFromPAMEnvLineOnNixOS(t *testing.T) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
t.Skip("skipping on non-linux")
|
||||||
|
}
|
||||||
|
if distro.Get() != distro.NixOS {
|
||||||
|
t.Skip("skipping on non-NixOS")
|
||||||
|
}
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := defaultPathForUserOnNixOS(u)
|
||||||
|
if got == "" {
|
||||||
|
x, err := os.ReadFile("/etc/pam/environment")
|
||||||
|
t.Fatalf("no result. file was: err=%v, contents=%s", err, x)
|
||||||
|
}
|
||||||
|
t.Logf("success; got=%q", got)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user