Skip to content

Commit d7a4283

Browse files
committed
fix: properly support multi-word SSH commands
Signed-off-by: Bartek Mucha <muchzill4@gmail.com>
1 parent c1b8496 commit d7a4283

File tree

6 files changed

+42
-30
lines changed

6 files changed

+42
-30
lines changed

cmd/limactl/copy.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func copyAction(cmd *cobra.Command, args []string) error {
8484
scpFlags = append(scpFlags, "-r")
8585
}
8686
// this assumes that ssh and scp come from the same place, but scp has no -V
87-
legacySSH := sshutil.DetectOpenSSHVersion("ssh").LessThan(*semver.New("8.0.0"))
87+
legacySSH := sshutil.DetectOpenSSHVersion(sshutil.SSHExe{Executable: "ssh"}).LessThan(*semver.New("8.0.0"))
8888
for _, arg := range args {
8989
if runtime.GOOS == "windows" {
9090
if filepath.IsAbs(arg) {
@@ -135,14 +135,14 @@ func copyAction(cmd *cobra.Command, args []string) error {
135135
// arguments such as ControlPath. This is preferred as we can multiplex
136136
// sessions without re-authenticating (MaxSessions permitting).
137137
for _, inst := range instances {
138-
sshOpts, err = sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, false, false, false)
138+
sshOpts, err = sshutil.SSHOpts(sshutil.SSHExe{Executable: "ssh"}, inst.Dir, *inst.Config.User.Name, false, false, false, false)
139139
if err != nil {
140140
return err
141141
}
142142
}
143143
} else {
144144
// Copying among multiple hosts; we can't pass in host-specific options.
145-
sshOpts, err = sshutil.CommonOpts("ssh", false)
145+
sshOpts, err = sshutil.CommonOpts(sshutil.SSHExe{Executable: "ssh"}, false)
146146
if err != nil {
147147
return err
148148
}

cmd/limactl/shell.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,13 @@ func shellAction(cmd *cobra.Command, args []string) error {
196196
)
197197
}
198198

199-
arg0, arg0Args, err := sshutil.SSHArguments()
199+
sshExe, err := sshutil.SSHArguments()
200200
if err != nil {
201201
return err
202202
}
203203

204204
sshOpts, err := sshutil.SSHOpts(
205-
arg0,
205+
sshExe,
206206
inst.Dir,
207207
*inst.Config.User.Name,
208208
*inst.Config.SSH.LoadDotSSHPubKeys,
@@ -224,7 +224,7 @@ func shellAction(cmd *cobra.Command, args []string) error {
224224
logLevel := "ERROR"
225225
// For versions older than OpenSSH 8.9p, LogLevel=QUIET was needed to
226226
// avoid the "Shared connection to 127.0.0.1 closed." message with -t.
227-
olderSSH := sshutil.DetectOpenSSHVersion(arg0).LessThan(*semver.New("8.9.0"))
227+
olderSSH := sshutil.DetectOpenSSHVersion(sshExe).LessThan(*semver.New("8.9.0"))
228228
if olderSSH {
229229
logLevel = "QUIET"
230230
}
@@ -235,7 +235,8 @@ func shellAction(cmd *cobra.Command, args []string) error {
235235
"--",
236236
script,
237237
}...)
238-
sshCmd := exec.Command(arg0, append(arg0Args, sshArgs...)...)
238+
allArgs := append(sshExe.Args, sshArgs...)
239+
sshCmd := exec.Command(sshExe.Executable, allArgs...)
239240
sshCmd.Stdin = os.Stdin
240241
sshCmd.Stdout = os.Stdout
241242
sshCmd.Stderr = os.Stderr

cmd/limactl/show-ssh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func showSSHAction(cmd *cobra.Command, args []string) error {
9393
logrus.Warnf("`limactl show-ssh` is deprecated. Instead, use `ssh -F %s %s`.",
9494
filepath.Join(inst.Dir, filenames.SSHConfig), inst.Hostname)
9595
opts, err := sshutil.SSHOpts(
96-
"ssh",
96+
sshutil.SSHExe{Executable: "ssh"},
9797
inst.Dir,
9898
*inst.Config.User.Name,
9999
*inst.Config.SSH.LoadDotSSHPubKeys,

cmd/limactl/tunnel.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ func tunnelAction(cmd *cobra.Command, args []string) error {
8282
}
8383
}
8484

85-
arg0, arg0Args, err := sshutil.SSHArguments()
85+
sshExe, err := sshutil.SSHArguments()
8686
if err != nil {
8787
return err
8888
}
8989

9090
sshOpts, err := sshutil.SSHOpts(
91-
arg0,
91+
sshExe,
9292
inst.Dir,
9393
*inst.Config.User.Name,
9494
*inst.Config.SSH.LoadDotSSHPubKeys,
@@ -107,7 +107,8 @@ func tunnelAction(cmd *cobra.Command, args []string) error {
107107
"-p", strconv.Itoa(inst.SSHLocalPort),
108108
inst.SSHAddress,
109109
}...)
110-
sshCmd := exec.Command(arg0, append(arg0Args, sshArgs...)...)
110+
allArgs := append(sshExe.Args, sshArgs...)
111+
sshCmd := exec.Command(sshExe.Executable, allArgs...)
111112
sshCmd.Stdout = stderr
112113
sshCmd.Stderr = stderr
113114
logrus.Debugf("executing ssh (may take a long)): %+v", sshCmd.Args)

pkg/hostagent/hostagent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
147147
}
148148

149149
sshOpts, err := sshutil.SSHOpts(
150-
"ssh",
150+
sshutil.SSHExe{Executable: "ssh"},
151151
inst.Dir,
152152
*inst.Config.User.Name,
153153
*inst.Config.SSH.LoadDotSSHPubKeys,

pkg/sshutil/sshutil.go

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,36 @@ import (
3535
// in place of the 'ssh' executable.
3636
const EnvShellSSH = "SSH"
3737

38-
func SSHArguments() (arg0 string, arg0Args []string, err error) {
38+
type SSHExe struct {
39+
Executable string
40+
Args []string
41+
}
42+
43+
func SSHArguments() (SSHExe, error) {
44+
var sshExe SSHExe
45+
3946
if sshShell := os.Getenv(EnvShellSSH); sshShell != "" {
4047
sshShellFields, err := shellwords.Parse(sshShell)
4148
switch {
4249
case err != nil:
4350
logrus.WithError(err).Warnf("Failed to split %s variable into shell tokens. "+
4451
"Falling back to 'ssh' command", EnvShellSSH)
4552
case len(sshShellFields) > 0:
46-
arg0 = sshShellFields[0]
53+
sshExe.Executable = sshShellFields[0]
4754
if len(sshShellFields) > 1 {
48-
arg0Args = sshShellFields[1:]
55+
sshExe.Args = sshShellFields[1:]
4956
}
57+
return sshExe, nil
5058
}
5159
}
5260

53-
if arg0 == "" {
54-
arg0, err = exec.LookPath("ssh")
55-
if err != nil {
56-
return "", []string{""}, err
57-
}
61+
executable, err := exec.LookPath("ssh")
62+
if err != nil {
63+
return SSHExe{}, err
5864
}
65+
sshExe.Executable = executable
5966

60-
return arg0, arg0Args, nil
67+
return sshExe, nil
6168
}
6269

6370
type PubKey struct {
@@ -177,7 +184,7 @@ var sshInfo struct {
177184
//
178185
// The result always contains the IdentityFile option.
179186
// The result never contains the Port option.
180-
func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
187+
func CommonOpts(sshExe SSHExe, useDotSSH bool) ([]string, error) {
181188
configDir, err := dirnames.LimaConfigDir()
182189
if err != nil {
183190
return nil, err
@@ -243,7 +250,7 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
243250

244251
sshInfo.Do(func() {
245252
sshInfo.aesAccelerated = detectAESAcceleration()
246-
sshInfo.openSSH = detectOpenSSHInfo(sshPath)
253+
sshInfo.openSSH = detectOpenSSHInfo(sshExe)
247254
})
248255

249256
if sshInfo.openSSH.GSSAPISupported {
@@ -287,12 +294,12 @@ func identityFileEntry(privateKeyPath string) (string, error) {
287294
}
288295

289296
// SSHOpts adds the following options to CommonOptions: User, ControlMaster, ControlPath, ControlPersist.
290-
func SSHOpts(sshPath, instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) {
297+
func SSHOpts(sshExe SSHExe, instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) {
291298
controlSock := filepath.Join(instDir, filenames.SSHSock)
292299
if len(controlSock) >= osutil.UnixPathMax {
293300
return nil, fmt.Errorf("socket path %q is too long: >= UNIX_PATH_MAX=%d", controlSock, osutil.UnixPathMax)
294301
}
295-
opts, err := CommonOpts(sshPath, useDotSSH)
302+
opts, err := CommonOpts(sshExe, useDotSSH)
296303
if err != nil {
297304
return nil, err
298305
}
@@ -361,13 +368,15 @@ var (
361368
openSSHInfosRW sync.RWMutex
362369
)
363370

364-
func detectOpenSSHInfo(ssh string) openSSHInfo {
371+
func detectOpenSSHInfo(sshExe SSHExe) openSSHInfo {
365372
var (
366373
info openSSHInfo
367374
exe sshExecutable
368375
stderr bytes.Buffer
369376
)
370-
path, err := exec.LookPath(ssh)
377+
// TODO: Fix this function to properly handle complex SSH commands like "kitten ssh"
378+
// The current LookPath, os.Stat, and caching logic doesn't work well for multi-word commands
379+
path, err := exec.LookPath(sshExe.Executable)
371380
if err != nil {
372381
logrus.Warnf("failed to find ssh executable: %v", err)
373382
} else {
@@ -381,7 +390,8 @@ func detectOpenSSHInfo(ssh string) openSSHInfo {
381390
}
382391
}
383392
// -V should be last
384-
cmd := exec.Command(path, "-o", "GSSAPIAuthentication=no", "-V")
393+
allArgs := append(sshExe.Args, "-o", "GSSAPIAuthentication=no", "-V")
394+
cmd := exec.Command(sshExe.Executable, allArgs...)
385395
cmd.Stderr = &stderr
386396
if err := cmd.Run(); err != nil {
387397
logrus.Warnf("failed to run %v: stderr=%q", cmd.Args, stderr.String())
@@ -398,8 +408,8 @@ func detectOpenSSHInfo(ssh string) openSSHInfo {
398408
return info
399409
}
400410

401-
func DetectOpenSSHVersion(ssh string) semver.Version {
402-
return detectOpenSSHInfo(ssh).Version
411+
func DetectOpenSSHVersion(sshExe SSHExe) semver.Version {
412+
return detectOpenSSHInfo(sshExe).Version
403413
}
404414

405415
// detectValidPublicKey returns whether content represent a public key.

0 commit comments

Comments
 (0)