@@ -10,22 +10,34 @@ import (
10
10
"os/exec"
11
11
"path/filepath"
12
12
"strconv"
13
+ "strings"
13
14
14
15
"github.com/lima-vm/sshocker/pkg/ssh"
15
16
"github.com/lima-vm/sshocker/pkg/util"
16
17
"github.com/pkg/sftp"
17
18
"github.com/sirupsen/logrus"
18
19
)
19
20
21
+ type Driver = string
22
+
23
+ const (
24
+ DriverAuto = Driver ("auto" ) // Default
25
+ DriverBuiltin = Driver ("builtin" ) // Legacy. Unrecommended.
26
+ DriverOpensshSftpServer = Driver ("openssh-sftp-server" ) // More robust and secure. Recommended.
27
+ )
28
+
20
29
type ReverseSSHFS struct {
21
30
* ssh.SSHConfig
22
- LocalPath string
23
- Host string
24
- Port int
25
- RemotePath string
26
- Readonly bool
27
- sshCmd * exec.Cmd
28
- SSHFSAdditionalArgs []string
31
+ Driver Driver
32
+ OpensshSftpServerBinary string // used only when Driver == DriverOpensshSftpServer
33
+ LocalPath string
34
+ Host string
35
+ Port int
36
+ RemotePath string
37
+ Readonly bool
38
+ sshCmd * exec.Cmd
39
+ opensshSftpServerCmd * exec.Cmd
40
+ SSHFSAdditionalArgs []string
29
41
}
30
42
31
43
func (rsf * ReverseSSHFS ) Prepare () error {
@@ -48,6 +60,55 @@ func (rsf *ReverseSSHFS) Prepare() error {
48
60
return nil
49
61
}
50
62
63
+ func DetectOpensshSftpServerBinary () string {
64
+ homebrewSSHD := []string {
65
+ "/usr/local/sbin/sshd" ,
66
+ "/opt/homebrew/sbin/sshd" ,
67
+ }
68
+ for _ , f := range homebrewSSHD {
69
+ // sshd is like "/usr/local/Cellar/openssh/8.9p1/sbin/sshd"
70
+ sshd , err := filepath .EvalSymlinks (f )
71
+ if err != nil {
72
+ continue
73
+ }
74
+ // local is like "/usr/local/Cellar/openssh"
75
+ local := filepath .Dir (filepath .Dir (sshd ))
76
+ // sftpServer is like "/usr/local/Cellar/openssh/8.9p1/libexec/sftp-server"
77
+ sftpServer := filepath .Join (local , "libexec" , "sftp-server" )
78
+ if exe , err := exec .LookPath (sftpServer ); err == nil {
79
+ return exe
80
+ }
81
+ }
82
+ candidates := []string {
83
+ "/usr/libexec/sftp-server" , // macOS, OpenWrt
84
+ "/usr/libexec/openssh/sftp-server" , // Fedora
85
+ "/usr/lib/sftp-server" , // Debian (symlink to openssh/sftp-server)
86
+ "/usr/lib/openssh/sftp-server" , // Debian
87
+ "/usr/lib/ssh/sftp-server" , // Alpine
88
+ }
89
+ for _ , cand := range candidates {
90
+ if exe , err := exec .LookPath (cand ); err == nil {
91
+ return exe
92
+ }
93
+ }
94
+ return ""
95
+ }
96
+
97
+ func DetectDriver (explicitOpensshSftpServerBinary string ) (Driver , string , error ) {
98
+ if explicitOpensshSftpServerBinary != "" {
99
+ exe , err := exec .LookPath (explicitOpensshSftpServerBinary )
100
+ if err != nil {
101
+ return "" , "" , err
102
+ }
103
+ return DriverOpensshSftpServer , exe , nil
104
+ }
105
+ exe := DetectOpensshSftpServerBinary ()
106
+ if exe != "" {
107
+ return DriverOpensshSftpServer , exe , nil
108
+ }
109
+ return DriverBuiltin , "" , nil
110
+ }
111
+
51
112
func (rsf * ReverseSSHFS ) Start () error {
52
113
sshBinary := rsf .SSHConfig .Binary ()
53
114
sshArgs := rsf .SSHConfig .Args ()
@@ -68,44 +129,103 @@ func (rsf *ReverseSSHFS) Start() error {
68
129
sshArgs = append (sshArgs , rsf .SSHFSAdditionalArgs ... )
69
130
rsf .sshCmd = exec .Command (sshBinary , sshArgs ... )
70
131
rsf .sshCmd .Stderr = os .Stderr
71
- stdinPipe , err := rsf .sshCmd .StdinPipe ()
72
- if err != nil {
73
- return err
74
- }
75
- stdoutPipe , err := rsf .sshCmd .StdoutPipe ()
76
- if err != nil {
77
- return err
78
- }
79
- stdio := & util.RWC {
80
- ReadCloser : stdoutPipe ,
81
- WriteCloser : stdinPipe ,
82
- }
83
- var sftpOpts []sftp.ServerOption
84
- if rsf .Readonly {
85
- sftpOpts = append (sftpOpts , sftp .ReadOnly ())
132
+ driver := rsf .Driver
133
+ opensshSftpServerBinary := rsf .OpensshSftpServerBinary
134
+ switch driver {
135
+ case DriverBuiltin , DriverOpensshSftpServer :
136
+ // NOP
137
+ case "" , DriverAuto :
138
+ var err error
139
+ driver , opensshSftpServerBinary , err = DetectDriver (opensshSftpServerBinary )
140
+ if err != nil {
141
+ return fmt .Errorf ("failed to choose driver automatically: %w" , err )
142
+ }
143
+ logrus .Debugf ("Chosen driver %q" , driver )
144
+ default :
145
+ return fmt .Errorf ("unknown driver %q" , driver )
86
146
}
87
- // NOTE: sftp.NewServer doesn't support specifying the root.
88
- // https://github.com/pkg/sftp/pull/238
89
- //
90
- // TODO: use sftp.NewRequestServer with custom handlers to mitigate potential vulnerabilities.
91
- server , err := sftp .NewServer (stdio , sftpOpts ... )
92
- if err != nil {
93
- return err
147
+ var builtinSftpServer * sftp.Server
148
+ switch driver {
149
+ case DriverBuiltin :
150
+ stdinPipe , err := rsf .sshCmd .StdinPipe ()
151
+ if err != nil {
152
+ return err
153
+ }
154
+ stdoutPipe , err := rsf .sshCmd .StdoutPipe ()
155
+ if err != nil {
156
+ return err
157
+ }
158
+ stdio := & util.RWC {
159
+ ReadCloser : stdoutPipe ,
160
+ WriteCloser : stdinPipe ,
161
+ }
162
+ var sftpOpts []sftp.ServerOption
163
+ if rsf .Readonly {
164
+ sftpOpts = append (sftpOpts , sftp .ReadOnly ())
165
+ }
166
+ // NOTE: sftp.NewServer doesn't support specifying the root.
167
+ // https://github.com/pkg/sftp/pull/238
168
+ //
169
+ // TODO: use sftp.NewRequestServer with custom handlers to mitigate potential vulnerabilities.
170
+ builtinSftpServer , err = sftp .NewServer (stdio , sftpOpts ... )
171
+ if err != nil {
172
+ return err
173
+ }
174
+ case DriverOpensshSftpServer :
175
+ if opensshSftpServerBinary == "" {
176
+ opensshSftpServerBinary = DetectOpensshSftpServerBinary ()
177
+ if opensshSftpServerBinary == "" {
178
+ return errors .New ("no openssh sftp-server found" )
179
+ }
180
+ }
181
+ logrus .Debugf ("Using OpenSSH SFTP Server %q" , opensshSftpServerBinary )
182
+ sftpServerArgs := []string {
183
+ // `-e` available since OpenSSH 5.4p1 (2010) https://github.com/openssh/openssh-portable/commit/7bee06ab
184
+ "-e" ,
185
+ // `-d` available since OpenSSH 6.2p1 (2013) https://github.com/openssh/openssh-portable/commit/502ab0ef
186
+ // NOTE: `-d` just chdirs the sftp server process to the specified directory.
187
+ // This is expected to be used in conjunction with chroot (in future), however, macOS does not support unprivileged chroot.
188
+ "-d" , strings .ReplaceAll (rsf .LocalPath , "%" , "%%" ),
189
+ }
190
+ if rsf .Readonly {
191
+ // `-R` available since OpenSSH 5.4p1 (2010) https://github.com/openssh/openssh-portable/commit/db7bf825
192
+ sftpServerArgs = append (sftpServerArgs , "-R" )
193
+ }
194
+ rsf .opensshSftpServerCmd = exec .Command (opensshSftpServerBinary , sftpServerArgs ... )
195
+ rsf .opensshSftpServerCmd .Stderr = os .Stderr
196
+ var err error
197
+ rsf .opensshSftpServerCmd .Stdin , err = rsf .sshCmd .StdoutPipe ()
198
+ if err != nil {
199
+ return err
200
+ }
201
+ rsf .sshCmd .Stdin , err = rsf .opensshSftpServerCmd .StdoutPipe ()
202
+ if err != nil {
203
+ return err
204
+ }
94
205
}
95
206
logrus .Debugf ("executing ssh for remote sshfs: %s %v" , rsf .sshCmd .Path , rsf .sshCmd .Args )
96
207
if err := rsf .sshCmd .Start (); err != nil {
97
208
return err
98
209
}
99
210
logrus .Debugf ("starting sftp server for %v" , rsf .LocalPath )
100
- go func () {
101
- if srvErr := server .Serve (); srvErr != nil {
102
- if errors .Is (srvErr , io .EOF ) {
103
- logrus .WithError (srvErr ).Debugf ("sftp server for %v exited with EOF (negligible)" , rsf .LocalPath )
104
- } else {
105
- logrus .WithError (srvErr ).Errorf ("sftp server for %v exited" , rsf .LocalPath )
211
+ switch driver {
212
+ case DriverBuiltin :
213
+ go func () {
214
+ if srvErr := builtinSftpServer .Serve (); srvErr != nil {
215
+ if errors .Is (srvErr , io .EOF ) {
216
+ logrus .WithError (srvErr ).Debugf ("sftp server for %v exited with EOF (negligible)" , rsf .LocalPath )
217
+ } else {
218
+ logrus .WithError (srvErr ).Errorf ("sftp server for %v exited" , rsf .LocalPath )
219
+ }
106
220
}
221
+ }()
222
+ case DriverOpensshSftpServer :
223
+ logrus .Debugf ("executing OpenSSH SFTP Server: %s %v" , rsf .opensshSftpServerCmd .Path , rsf .opensshSftpServerCmd .Args )
224
+ if err := rsf .opensshSftpServerCmd .Start (); err != nil {
225
+ return err
107
226
}
108
- }()
227
+ }
228
+ logrus .Debugf ("waiting for remote ready" )
109
229
if err := rsf .waitForRemoteReady (); err != nil {
110
230
// not a fatal error
111
231
logrus .WithError (err ).Warnf ("failed to confirm whether %v [remote] is successfully mounted" , rsf .RemotePath )
@@ -158,9 +278,20 @@ done
158
278
}
159
279
160
280
func (rsf * ReverseSSHFS ) Close () error {
161
- logrus .Debugf ("killing ssh server for remote sshfs: %s %v" , rsf .sshCmd .Path , rsf .sshCmd .Args )
162
- if err := rsf .sshCmd .Process .Kill (); err != nil {
163
- return err
281
+ logrus .Debugf ("killing processes for remote sshfs: %s %v" , rsf .sshCmd .Path , rsf .sshCmd .Args )
282
+ var errors []error
283
+ if rsf .sshCmd != nil && rsf .sshCmd .Process != nil {
284
+ if err := rsf .sshCmd .Process .Kill (); err != nil {
285
+ errors = append (errors , err )
286
+ }
287
+ }
288
+ if rsf .opensshSftpServerCmd != nil && rsf .opensshSftpServerCmd .Process != nil {
289
+ if err := rsf .opensshSftpServerCmd .Process .Kill (); err != nil {
290
+ errors = append (errors , err )
291
+ }
292
+ }
293
+ if len (errors ) > 0 {
294
+ return fmt .Errorf ("%v" , errors )
164
295
}
165
296
return nil
166
297
}
0 commit comments