Skip to content

Commit

Permalink
feat: handle ssh error messages during in channel auth like py version
Browse files Browse the repository at this point in the history
  • Loading branch information
carlmontanari committed Aug 23, 2023
1 parent 33282f9 commit 3160566
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 4 deletions.
86 changes: 83 additions & 3 deletions channel/auth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package channel

import (
"bytes"
"fmt"
"regexp"
"sync"
Expand All @@ -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 {
Expand All @@ -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

Expand All @@ -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}
}
Expand Down Expand Up @@ -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
}
11 changes: 10 additions & 1 deletion channel/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions util/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3160566

Please sign in to comment.