Skip to content

Commit 0279010

Browse files
authored
Merge pull request #3783 from muchzill4/properly-support-multi-word-SSH-env
fix: Properly support multi-word executables set via `SSH` environment variable
2 parents d1b3766 + 18cb183 commit 0279010

File tree

6 files changed

+68
-37
lines changed

6 files changed

+68
-37
lines changed

cmd/limactl/copy.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ 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+
sshExe, err := sshutil.NewSSHExe()
88+
if err != nil {
89+
return err
90+
}
91+
legacySSH := sshutil.DetectOpenSSHVersion(sshExe).LessThan(*semver.New("8.0.0"))
8892
for _, arg := range args {
8993
if runtime.GOOS == "windows" {
9094
if filepath.IsAbs(arg) {
@@ -135,14 +139,22 @@ func copyAction(cmd *cobra.Command, args []string) error {
135139
// arguments such as ControlPath. This is preferred as we can multiplex
136140
// sessions without re-authenticating (MaxSessions permitting).
137141
for _, inst := range instances {
138-
sshOpts, err = sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, false, false, false)
142+
sshExe, err := sshutil.NewSSHExe()
143+
if err != nil {
144+
return err
145+
}
146+
sshOpts, err = sshutil.SSHOpts(sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
139147
if err != nil {
140148
return err
141149
}
142150
}
143151
} else {
144152
// Copying among multiple hosts; we can't pass in host-specific options.
145-
sshOpts, err = sshutil.CommonOpts("ssh", false)
153+
sshExe, err := sshutil.NewSSHExe()
154+
if err != nil {
155+
return err
156+
}
157+
sshOpts, err = sshutil.CommonOpts(sshExe, false)
146158
if err != nil {
147159
return err
148160
}

cmd/limactl/shell.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,13 @@ func shellAction(cmd *cobra.Command, args []string) error {
214214
)
215215
}
216216

217-
arg0, arg0Args, err := sshutil.SSHArguments()
217+
sshExe, err := sshutil.NewSSHExe()
218218
if err != nil {
219219
return err
220220
}
221221

222222
sshOpts, err := sshutil.SSHOpts(
223-
arg0,
223+
sshExe,
224224
inst.Dir,
225225
*inst.Config.User.Name,
226226
*inst.Config.SSH.LoadDotSSHPubKeys,
@@ -230,7 +230,8 @@ func shellAction(cmd *cobra.Command, args []string) error {
230230
if err != nil {
231231
return err
232232
}
233-
sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
233+
sshArgs := append([]string{}, sshExe.Args...)
234+
sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)
234235
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
235236
// required for showing the shell prompt: https://stackoverflow.com/a/626574
236237
sshArgs = append(sshArgs, "-t")
@@ -242,7 +243,7 @@ func shellAction(cmd *cobra.Command, args []string) error {
242243
logLevel := "ERROR"
243244
// For versions older than OpenSSH 8.9p, LogLevel=QUIET was needed to
244245
// avoid the "Shared connection to 127.0.0.1 closed." message with -t.
245-
olderSSH := sshutil.DetectOpenSSHVersion(arg0).LessThan(*semver.New("8.9.0"))
246+
olderSSH := sshutil.DetectOpenSSHVersion(sshExe).LessThan(*semver.New("8.9.0"))
246247
if olderSSH {
247248
logLevel = "QUIET"
248249
}
@@ -253,7 +254,7 @@ func shellAction(cmd *cobra.Command, args []string) error {
253254
"--",
254255
script,
255256
}...)
256-
sshCmd := exec.Command(arg0, append(arg0Args, sshArgs...)...)
257+
sshCmd := exec.Command(sshExe.Exe, sshArgs...)
257258
sshCmd.Stdin = os.Stdin
258259
sshCmd.Stdout = os.Stdout
259260
sshCmd.Stderr = os.Stderr

cmd/limactl/show-ssh.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@ func showSSHAction(cmd *cobra.Command, args []string) error {
9292
}
9393
logrus.Warnf("`limactl show-ssh` is deprecated. Instead, use `ssh -F %s %s`.",
9494
filepath.Join(inst.Dir, filenames.SSHConfig), inst.Hostname)
95+
sshExe, err := sshutil.NewSSHExe()
96+
if err != nil {
97+
return err
98+
}
9599
opts, err := sshutil.SSHOpts(
96-
"ssh",
100+
sshExe,
97101
inst.Dir,
98102
*inst.Config.User.Name,
99103
*inst.Config.SSH.LoadDotSSHPubKeys,

cmd/limactl/tunnel.go

Lines changed: 5 additions & 4 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.NewSSHExe()
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,
@@ -98,7 +98,8 @@ func tunnelAction(cmd *cobra.Command, args []string) error {
9898
if err != nil {
9999
return err
100100
}
101-
sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
101+
sshArgs := append([]string{}, sshExe.Args...)
102+
sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)
102103
sshArgs = append(sshArgs, []string{
103104
"-q", // quiet
104105
"-f", // background
@@ -107,7 +108,7 @@ func tunnelAction(cmd *cobra.Command, args []string) error {
107108
"-p", strconv.Itoa(inst.SSHLocalPort),
108109
inst.SSHAddress,
109110
}...)
110-
sshCmd := exec.Command(arg0, append(arg0Args, sshArgs...)...)
111+
sshCmd := exec.Command(sshExe.Exe, sshArgs...)
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,12 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
155155
return nil, err
156156
}
157157

158+
sshExe, err := sshutil.NewSSHExe()
159+
if err != nil {
160+
return nil, err
161+
}
158162
sshOpts, err := sshutil.SSHOpts(
159-
"ssh",
163+
sshExe,
160164
inst.Dir,
161165
*inst.Config.User.Name,
162166
*inst.Config.SSH.LoadDotSSHPubKeys,

pkg/sshutil/sshutil.go

Lines changed: 32 additions & 23 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+
Exe string
40+
Args []string
41+
}
42+
43+
func NewSSHExe() (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.Exe = 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.Exe = 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,27 +368,29 @@ 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)
371-
if err != nil {
372-
logrus.Warnf("failed to find ssh executable: %v", err)
373-
} else {
374-
st, _ := os.Stat(path)
375-
exe = sshExecutable{Path: path, Size: st.Size(), ModTime: st.ModTime()}
377+
// Note: For SSH wrappers like "kitten ssh", os.Stat will check the wrapper
378+
// executable (kitten) instead of the underlying ssh binary. This means
379+
// cache invalidation won't work properly - ssh upgrades won't be detected
380+
// since kitten's size/mtime won't change. This is probably acceptable.
381+
if st, err := os.Stat(sshExe.Exe); err == nil {
382+
exe = sshExecutable{Path: sshExe.Exe, Size: st.Size(), ModTime: st.ModTime()}
376383
openSSHInfosRW.RLock()
377384
info := openSSHInfos[exe]
378385
openSSHInfosRW.RUnlock()
379386
if info != nil {
380387
return *info
381388
}
382389
}
390+
sshArgs := append([]string{}, sshExe.Args...)
383391
// -V should be last
384-
cmd := exec.Command(path, "-o", "GSSAPIAuthentication=no", "-V")
392+
sshArgs = append(sshArgs, "-o", "GSSAPIAuthentication=no", "-V")
393+
cmd := exec.Command(sshExe.Exe, sshArgs...)
385394
cmd.Stderr = &stderr
386395
if err := cmd.Run(); err != nil {
387396
logrus.Warnf("failed to run %v: stderr=%q", cmd.Args, stderr.String())
@@ -398,8 +407,8 @@ func detectOpenSSHInfo(ssh string) openSSHInfo {
398407
return info
399408
}
400409

401-
func DetectOpenSSHVersion(ssh string) semver.Version {
402-
return detectOpenSSHInfo(ssh).Version
410+
func DetectOpenSSHVersion(sshExe SSHExe) semver.Version {
411+
return detectOpenSSHInfo(sshExe).Version
403412
}
404413

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

0 commit comments

Comments
 (0)