From 3160566f987a395624baa74b81d89617ed8c3d94 Mon Sep 17 00:00:00 2001 From: Carl Montanari Date: Wed, 23 Aug 2023 11:55:33 -0700 Subject: [PATCH] feat: handle ssh error messages during in channel auth like py version --- channel/auth.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++-- channel/read.go | 11 ++++++- util/errors.go | 4 +++ 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/channel/auth.go b/channel/auth.go index 05a336a..acc21f9 100644 --- a/channel/auth.go +++ b/channel/auth.go @@ -1,6 +1,7 @@ package channel import ( + "bytes" "fmt" "regexp" "sync" @@ -21,9 +22,17 @@ type authPatterns struct { passphrase *regexp.Regexp } +type sshErrorMessagePatterns struct { + offeredOptions *regexp.Regexp + badConfig *regexp.Regexp +} + var ( authPatternsInstance *authPatterns //nolint:gochecknoglobals authPatternsInstanceOnce sync.Once //nolint:gochecknoglobals + + sshErrorMessagePatternsInstance *sshErrorMessagePatterns //nolint:gochecknoglobals + sshErrorMessagePatternsOnce sync.Once //nolint:gochecknoglobals ) func getAuthPatterns() *authPatterns { @@ -38,6 +47,17 @@ func getAuthPatterns() *authPatterns { return authPatternsInstance } +func getSSHErrorMessagePatterns() *sshErrorMessagePatterns { + sshErrorMessagePatternsOnce.Do(func() { + sshErrorMessagePatternsInstance = &sshErrorMessagePatterns{ + offeredOptions: regexp.MustCompile(`(?im)their offer: ([a-z0-9\-,]*)`), + badConfig: regexp.MustCompile(`(?im)bad configuration option: ([a-z0-9+=,]*)`), + } + }) + + return sshErrorMessagePatternsInstance +} + func (c *Channel) authenticateSSH(p, pp []byte) *result { pCount := 0 @@ -46,15 +66,18 @@ func (c *Channel) authenticateSSH(p, pp []byte) *result { var b []byte for { - nb, err := c.ReadUntilAnyPrompt( - []*regexp.Regexp{c.PromptPattern, c.PasswordPattern, c.PassphrasePattern}, - ) + nb, err := c.Read() if err != nil { return &result{nil, err} } b = append(b, nb...) + err = c.sshMessageHandler(b) + if err != nil { + return &result{nil, err} + } + if c.PromptPattern.Match(b) { return &result{b, nil} } @@ -226,3 +249,60 @@ func (c *Channel) AuthenticateTelnet(u, p []byte) ([]byte, error) { ) } } + +func (c *Channel) sshMessageHandler(b []byte) error { //nolint:gocyclo + var errorMessage string + + normalizedB := bytes.ToLower(b) + + switch { + case bytes.Contains(normalizedB, []byte("host key verification failed")): + errorMessage = "host key verification failed" + case bytes.Contains(normalizedB, []byte("operation timed out")) || + bytes.Contains(normalizedB, []byte("connection timed out")): + errorMessage = "timed out connecting to host" + case bytes.Contains(normalizedB, []byte("no route to host")): + errorMessage = "no route to host" + case bytes.Contains(normalizedB, []byte("no matching")): + switch { + case bytes.Contains(normalizedB, []byte("no matching host key")): + errorMessage = "no matching host key found for host" + case bytes.Contains(normalizedB, []byte("no matching key exchange")): + errorMessage = "no matching key exchange found for host" + case bytes.Contains(normalizedB, []byte("no matching cipher")): + errorMessage = "no matching cipher found for host" + } + + patterns := getSSHErrorMessagePatterns() + + theirOffer := patterns.offeredOptions.FindSubmatch(b) + if len(theirOffer) > 0 { + errorMessage += fmt.Sprintf(", their offer: %s", theirOffer[0]) + } + case bytes.Contains(normalizedB, []byte("bad configuration")): + errorMessage = "bad ssh configuration option(s) for host" + + patterns := getSSHErrorMessagePatterns() + + badOption := patterns.offeredOptions.FindSubmatch(b) + if len(badOption) > 0 { + errorMessage += fmt.Sprintf(", bad configuration option: %s", badOption[0]) + } + case bytes.Contains(normalizedB, []byte("warning: unprotected private key file")): + errorMessage = "permissions for private key are too open" + case bytes.Contains(normalizedB, []byte("could not resolve hostname")): + errorMessage = "could not resolve hostname" + case bytes.Contains(normalizedB, []byte("permission denied")): + errorMessage = "permission denied" + } + + if errorMessage != "" { + return fmt.Errorf( + "%w: encountered error output during in channel ssh authentication, error: '%s'", + util.ErrConnectionError, + errorMessage, + ) + } + + return nil +} diff --git a/channel/read.go b/channel/read.go index 568fb09..166447d 100644 --- a/channel/read.go +++ b/channel/read.go @@ -24,9 +24,18 @@ func (c *Channel) read() { // the underlying transport was closed so just return return } + // we got a transport error, put it into the error channel for processing during - // the next read activity + // the next read activity, log it, sleep and then try again... + c.l.Criticalf( + "encountered error reading from transport during channel read loop. error: %s", err, + ) + c.Errs <- err + + time.Sleep(c.ReadDelay) + + continue } // not 100% this is required, but has existed in scrapli/scrapligo for a long time and am diff --git a/util/errors.go b/util/errors.go index 894a42f..6205c56 100644 --- a/util/errors.go +++ b/util/errors.go @@ -6,6 +6,10 @@ var ( // ErrIgnoredOption is the error returned when attempting to apply an option to a struct that // is not of the expected type. This error should not be exposed to end users. ErrIgnoredOption = errors.New("errIgnoredOption") + // ErrConnectionError is the error returned for non auth related connection failures typically + // encountered during *in channel ssh authentication* -- things like host key verification + // failures and other openssh errors. + ErrConnectionError = errors.New("errConnectionError") // ErrBadOption is returned when a bad value is passed to an option function. ErrBadOption = errors.New("errBadOption") // ErrTimeoutError is returned for any scrapligo timeout issues, meaning socket, transport or