@@ -7,8 +7,10 @@ import (
77 "io"
88 "net"
99 "strconv"
10+ "strings"
1011 "sync"
1112 "sync/atomic"
13+ "syscall"
1214 "time"
1315
1416 "golang.org/x/crypto/ssh"
@@ -278,20 +280,62 @@ func handleConnectionError(ctx context.Context, err error) {
278280 return
279281 }
280282
281- authErr , isAuthErr := errors.AsType [ * ssh. ServerAuthError ] (err )
282- if ! isAuthErr {
283- logger .WithError (err ).Error ( "failed to handshake client" )
283+ if errors .Is (err , syscall . ECONNRESET ) {
284+ // For example, OpenSSH client may send RST during key exchange if the host keys have changed
285+ logger .WithError (err ).Debug ( "connection reset by client" )
284286 return
285287 }
286288
287- for _ , err := range authErr .Errors {
288- if errors .Is (err , ErrUpstreamNotFound ) {
289- logger .Debug ("client requested unknown upstream" )
289+ if errors .Is (err , syscall .ETIMEDOUT ) {
290+ logger .WithError (err ).Debug ("client connection timed out" )
291+ return
292+ }
293+
294+ if authErr , ok := errors.AsType [* ssh.ServerAuthError ](err ); ok {
295+ for _ , err := range authErr .Errors {
296+ if errors .Is (err , ErrUpstreamNotFound ) {
297+ logger .Debug ("client requested unknown upstream" )
298+ return
299+ }
300+ }
301+
302+ logger .WithError (err ).Debug ("client auth failed" )
303+ return
304+ }
305+
306+ if algoErr , ok := errors.AsType [* ssh.AlgorithmNegotiationError ](err ); ok {
307+ logger .WithField ("offered" , algoErr .RequestedAlgorithms ).Debugf ("no common algorithm for %s" , algoErr .What )
308+ return
309+ }
310+
311+ if sshErr := getSSHError (err ); sshErr != nil {
312+ errMsg := sshErr .Error ()
313+
314+ for _ , msg := range [... ]string {
315+ // https://github.com/golang/crypto/blob/982eaa62dfb7273603b97fc1835561450096f3bd/ssh/transport.go#L369
316+ "overflow reading version string" ,
317+ // https://github.com/golang/crypto/blob/982eaa62dfb7273603b97fc1835561450096f3bd/ssh/messages.go#L385
318+ // e.g., "unmarshal error for field Language of type disconnectMsg"
319+ "unmarshal error" ,
320+ // https://github.com/golang/crypto/blob/982eaa62dfb7273603b97fc1835561450096f3bd/ssh/common.go#L382
321+ "unexpected message type" ,
322+ } {
323+ if strings .Contains (errMsg , msg ) {
324+ logger .WithError (err ).Debug ("suspicious client" )
325+ return
326+ }
327+ }
328+
329+ // https://github.com/golang/crypto/blob/982eaa62dfb7273603b97fc1835561450096f3bd/ssh/messages.go#L47
330+ // e.g., "ssh: disconnect, reason 11: disconnected by user"
331+ // Most probably this is also a suspicious client, but may be a legitimate use of SSH_MSG_DISCONNECT
332+ if strings .Contains (errMsg , "disconnect, reason" ) {
333+ logger .WithError (err ).Debug ("client disconnected" )
290334 return
291335 }
292336 }
293337
294- logger .WithError (err ).Debug ( "client auth failed " )
338+ logger .WithError (err ).Error ( "failed to handshake client " )
295339}
296340
297341func connectToUpstream (
@@ -458,3 +502,17 @@ func bridgeChannelRequests(ctx context.Context, dir direction, inReqs <-chan *ss
458502func isClosedError (err error ) bool {
459503 return errors .Is (err , net .ErrClosed ) || errors .Is (err , io .EOF )
460504}
505+
506+ func getSSHError (err error ) error {
507+ for {
508+ if strings .HasPrefix (err .Error (), "ssh: " ) {
509+ return err
510+ }
511+ err = errors .Unwrap (err )
512+ if err == nil {
513+ break
514+ }
515+ }
516+
517+ return nil
518+ }
0 commit comments