From 695f8a1d7e4dfb6a01a83b505f34778408b882a1 Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Thu, 21 Apr 2022 10:11:16 -0700 Subject: [PATCH] ssh/tailssh: add support for sftp Updates #3802 Signed-off-by: Maisem Ali --- cmd/tailscaled/depaware.txt | 5 ++- ssh/tailssh/incubator.go | 86 +++++++++++++++++++++++++++---------- ssh/tailssh/tailssh.go | 57 ++++++++++++++---------- 3 files changed, 103 insertions(+), 45 deletions(-) diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index b2e977957..69414a08e 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -82,6 +82,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd github.com/klauspost/compress/zstd from tailscale.com/smallzstd github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd + LD github.com/kr/fs from github.com/pkg/sftp L github.com/mdlayher/genetlink from tailscale.com/net/tstun L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ @@ -89,6 +90,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink 💣 github.com/mitchellh/go-ps from tailscale.com/safesocket W github.com/pkg/errors from github.com/tailscale/certstore + LD github.com/pkg/sftp from tailscale.com/ssh/tailssh + LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh LD 💣 github.com/tailscale/golang-x-crypto/internal/subtle from github.com/tailscale/golang-x-crypto/chacha20 @@ -298,7 +301,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box golang.org/x/crypto/poly1305 from golang.zx2c4.com/wireguard/device+ golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+ - LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh + LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+ golang.org/x/net/bpf from github.com/mdlayher/genetlink+ golang.org/x/net/dns/dnsmessage from net+ golang.org/x/net/http/httpguts from golang.org/x/net/http2+ diff --git a/ssh/tailssh/incubator.go b/ssh/tailssh/incubator.go index 5f33077ba..f1d61ffe3 100644 --- a/ssh/tailssh/incubator.go +++ b/ssh/tailssh/incubator.go @@ -13,7 +13,6 @@ package tailssh import ( - "context" "errors" "flag" "fmt" @@ -29,6 +28,7 @@ import ( "syscall" "github.com/creack/pty" + "github.com/pkg/sftp" "github.com/u-root/u-root/pkg/termios" gossh "golang.org/x/crypto/ssh" "golang.org/x/sys/unix" @@ -58,9 +58,29 @@ var maybeStartLoginSession = func(logf logger.Logf, uid uint32, localUser, remot // // If ss.srv.tailscaledPath is empty, this method is equivalent to // exec.CommandContext. -func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args []string) *exec.Cmd { +func (ss *sshSession) newIncubatorCommand() *exec.Cmd { + var ( + name string + args []string + isSFTP bool + ) + switch ss.Subsystem() { + case "sftp": + isSFTP = true + case "": + name = loginShell(ss.conn.localUser.Uid) + if rawCmd := ss.RawCommand(); rawCmd != "" { + args = append(args, "-c", rawCmd) + } else { + args = append(args, "-l") // login shell + } + default: + panic(fmt.Sprintf("unexpected subsystem: %v", ss.Subsystem())) + } + if ss.conn.srv.tailscaledPath == "" { - return exec.CommandContext(ctx, name, args...) + // TODO(maisem): this doesn't work with sftp + return exec.CommandContext(ss.ctx, name, args...) } lu := ss.conn.localUser ci := ss.conn.info @@ -76,20 +96,39 @@ func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args "--local-user=" + lu.Username, "--remote-user=" + remoteUser, "--remote-ip=" + ci.src.IP().String(), - "--cmd=" + name, "--has-tty=false", // updated in-place by startWithPTY "--tty-name=", // updated in-place by startWithPTY } - if len(args) > 0 { - incubatorArgs = append(incubatorArgs, "--") - incubatorArgs = append(incubatorArgs, args...) - } - return exec.CommandContext(ctx, ss.conn.srv.tailscaledPath, incubatorArgs...) + if isSFTP { + incubatorArgs = append(incubatorArgs, "--sftp") + } else { + incubatorArgs = append(incubatorArgs, "--cmd="+name) + if len(args) > 0 { + incubatorArgs = append(incubatorArgs, "--") + incubatorArgs = append(incubatorArgs, args...) + } + } + return exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...) } const debugIncubator = false +type stdRWC struct{} + +func (stdRWC) Read(p []byte) (n int, err error) { + return os.Stdin.Read(p) +} + +func (stdRWC) Write(b []byte) (n int, err error) { + return os.Stdout.Write(b) +} + +func (stdRWC) Close() error { + os.Exit(0) + return nil +} + // beIncubator is the entrypoint to the `tailscaled be-child ssh` subcommand. // It is responsible for informing the system of a new login session for the user. // This is sometimes necessary for mounting home directories and decrypting file @@ -107,7 +146,8 @@ func beIncubator(args []string) error { remoteIP = flags.String("remote-ip", "", "the remote Tailscale IP") ttyName = flags.String("tty-name", "", "the tty name (pts/3)") hasTTY = flags.Bool("has-tty", false, "is the output attached to a tty") - cmdName = flags.String("cmd", "", "the cmd to launch") + cmdName = flags.String("cmd", "", "the cmd to launch (ignored in sftp mode)") + sftpMode = flags.Bool("sftp", false, "run sftp server (cmd is ignored)") ) if err := flags.Parse(args); err != nil { return err @@ -138,6 +178,15 @@ func beIncubator(args []string) error { os.Exit(1) } } + if *sftpMode { + logf("handling sftp") + + server, err := sftp.NewServer(stdRWC{}) + if err != nil { + return err + } + return server.Serve() + } cmd := exec.Command(*cmdName, cmdArgs...) cmd.Stdin = os.Stdin @@ -165,27 +214,20 @@ func beIncubator(args []string) error { // The caller can wait for the process to exit by calling cmd.Wait(). // // It sets ss.cmd, stdin, stdout, and stderr. -func (ss *sshSession) launchProcess(ctx context.Context) error { - shell := loginShell(ss.conn.localUser.Uid) - var args []string - if rawCmd := ss.RawCommand(); rawCmd != "" { - args = append(args, "-c", rawCmd) - } else { - args = append(args, "-l") // login shell - } +func (ss *sshSession) launchProcess() error { + ss.cmd = ss.newIncubatorCommand() - ci := ss.conn.info - cmd := ss.newIncubatorCommand(ctx, shell, args) + cmd := ss.cmd cmd.Dir = ss.conn.localUser.HomeDir cmd.Env = append(cmd.Env, envForUser(ss.conn.localUser)...) cmd.Env = append(cmd.Env, ss.Environ()...) + + ci := ss.conn.info cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_CLIENT=%s %d %d", ci.src.IP(), ci.src.Port(), ci.dst.Port()), fmt.Sprintf("SSH_CONNECTION=%s %d %s %d", ci.src.IP(), ci.src.Port(), ci.dst.IP(), ci.dst.Port()), ) - ss.cmd = cmd - if ss.agentListener != nil { cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_AUTH_SOCK=%s", ss.agentListener.Addr())) } diff --git a/ssh/tailssh/tailssh.go b/ssh/tailssh/tailssh.go index 468359a68..0f485756a 100644 --- a/ssh/tailssh/tailssh.go +++ b/ssh/tailssh/tailssh.go @@ -221,10 +221,12 @@ func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig { func (srv *server) newConn() (*conn, error) { c := &conn{srv: srv, now: srv.now()} c.Server = &ssh.Server{ - Version: "Tailscale", - Handler: c.handleConnPostSSHAuth, - RequestHandlers: map[string]ssh.RequestHandler{}, - SubsystemHandlers: map[string]ssh.SubsystemHandler{}, + Version: "Tailscale", + Handler: c.handleConnPostSSHAuth, + RequestHandlers: map[string]ssh.RequestHandler{}, + SubsystemHandlers: map[string]ssh.SubsystemHandler{ + "sftp": c.handleConnPostSSHAuth, + }, // Note: the direct-tcpip channel handler and LocalPortForwardingCallback // only adds support for forwarding ports from the local machine. @@ -475,7 +477,7 @@ func (srv *server) fetchPublicKeysURL(url string) ([]string, error) { // handleConnPostSSHAuth runs an SSH session after the SSH-level authentication, // but not necessarily before all the Tailscale-level extra verification has -// completed. +// completed. It also handles SFTP requests. func (c *conn) handleConnPostSSHAuth(s ssh.Session) { sshUser := s.User() action, err := c.resolveTerminalAction(s) @@ -491,6 +493,15 @@ func (c *conn) handleConnPostSSHAuth(s ssh.Session) { return } + // Do this check after auth, but before starting the session. + switch s.Subsystem() { + case "sftp", "": + default: + fmt.Fprintf(s.Stderr(), "Unsupported subsystem %q \r\n", s.Subsystem()) + s.Exit(1) + return + } + ss := c.newSSHSession(s, action) ss.logf("handling new SSH connection from %v (%v) to ssh-user %q", c.info.uprof.LoginName, c.info.src.IP(), sshUser) ss.logf("access granted to %v as ssh-user %q", c.info.uprof.LoginName, sshUser) @@ -813,27 +824,29 @@ func (ss *sshSession) run() { // See https://github.com/tailscale/tailscale/issues/4146 ss.DisablePTYEmulation() - if err := ss.handleSSHAgentForwarding(ss, lu); err != nil { - ss.logf("agent forwarding failed: %v", err) - } else if ss.agentListener != nil { - // TODO(maisem/bradfitz): add a way to close all session resources - defer ss.agentListener.Close() - } - var rec *recording // or nil if disabled - if ss.shouldRecord() { - var err error - rec, err = ss.startNewRecording() - if err != nil { - fmt.Fprintf(ss, "can't start new recording\r\n") - ss.logf("startNewRecording: %v", err) - ss.Exit(1) - return + if ss.Subsystem() != "sftp" { + if err := ss.handleSSHAgentForwarding(ss, lu); err != nil { + ss.logf("agent forwarding failed: %v", err) + } else if ss.agentListener != nil { + // TODO(maisem/bradfitz): add a way to close all session resources + defer ss.agentListener.Close() + } + + if ss.shouldRecord() { + var err error + rec, err = ss.startNewRecording() + if err != nil { + fmt.Fprintf(ss, "can't start new recording\r\n") + ss.logf("startNewRecording: %v", err) + ss.Exit(1) + return + } + defer rec.Close() } - defer rec.Close() } - err := ss.launchProcess(ss.ctx) + err := ss.launchProcess() if err != nil { logf("start failed: %v", err.Error()) ss.Exit(1)