Skip to content

Commit 274d3d9

Browse files
authored
Merge pull request #3128 from afbjorklund/ssh-version-cache
Cache the OpenSSH version if it is needed again
2 parents 3a59746 + c735660 commit 274d3d9

File tree

7 files changed

+52
-16
lines changed

7 files changed

+52
-16
lines changed

cmd/limactl/copy.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ func copyAction(cmd *cobra.Command, args []string) error {
7474
if recursive {
7575
scpFlags = append(scpFlags, "-r")
7676
}
77-
legacySSH := sshutil.DetectOpenSSHVersion().LessThan(*semver.New("8.0.0"))
77+
// this assumes that ssh and scp come from the same place, but scp has no -V
78+
legacySSH := sshutil.DetectOpenSSHVersion("ssh").LessThan(*semver.New("8.0.0"))
7879
for _, arg := range args {
7980
path := strings.Split(arg, ":")
8081
switch len(path) {
@@ -115,14 +116,14 @@ func copyAction(cmd *cobra.Command, args []string) error {
115116
// arguments such as ControlPath. This is preferred as we can multiplex
116117
// sessions without re-authenticating (MaxSessions permitting).
117118
for _, inst := range instances {
118-
sshOpts, err = sshutil.SSHOpts(inst.Dir, *inst.Config.User.Name, false, false, false, false)
119+
sshOpts, err = sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, false, false, false)
119120
if err != nil {
120121
return err
121122
}
122123
}
123124
} else {
124125
// Copying among multiple hosts; we can't pass in host-specific options.
125-
sshOpts, err = sshutil.CommonOpts(false)
126+
sshOpts, err = sshutil.CommonOpts("ssh", false)
126127
if err != nil {
127128
return err
128129
}

cmd/limactl/shell.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ func shellAction(cmd *cobra.Command, args []string) error {
143143
}
144144

145145
sshOpts, err := sshutil.SSHOpts(
146+
arg0,
146147
inst.Dir,
147148
*inst.Config.User.Name,
148149
*inst.Config.SSH.LoadDotSSHPubKeys,
@@ -164,7 +165,7 @@ func shellAction(cmd *cobra.Command, args []string) error {
164165
logLevel := "ERROR"
165166
// For versions older than OpenSSH 8.9p, LogLevel=QUIET was needed to
166167
// avoid the "Shared connection to 127.0.0.1 closed." message with -t.
167-
olderSSH := sshutil.DetectOpenSSHVersion().LessThan(*semver.New("8.9.0"))
168+
olderSSH := sshutil.DetectOpenSSHVersion(arg0).LessThan(*semver.New("8.9.0"))
168169
if olderSSH {
169170
logLevel = "QUIET"
170171
}

cmd/limactl/show-ssh.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func showSSHAction(cmd *cobra.Command, args []string) error {
8989
logrus.Warnf("`limactl show-ssh` is deprecated. Instead, use `ssh -F %s %s`.",
9090
filepath.Join(inst.Dir, filenames.SSHConfig), inst.Hostname)
9191
opts, err := sshutil.SSHOpts(
92+
"ssh",
9293
inst.Dir,
9394
*inst.Config.User.Name,
9495
*inst.Config.SSH.LoadDotSSHPubKeys,
@@ -100,7 +101,7 @@ func showSSHAction(cmd *cobra.Command, args []string) error {
100101
}
101102
opts = append(opts, "Hostname=127.0.0.1")
102103
opts = append(opts, fmt.Sprintf("Port=%d", inst.SSHLocalPort))
103-
return sshutil.Format(w, instName, format, opts)
104+
return sshutil.Format(w, "ssh", instName, format, opts)
104105
}
105106

106107
func showSSHBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {

cmd/limactl/tunnel.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func tunnelAction(cmd *cobra.Command, args []string) error {
8484
}
8585

8686
sshOpts, err := sshutil.SSHOpts(
87+
arg0,
8788
inst.Dir,
8889
*inst.Config.User.Name,
8990
*inst.Config.SSH.LoadDotSSHPubKeys,

pkg/hostagent/hostagent.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
143143
}
144144

145145
sshOpts, err := sshutil.SSHOpts(
146+
"ssh",
146147
inst.Dir,
147148
*inst.Config.User.Name,
148149
*inst.Config.SSH.LoadDotSSHPubKeys,
@@ -152,7 +153,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
152153
if err != nil {
153154
return nil, err
154155
}
155-
if err = writeSSHConfigFile(inst.Name, inst.Dir, inst.SSHAddress, sshLocalPort, sshOpts); err != nil {
156+
if err = writeSSHConfigFile("ssh", inst.Name, inst.Dir, inst.SSHAddress, sshLocalPort, sshOpts); err != nil {
156157
return nil, err
157158
}
158159
sshConfig := &ssh.SSHConfig{
@@ -220,7 +221,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
220221
return a, nil
221222
}
222223

223-
func writeSSHConfigFile(instName, instDir, instSSHAddress string, sshLocalPort int, sshOpts []string) error {
224+
func writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLocalPort int, sshOpts []string) error {
224225
if instDir == "" {
225226
return fmt.Errorf("directory is unknown for the instance %q", instName)
226227
}
@@ -231,7 +232,7 @@ func writeSSHConfigFile(instName, instDir, instSSHAddress string, sshLocalPort i
231232
`); err != nil {
232233
return err
233234
}
234-
if err := sshutil.Format(&b, instName, sshutil.FormatConfig,
235+
if err := sshutil.Format(&b, sshPath, instName, sshutil.FormatConfig,
235236
append(sshOpts,
236237
fmt.Sprintf("Hostname=%s", instSSHAddress),
237238
fmt.Sprintf("Port=%d", sshLocalPort),

pkg/sshutil/format.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ func quoteOption(o string) string {
5858
}
5959

6060
// Format formats the ssh options.
61-
func Format(w io.Writer, instName string, format FormatT, opts []string) error {
61+
func Format(w io.Writer, sshPath, instName string, format FormatT, opts []string) error {
6262
fakeHostname := identifierutil.HostnameFromInstName(instName) // TODO: support customization
6363
switch format {
6464
case FormatCmd:
65-
args := []string{"ssh"}
65+
args := []string{sshPath}
6666
for _, o := range opts {
6767
args = append(args, "-o", quoteOption(o))
6868
}

pkg/sshutil/sshutil.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"runtime"
1515
"strings"
1616
"sync"
17+
"time"
1718

1819
"github.com/coreos/go-semver/semver"
1920
"github.com/lima-vm/lima/pkg/ioutilx"
@@ -156,7 +157,7 @@ var sshInfo struct {
156157
//
157158
// The result always contains the IdentityFile option.
158159
// The result never contains the Port option.
159-
func CommonOpts(useDotSSH bool) ([]string, error) {
160+
func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
160161
configDir, err := dirnames.LimaConfigDir()
161162
if err != nil {
162163
return nil, err
@@ -224,7 +225,7 @@ func CommonOpts(useDotSSH bool) ([]string, error) {
224225

225226
sshInfo.Do(func() {
226227
sshInfo.aesAccelerated = detectAESAcceleration()
227-
sshInfo.openSSHVersion = DetectOpenSSHVersion()
228+
sshInfo.openSSHVersion = DetectOpenSSHVersion(sshPath)
228229
})
229230

230231
// Only OpenSSH version 8.1 and later support adding ciphers to the front of the default set
@@ -253,12 +254,12 @@ func CommonOpts(useDotSSH bool) ([]string, error) {
253254
}
254255

255256
// SSHOpts adds the following options to CommonOptions: User, ControlMaster, ControlPath, ControlPersist.
256-
func SSHOpts(instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) {
257+
func SSHOpts(sshPath, instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) {
257258
controlSock := filepath.Join(instDir, filenames.SSHSock)
258259
if len(controlSock) >= osutil.UnixPathMax {
259260
return nil, fmt.Errorf("socket path %q is too long: >= UNIX_PATH_MAX=%d", controlSock, osutil.UnixPathMax)
260261
}
261-
opts, err := CommonOpts(useDotSSH)
262+
opts, err := CommonOpts(sshPath, useDotSSH)
262263
if err != nil {
263264
return nil, err
264265
}
@@ -307,18 +308,48 @@ func ParseOpenSSHVersion(version []byte) *semver.Version {
307308
return &semver.Version{}
308309
}
309310

310-
func DetectOpenSSHVersion() semver.Version {
311+
// sshExecutable beyond path also records size and mtime, in the case of ssh upgrades.
312+
type sshExecutable struct {
313+
Path string
314+
Size int64
315+
ModTime time.Time
316+
}
317+
318+
var (
319+
// sshVersions caches the parsed version of each ssh executable, if it is needed again.
320+
sshVersions = map[sshExecutable]*semver.Version{}
321+
sshVersionsRW sync.RWMutex
322+
)
323+
324+
func DetectOpenSSHVersion(ssh string) semver.Version {
311325
var (
312326
v semver.Version
327+
exe sshExecutable
313328
stderr bytes.Buffer
314329
)
315-
cmd := exec.Command("ssh", "-V")
330+
path, err := exec.LookPath(ssh)
331+
if err != nil {
332+
logrus.Warnf("failed to find ssh executable: %v", err)
333+
} else {
334+
st, _ := os.Stat(path)
335+
exe = sshExecutable{Path: path, Size: st.Size(), ModTime: st.ModTime()}
336+
sshVersionsRW.RLock()
337+
ver := sshVersions[exe]
338+
sshVersionsRW.RUnlock()
339+
if ver != nil {
340+
return *ver
341+
}
342+
}
343+
cmd := exec.Command(path, "-V")
316344
cmd.Stderr = &stderr
317345
if err := cmd.Run(); err != nil {
318346
logrus.Warnf("failed to run %v: stderr=%q", cmd.Args, stderr.String())
319347
} else {
320348
v = *ParseOpenSSHVersion(stderr.Bytes())
321349
logrus.Debugf("OpenSSH version %s detected", v)
350+
sshVersionsRW.Lock()
351+
sshVersions[exe] = &v
352+
sshVersionsRW.Unlock()
322353
}
323354
return v
324355
}

0 commit comments

Comments
 (0)