Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions sftp/sftp.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,15 @@ func (s *Server) Listen() (err error) {
func (s *Server) handleConnection(conn net.Conn) {
// Before use, a handshake must be performed on the incoming net.Conn.
conn.SetDeadline(time.Now().Add(s.sshHandshakeDeadline))
sconn, chans, reqs, err := ssh.NewServerConn(conn, &s.sshConfig)
cfg, auth := s.connConfig()
sconn, chans, reqs, err := ssh.NewServerConn(conn, cfg)
if err != nil {
s.logger.Error(SSHKeyExchangeError, err)
s.logger.Error(SSHKeyExchangeError, &HandshakeError{
Addr: conn.RemoteAddr(),
User: auth.user,
Refused: auth.refused,
Err: err,
})
return
}

Expand Down Expand Up @@ -263,3 +269,65 @@ func (s *Server) handleConnection(conn net.Conn) {
}(requests)
}
}

// HandshakeError is the error passed to Logger.Error with SSHKeyExchangeError.
type HandshakeError struct {
// Addr is the remote address of the TCP connection. Behind a TCP proxy
// it is the proxy's address.
Addr net.Addr
// User is the name the client sent with the last authentication attempt
// the server decided. A "none" attempt that fails with ssh.ErrNoAuth
// offers no credential and does not count. The client chooses the name
// and it is not sanitized.
User string
// Refused is true when the server refused that attempt, including when
// its callback failed for another reason, such as an unreachable
// backend or a client that hung up at a prompt. A partial success is
// not a refusal. A public key signature that fails verification ends
// the handshake before the server decides, so User and Refused then
// describe the last attempt the server decided before it, and Err holds
// the verification error.
Refused bool
// Err is the error returned by the SSH handshake.
Err error
}

// Error returns the text of the underlying handshake error.
func (e *HandshakeError) Error() string {
if e.Err == nil {
return "ssh handshake failed"
}
return e.Err.Error()
}

// Unwrap returns the underlying handshake error.
func (e *HandshakeError) Unwrap() error { return e.Err }

type authState struct {
user string
refused bool
}

// connConfig returns a copy of the SSH config whose AuthLogCallback records,
// for one connection only, the server's verdict on the last attempt. The
// credential callbacks are not wrapped for that because checks run after them
// and a partial success replaces them. The callbacks of a connection run one
// after another on its handshake goroutine, so the state needs no lock.
func (s *Server) connConfig() (*ssh.ServerConfig, *authState) {
st := new(authState)
cfg := s.sshConfig
logAuth := cfg.AuthLogCallback
cfg.AuthLogCallback = func(c ssh.ConnMetadata, method string, err error) {
// A "none" attempt that fails with ErrNoAuth offers no credential.
if method != "none" || !errors.Is(err, ssh.ErrNoAuth) {
// x/crypto treats only an unwrapped PartialSuccessError as a
// partial success.
_, partial := err.(*ssh.PartialSuccessError)
st.user, st.refused = c.User(), err != nil && !partial
}
if logAuth != nil {
logAuth(c, method, err)
}
}
return &cfg, st
}
Loading
Loading