@@ -154,13 +154,22 @@ func DefaultPubKeys(loadDotSSH bool) ([]PubKey, error) {
154
154
return res , nil
155
155
}
156
156
157
+ type openSSHInfo struct {
158
+ // Version is set to the version of OpenSSH, or semver.New("0.0.0") if the version cannot be determined.
159
+ Version semver.Version
160
+
161
+ // Some distributions omit this feature by default, for example, Alpine, NixOS.
162
+ GSSAPISupported bool
163
+ }
164
+
157
165
var sshInfo struct {
158
166
sync.Once
159
167
// aesAccelerated is set to true when AES acceleration is available.
160
168
// Available on almost all modern Intel/AMD processors.
161
169
aesAccelerated bool
162
- // openSSHVersion is set to the version of OpenSSH, or semver.New("0.0.0") if the version cannot be determined.
163
- openSSHVersion semver.Version
170
+
171
+ // OpenSSH executable information for the version and supported options.
172
+ openSSH openSSHInfo
164
173
}
165
174
166
175
// CommonOpts returns ssh option key-value pairs like {"IdentityFile=/path/to/id_foo"}.
@@ -226,7 +235,6 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
226
235
"StrictHostKeyChecking=no" ,
227
236
"UserKnownHostsFile=/dev/null" ,
228
237
"NoHostAuthenticationForLocalhost=yes" ,
229
- "GSSAPIAuthentication=no" ,
230
238
"PreferredAuthentications=publickey" ,
231
239
"Compression=no" ,
232
240
"BatchMode=yes" ,
@@ -235,11 +243,15 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
235
243
236
244
sshInfo .Do (func () {
237
245
sshInfo .aesAccelerated = detectAESAcceleration ()
238
- sshInfo .openSSHVersion = DetectOpenSSHVersion (sshPath )
246
+ sshInfo .openSSH = detectOpenSSHInfo (sshPath )
239
247
})
240
248
249
+ if sshInfo .openSSH .GSSAPISupported {
250
+ opts = append (opts , "GSSAPIAuthentication=no" )
251
+ }
252
+
241
253
// Only OpenSSH version 8.1 and later support adding ciphers to the front of the default set
242
- if ! sshInfo .openSSHVersion .LessThan (* semver .New ("8.1.0" )) {
254
+ if ! sshInfo .openSSH . Version .LessThan (* semver .New ("8.1.0" )) {
243
255
// By default, `ssh` choose [email protected] , even when AES accelerator is available.
244
256
// (OpenSSH_8.1p1, macOS 11.6, MacBookPro 2020, Core i7-1068NG7)
245
257
//
@@ -321,7 +333,7 @@ func SSHArgsFromOpts(opts []string) []string {
321
333
}
322
334
323
335
func ParseOpenSSHVersion (version []byte ) * semver.Version {
324
- regex := regexp .MustCompile (`^OpenSSH_(\d+\.\d+)(?:p(\d+))?\b` )
336
+ regex := regexp .MustCompile (`(?m) ^OpenSSH_(\d+\.\d+)(?:p(\d+))?\b` )
325
337
matches := regex .FindSubmatch (version )
326
338
if len (matches ) == 3 {
327
339
if len (matches [2 ]) == 0 {
@@ -332,6 +344,10 @@ func ParseOpenSSHVersion(version []byte) *semver.Version {
332
344
return & semver.Version {}
333
345
}
334
346
347
+ func parseOpenSSHGSSAPISupported (version string ) bool {
348
+ return ! strings .Contains (version , `Unsupported option "gssapiauthentication"` )
349
+ }
350
+
335
351
// sshExecutable beyond path also records size and mtime, in the case of ssh upgrades.
336
352
type sshExecutable struct {
337
353
Path string
@@ -340,14 +356,14 @@ type sshExecutable struct {
340
356
}
341
357
342
358
var (
343
- // sshVersions caches the parsed version of each ssh executable, if it is needed again.
344
- sshVersions = map [sshExecutable ]* semver. Version {}
345
- sshVersionsRW sync.RWMutex
359
+ // openSSHInfos caches the parsed version and supported options of each ssh executable, if it is needed again.
360
+ openSSHInfos = map [sshExecutable ]* openSSHInfo {}
361
+ openSSHInfosRW sync.RWMutex
346
362
)
347
363
348
- func DetectOpenSSHVersion (ssh string ) semver. Version {
364
+ func detectOpenSSHInfo (ssh string ) openSSHInfo {
349
365
var (
350
- v semver. Version
366
+ info openSSHInfo
351
367
exe sshExecutable
352
368
stderr bytes.Buffer
353
369
)
@@ -357,25 +373,33 @@ func DetectOpenSSHVersion(ssh string) semver.Version {
357
373
} else {
358
374
st , _ := os .Stat (path )
359
375
exe = sshExecutable {Path : path , Size : st .Size (), ModTime : st .ModTime ()}
360
- sshVersionsRW .RLock ()
361
- ver := sshVersions [exe ]
362
- sshVersionsRW .RUnlock ()
363
- if ver != nil {
364
- return * ver
376
+ openSSHInfosRW .RLock ()
377
+ info := openSSHInfos [exe ]
378
+ openSSHInfosRW .RUnlock ()
379
+ if info != nil {
380
+ return * info
365
381
}
366
382
}
367
- cmd := exec .Command (path , "-V" )
383
+ // -V should be last
384
+ cmd := exec .Command (path , "-o" , "GSSAPIAuthentication=no" , "-V" )
368
385
cmd .Stderr = & stderr
369
386
if err := cmd .Run (); err != nil {
370
387
logrus .Warnf ("failed to run %v: stderr=%q" , cmd .Args , stderr .String ())
371
388
} else {
372
- v = * ParseOpenSSHVersion (stderr .Bytes ())
373
- logrus .Debugf ("OpenSSH version %s detected" , v )
374
- sshVersionsRW .Lock ()
375
- sshVersions [exe ] = & v
376
- sshVersionsRW .Unlock ()
389
+ info = openSSHInfo {
390
+ Version : * ParseOpenSSHVersion (stderr .Bytes ()),
391
+ GSSAPISupported : parseOpenSSHGSSAPISupported (stderr .String ()),
392
+ }
393
+ logrus .Debugf ("OpenSSH version %s detected, is GSSAPI supported: %t" , info .Version , info .GSSAPISupported )
394
+ openSSHInfosRW .Lock ()
395
+ openSSHInfos [exe ] = & info
396
+ openSSHInfosRW .Unlock ()
377
397
}
378
- return v
398
+ return info
399
+ }
400
+
401
+ func DetectOpenSSHVersion (ssh string ) semver.Version {
402
+ return detectOpenSSHInfo (ssh ).Version
379
403
}
380
404
381
405
// detectValidPublicKey returns whether content represent a public key.
0 commit comments