|
|
@@ -12,11 +12,13 @@
|
|
|
package tailssh
|
|
|
|
|
|
import (
|
|
|
+ "context"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"flag"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
+ "io/fs"
|
|
|
"log"
|
|
|
"log/syslog"
|
|
|
"os"
|
|
|
@@ -29,6 +31,7 @@ import (
|
|
|
"strings"
|
|
|
"sync/atomic"
|
|
|
"syscall"
|
|
|
+ "time"
|
|
|
|
|
|
"github.com/creack/pty"
|
|
|
"github.com/pkg/sftp"
|
|
|
@@ -70,11 +73,36 @@ var maybeStartLoginSession = func(dlogf logger.Logf, ia incubatorArgs) (close fu
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
+// tryExecInDir tries to run a command in dir and returns nil if it succeeds.
|
|
|
+// Otherwise, it returns a filesystem error or a timeout error if the command
|
|
|
+// took too long.
|
|
|
+func tryExecInDir(ctx context.Context, dir string) error {
|
|
|
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
|
+ defer cancel()
|
|
|
+
|
|
|
+ // Assume that the following executables exist, are executable, and
|
|
|
+ // immediately return.
|
|
|
+ var name string
|
|
|
+ switch runtime.GOOS {
|
|
|
+ case "windows":
|
|
|
+ windir := os.Getenv("windir")
|
|
|
+ name = filepath.Join(windir, "system32", "doskey.exe")
|
|
|
+ default:
|
|
|
+ name = "/bin/true"
|
|
|
+ }
|
|
|
+
|
|
|
+ cmd := exec.CommandContext(ctx, name)
|
|
|
+ cmd.Dir = dir
|
|
|
+ return cmd.Run()
|
|
|
+}
|
|
|
+
|
|
|
// newIncubatorCommand returns a new exec.Cmd configured with
|
|
|
// `tailscaled be-child ssh` as the entrypoint.
|
|
|
//
|
|
|
-// If ss.srv.tailscaledPath is empty, this method is equivalent to
|
|
|
-// exec.CommandContext.
|
|
|
+// If ss.srv.tailscaledPath is empty, this method is almost equivalent to
|
|
|
+// exec.CommandContext. It will refuse to run in SFTP-mode. It will simulate the
|
|
|
+// behavior of SSHD when by falling back to the root directory if it cannot run
|
|
|
+// a command in the user’s home directory.
|
|
|
//
|
|
|
// The returned Cmd.Env is guaranteed to be nil; the caller populates it.
|
|
|
func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err error) {
|
|
|
@@ -104,7 +132,35 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err
|
|
|
loginShell := ss.conn.localUser.LoginShell()
|
|
|
args := shellArgs(isShell, ss.RawCommand())
|
|
|
logf("directly running %s %q", loginShell, args)
|
|
|
- return exec.CommandContext(ss.ctx, loginShell, args...), nil
|
|
|
+ cmd = exec.CommandContext(ss.ctx, loginShell, args...)
|
|
|
+
|
|
|
+ // While running directly instead of using `tailscaled be-child`,
|
|
|
+ // do what sshd does by running inside the home directory,
|
|
|
+ // falling back to the root directory it doesn't have permissions.
|
|
|
+ // This can happen if the system has networked home directories,
|
|
|
+ // i.e. NFS or SMB, which enable root-squashing by default.
|
|
|
+ cmd.Dir = ss.conn.localUser.HomeDir
|
|
|
+ err := tryExecInDir(ss.ctx, cmd.Dir)
|
|
|
+ switch {
|
|
|
+ case errors.Is(err, exec.ErrNotFound):
|
|
|
+ // /bin/true might not be installed on a barebones system,
|
|
|
+ // so we assume that the home directory does not exist.
|
|
|
+ cmd.Dir = "/"
|
|
|
+ case errors.Is(err, fs.ErrPermission) || errors.Is(err, fs.ErrNotExist):
|
|
|
+ // Ensure that cmd.Dir is the source of the error.
|
|
|
+ var pathErr *fs.PathError
|
|
|
+ if errors.As(err, &pathErr) && pathErr.Path == cmd.Dir {
|
|
|
+ // If we cannot run loginShell in localUser.HomeDir,
|
|
|
+ // we will try to run this command in the root directory.
|
|
|
+ cmd.Dir = "/"
|
|
|
+ } else {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ case err != nil:
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return cmd, nil
|
|
|
}
|
|
|
|
|
|
lu := ss.conn.localUser
|
|
|
@@ -178,7 +234,10 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...), nil
|
|
|
+ cmd = exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...)
|
|
|
+ // The incubator will chdir into the home directory after it drops privileges.
|
|
|
+ cmd.Dir = "/"
|
|
|
+ return cmd, nil
|
|
|
}
|
|
|
|
|
|
var debugIncubator bool
|
|
|
@@ -777,7 +836,6 @@ func (ss *sshSession) launchProcess() error {
|
|
|
}
|
|
|
|
|
|
cmd := ss.cmd
|
|
|
- cmd.Dir = "/"
|
|
|
cmd.Env = envForUser(ss.conn.localUser)
|
|
|
for _, kv := range ss.Environ() {
|
|
|
if acceptEnvPair(kv) {
|