Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use OpenSSH SFTP Server instead of github.com/pkg/sftp (when available) #26

Merged
merged 1 commit into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ SSH flags:
* `-F`, `--ssh-config=FILE`: specify SSH config file used for `ssh -F`
* `--ssh-persist=(true|false)` (default: `true`): enable ControlPersist

SSHFS flags:
* `--sshfs-noempty` (default: `false`): enable sshfs nonempty

SFTP server flags:
* `--driver=DRIVER` (default: `auto`): SFTP server driver. `builtin` (legacy) or `openssh-sftp-server` (robust and secure, recommended).
`openssh-sftp-server` is chosen by default when the OpenSSH SFTP Server binary is detected.
* `--openssh-sftp-server=BINARY`: OpenSSH SFTP Server binary.
Automatically detected when installed in well-known locations such as `/usr/libexec/sftp-server`.

### Subcommand: `help`
Shows help

22 changes: 17 additions & 5 deletions cmd/sshocker/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ var (
Usage: "enable sshfs nonempty",
Value: false,
},
&cli.StringFlag{
Name: "driver",
Usage: "SFTP server driver. \"builtin\" (legacy) or \"openssh-sftp-server\" (robust and secure, recommended), automatically chosen by default",
Value: "auto",
},
&cli.StringFlag{
Name: "openssh-sftp-server",
Usage: "OpenSSH SFTP Server binary, automatically chosen by default",
Value: "",
},
}
runCommand = &cli.Command{
Name: "run",
Expand Down Expand Up @@ -85,11 +95,13 @@ func runAction(clicontext *cli.Context) error {
sshfsAdditionalArgs = append(sshfsAdditionalArgs, "-o", "nonempty")
}
x := &sshocker.Sshocker{
SSHConfig: sshConfig,
Host: host,
Port: port,
Command: clicontext.Args().Tail(),
SSHFSAdditionalArgs: sshfsAdditionalArgs,
SSHConfig: sshConfig,
Host: host,
Port: port,
Command: clicontext.Args().Tail(),
SSHFSAdditionalArgs: sshfsAdditionalArgs,
Driver: clicontext.String("driver"),
OpensshSftpServerBinary: clicontext.String("openssh-sftp-server"),
}
if len(x.Command) > 0 && x.Command[0] == "--" {
x.Command = x.Command[1:]
Expand Down
209 changes: 170 additions & 39 deletions pkg/reversesshfs/reversesshfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,34 @@ import (
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/lima-vm/sshocker/pkg/ssh"
"github.com/lima-vm/sshocker/pkg/util"
"github.com/pkg/sftp"
"github.com/sirupsen/logrus"
)

type Driver = string

const (
DriverAuto = Driver("auto") // Default
DriverBuiltin = Driver("builtin") // Legacy. Unrecommended.
DriverOpensshSftpServer = Driver("openssh-sftp-server") // More robust and secure. Recommended.
)

type ReverseSSHFS struct {
*ssh.SSHConfig
LocalPath string
Host string
Port int
RemotePath string
Readonly bool
sshCmd *exec.Cmd
SSHFSAdditionalArgs []string
Driver Driver
OpensshSftpServerBinary string // used only when Driver == DriverOpensshSftpServer
LocalPath string
Host string
Port int
RemotePath string
Readonly bool
sshCmd *exec.Cmd
opensshSftpServerCmd *exec.Cmd
SSHFSAdditionalArgs []string
}

func (rsf *ReverseSSHFS) Prepare() error {
Expand All @@ -48,6 +60,55 @@ func (rsf *ReverseSSHFS) Prepare() error {
return nil
}

func DetectOpensshSftpServerBinary() string {
homebrewSSHD := []string{
"/usr/local/sbin/sshd",
"/opt/homebrew/sbin/sshd",
}
for _, f := range homebrewSSHD {
// sshd is like "/usr/local/Cellar/openssh/8.9p1/sbin/sshd"
sshd, err := filepath.EvalSymlinks(f)
if err != nil {
continue
}
// local is like "/usr/local/Cellar/openssh"
local := filepath.Dir(filepath.Dir(sshd))
// sftpServer is like "/usr/local/Cellar/openssh/8.9p1/libexec/sftp-server"
sftpServer := filepath.Join(local, "libexec", "sftp-server")
if exe, err := exec.LookPath(sftpServer); err == nil {
return exe
}
}
candidates := []string{
"/usr/libexec/sftp-server", // macOS, OpenWrt
"/usr/libexec/openssh/sftp-server", // Fedora
"/usr/lib/sftp-server", // Debian (symlink to openssh/sftp-server)
"/usr/lib/openssh/sftp-server", // Debian
"/usr/lib/ssh/sftp-server", // Alpine
}
for _, cand := range candidates {
if exe, err := exec.LookPath(cand); err == nil {
return exe
}
}
return ""
}

func DetectDriver(explicitOpensshSftpServerBinary string) (Driver, string, error) {
if explicitOpensshSftpServerBinary != "" {
exe, err := exec.LookPath(explicitOpensshSftpServerBinary)
if err != nil {
return "", "", err
}
return DriverOpensshSftpServer, exe, nil
}
exe := DetectOpensshSftpServerBinary()
if exe != "" {
return DriverOpensshSftpServer, exe, nil
}
return DriverBuiltin, "", nil
}

func (rsf *ReverseSSHFS) Start() error {
sshBinary := rsf.SSHConfig.Binary()
sshArgs := rsf.SSHConfig.Args()
Expand All @@ -68,44 +129,103 @@ func (rsf *ReverseSSHFS) Start() error {
sshArgs = append(sshArgs, rsf.SSHFSAdditionalArgs...)
rsf.sshCmd = exec.Command(sshBinary, sshArgs...)
rsf.sshCmd.Stderr = os.Stderr
stdinPipe, err := rsf.sshCmd.StdinPipe()
if err != nil {
return err
}
stdoutPipe, err := rsf.sshCmd.StdoutPipe()
if err != nil {
return err
}
stdio := &util.RWC{
ReadCloser: stdoutPipe,
WriteCloser: stdinPipe,
}
var sftpOpts []sftp.ServerOption
if rsf.Readonly {
sftpOpts = append(sftpOpts, sftp.ReadOnly())
driver := rsf.Driver
opensshSftpServerBinary := rsf.OpensshSftpServerBinary
switch driver {
case DriverBuiltin, DriverOpensshSftpServer:
// NOP
case "", DriverAuto:
var err error
driver, opensshSftpServerBinary, err = DetectDriver(opensshSftpServerBinary)
if err != nil {
return fmt.Errorf("failed to choose driver automatically: %w", err)
}
logrus.Debugf("Chosen driver %q", driver)
default:
return fmt.Errorf("unknown driver %q", driver)
}
// NOTE: sftp.NewServer doesn't support specifying the root.
// https://github.com/pkg/sftp/pull/238
//
// TODO: use sftp.NewRequestServer with custom handlers to mitigate potential vulnerabilities.
server, err := sftp.NewServer(stdio, sftpOpts...)
if err != nil {
return err
var builtinSftpServer *sftp.Server
switch driver {
case DriverBuiltin:
stdinPipe, err := rsf.sshCmd.StdinPipe()
if err != nil {
return err
}
stdoutPipe, err := rsf.sshCmd.StdoutPipe()
if err != nil {
return err
}
stdio := &util.RWC{
ReadCloser: stdoutPipe,
WriteCloser: stdinPipe,
}
var sftpOpts []sftp.ServerOption
if rsf.Readonly {
sftpOpts = append(sftpOpts, sftp.ReadOnly())
}
// NOTE: sftp.NewServer doesn't support specifying the root.
// https://github.com/pkg/sftp/pull/238
//
// TODO: use sftp.NewRequestServer with custom handlers to mitigate potential vulnerabilities.
builtinSftpServer, err = sftp.NewServer(stdio, sftpOpts...)
if err != nil {
return err
}
case DriverOpensshSftpServer:
if opensshSftpServerBinary == "" {
opensshSftpServerBinary = DetectOpensshSftpServerBinary()
if opensshSftpServerBinary == "" {
return errors.New("no openssh sftp-server found")
}
}
logrus.Debugf("Using OpenSSH SFTP Server %q", opensshSftpServerBinary)
sftpServerArgs := []string{
// `-e` available since OpenSSH 5.4p1 (2010) https://github.com/openssh/openssh-portable/commit/7bee06ab
"-e",
// `-d` available since OpenSSH 6.2p1 (2013) https://github.com/openssh/openssh-portable/commit/502ab0ef
// NOTE: `-d` just chdirs the sftp server process to the specified directory.
// This is expected to be used in conjunction with chroot (in future), however, macOS does not support unprivileged chroot.
"-d", strings.ReplaceAll(rsf.LocalPath, "%", "%%"),
}
if rsf.Readonly {
// `-R` available since OpenSSH 5.4p1 (2010) https://github.com/openssh/openssh-portable/commit/db7bf825
sftpServerArgs = append(sftpServerArgs, "-R")
}
rsf.opensshSftpServerCmd = exec.Command(opensshSftpServerBinary, sftpServerArgs...)
rsf.opensshSftpServerCmd.Stderr = os.Stderr
var err error
rsf.opensshSftpServerCmd.Stdin, err = rsf.sshCmd.StdoutPipe()
if err != nil {
return err
}
rsf.sshCmd.Stdin, err = rsf.opensshSftpServerCmd.StdoutPipe()
if err != nil {
return err
}
}
logrus.Debugf("executing ssh for remote sshfs: %s %v", rsf.sshCmd.Path, rsf.sshCmd.Args)
if err := rsf.sshCmd.Start(); err != nil {
return err
}
logrus.Debugf("starting sftp server for %v", rsf.LocalPath)
go func() {
if srvErr := server.Serve(); srvErr != nil {
if errors.Is(srvErr, io.EOF) {
logrus.WithError(srvErr).Debugf("sftp server for %v exited with EOF (negligible)", rsf.LocalPath)
} else {
logrus.WithError(srvErr).Errorf("sftp server for %v exited", rsf.LocalPath)
switch driver {
case DriverBuiltin:
go func() {
if srvErr := builtinSftpServer.Serve(); srvErr != nil {
if errors.Is(srvErr, io.EOF) {
logrus.WithError(srvErr).Debugf("sftp server for %v exited with EOF (negligible)", rsf.LocalPath)
} else {
logrus.WithError(srvErr).Errorf("sftp server for %v exited", rsf.LocalPath)
}
}
}()
case DriverOpensshSftpServer:
logrus.Debugf("executing OpenSSH SFTP Server: %s %v", rsf.opensshSftpServerCmd.Path, rsf.opensshSftpServerCmd.Args)
if err := rsf.opensshSftpServerCmd.Start(); err != nil {
return err
}
}()
}
logrus.Debugf("waiting for remote ready")
if err := rsf.waitForRemoteReady(); err != nil {
// not a fatal error
logrus.WithError(err).Warnf("failed to confirm whether %v [remote] is successfully mounted", rsf.RemotePath)
Expand Down Expand Up @@ -158,9 +278,20 @@ done
}

func (rsf *ReverseSSHFS) Close() error {
logrus.Debugf("killing ssh server for remote sshfs: %s %v", rsf.sshCmd.Path, rsf.sshCmd.Args)
if err := rsf.sshCmd.Process.Kill(); err != nil {
return err
logrus.Debugf("killing processes for remote sshfs: %s %v", rsf.sshCmd.Path, rsf.sshCmd.Args)
var errors []error
if rsf.sshCmd != nil && rsf.sshCmd.Process != nil {
if err := rsf.sshCmd.Process.Kill(); err != nil {
errors = append(errors, err)
}
}
if rsf.opensshSftpServerCmd != nil && rsf.opensshSftpServerCmd.Process != nil {
if err := rsf.opensshSftpServerCmd.Process.Kill(); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
return fmt.Errorf("%v", errors)
}
return nil
}
30 changes: 17 additions & 13 deletions pkg/sshocker/sshocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import (

type Sshocker struct {
*ssh.SSHConfig
Host string // Required
Port int // Required
Command []string // Optional
Mounts []mount.Mount
LForwards []string
SSHFSAdditionalArgs []string
Host string // Required
Port int // Required
Command []string // Optional
Mounts []mount.Mount
LForwards []string
SSHFSAdditionalArgs []string
Driver reversesshfs.Driver
OpensshSftpServerBinary string
}

func (x *Sshocker) Run() error {
Expand Down Expand Up @@ -48,13 +50,15 @@ func (x *Sshocker) Run() error {
switch m.Type {
case mount.MountTypeReverseSSHFS:
rsf := &reversesshfs.ReverseSSHFS{
SSHConfig: x.SSHConfig,
LocalPath: m.Source,
Host: x.Host,
Port: x.Port,
RemotePath: m.Destination,
Readonly: m.Readonly,
SSHFSAdditionalArgs: x.SSHFSAdditionalArgs,
Driver: x.Driver,
OpensshSftpServerBinary: x.OpensshSftpServerBinary,
SSHConfig: x.SSHConfig,
LocalPath: m.Source,
Host: x.Host,
Port: x.Port,
RemotePath: m.Destination,
Readonly: m.Readonly,
SSHFSAdditionalArgs: x.SSHFSAdditionalArgs,
}
if err := rsf.Prepare(); err != nil {
return fmt.Errorf("failed to prepare mounting %q (local) onto %q (remote): %w", rsf.LocalPath, rsf.RemotePath, err)
Expand Down