@@ -154,13 +154,22 @@ func DefaultPubKeys(loadDotSSH bool) ([]PubKey, error) {
154154 return res , nil
155155}
156156
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+
157165var sshInfo struct {
158166 sync.Once
159167 // aesAccelerated is set to true when AES acceleration is available.
160168 // Available on almost all modern Intel/AMD processors.
161169 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
164173}
165174
166175// 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) {
226235 "StrictHostKeyChecking=no" ,
227236 "UserKnownHostsFile=/dev/null" ,
228237 "NoHostAuthenticationForLocalhost=yes" ,
229- "GSSAPIAuthentication=no" ,
230238 "PreferredAuthentications=publickey" ,
231239 "Compression=no" ,
232240 "BatchMode=yes" ,
@@ -235,11 +243,15 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
235243
236244 sshInfo .Do (func () {
237245 sshInfo .aesAccelerated = detectAESAcceleration ()
238- sshInfo .openSSHVersion = DetectOpenSSHVersion (sshPath )
246+ sshInfo .openSSH = detectOpenSSHInfo (sshPath )
239247 })
240248
249+ if sshInfo .openSSH .GSSAPISupported {
250+ opts = append (opts , "GSSAPIAuthentication=no" )
251+ }
252+
241253 // 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" )) {
243255 // By default, `ssh` choose chacha20-poly1305@openssh.com, even when AES accelerator is available.
244256 // (OpenSSH_8.1p1, macOS 11.6, MacBookPro 2020, Core i7-1068NG7)
245257 //
@@ -321,7 +333,7 @@ func SSHArgsFromOpts(opts []string) []string {
321333}
322334
323335func 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` )
325337 matches := regex .FindSubmatch (version )
326338 if len (matches ) == 3 {
327339 if len (matches [2 ]) == 0 {
@@ -332,6 +344,10 @@ func ParseOpenSSHVersion(version []byte) *semver.Version {
332344 return & semver.Version {}
333345}
334346
347+ func parseOpenSSHGSSAPISupported (version string ) bool {
348+ return ! strings .Contains (version , `Unsupported option "gssapiauthentication"` )
349+ }
350+
335351// sshExecutable beyond path also records size and mtime, in the case of ssh upgrades.
336352type sshExecutable struct {
337353 Path string
@@ -340,14 +356,14 @@ type sshExecutable struct {
340356}
341357
342358var (
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
346362)
347363
348- func DetectOpenSSHVersion (ssh string ) semver. Version {
364+ func detectOpenSSHInfo (ssh string ) openSSHInfo {
349365 var (
350- v semver. Version
366+ info openSSHInfo
351367 exe sshExecutable
352368 stderr bytes.Buffer
353369 )
@@ -357,25 +373,33 @@ func DetectOpenSSHVersion(ssh string) semver.Version {
357373 } else {
358374 st , _ := os .Stat (path )
359375 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
365381 }
366382 }
367- cmd := exec .Command (path , "-V" )
383+ // -V should be last
384+ cmd := exec .Command (path , "-o" , "GSSAPIAuthentication=no" , "-V" )
368385 cmd .Stderr = & stderr
369386 if err := cmd .Run (); err != nil {
370387 logrus .Warnf ("failed to run %v: stderr=%q" , cmd .Args , stderr .String ())
371388 } 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 ()
377397 }
378- return v
398+ return info
399+ }
400+
401+ func DetectOpenSSHVersion (ssh string ) semver.Version {
402+ return detectOpenSSHInfo (ssh ).Version
379403}
380404
381405// detectValidPublicKey returns whether content represent a public key.
0 commit comments