mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-09 00:11:28 +02:00
* [client] Return the context error when the SSH handshake fails on a context deadline The handshake mapped the context deadline onto the socket but returned the raw socket error. Which error surfaces depends on a race between the x/crypto ssh readLoop and kexLoop goroutines: the kexLoop write fails with i/o timeout and closes the conn, and the readLoop then reports use of closed network connection. Callers checking errors.Is(err, context.DeadlineExceeded) never matched, and TestSSHClient_ContextCancellation flaked on the FreeBSD job. Handshake now wraps the context error when the context is done or its deadline has passed. The deadline comparison is needed because the socket deadline and the context timer fire independently, so ctx.Err() can still be nil when the deadline-triggered socket error arrives. * [client] Close the silent test server conn without racing t.Cleanup The accept goroutine registered the conn close via t.Cleanup, which can run after the test's cleanup list has already been drained, leaving the accepted connection open. The goroutine now holds the conn until a cleanup-closed channel signals the end of the test and closes it on the way out. * [client] Bind the SSH handshake to the context instead of a socket deadline Mapping only the context deadline onto the socket left context cancellation unobserved: an in-flight handshake kept running until the deadline, and the error classification had to guess whether a raw socket error was caused by the deadline. Closing the conn from context.AfterFunc covers both deadline and cancellation, and ctx.Err() is already set by the time the close-induced error surfaces, so the time-based DeadlineExceeded attribution is no longer needed. The stop() result guards the window between a successful handshake and the AfterFunc firing so a closed conn is never handed back as a client.
49 lines
1.4 KiB
Go
49 lines
1.4 KiB
Go
package ssh
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// Handshake runs the SSH client handshake on an already dialed conn and
|
|
// returns the resulting client. Dialing bounds only the TCP establishment;
|
|
// a peer that accepts and then goes silent would block the handshake forever,
|
|
// so conn is closed as soon as ctx is done, which unblocks the handshake and
|
|
// surfaces the context error. conn is closed on any error.
|
|
func Handshake(ctx context.Context, conn net.Conn, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
|
|
stop := context.AfterFunc(ctx, func() { closeHandshake(conn, "conn on context done") })
|
|
|
|
sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
|
|
if err != nil {
|
|
if stop() {
|
|
closeHandshake(conn, "conn after handshake error")
|
|
}
|
|
return nil, handshakeError(ctx, err)
|
|
}
|
|
|
|
if !stop() {
|
|
closeHandshake(sshConn, "ssh conn after context done")
|
|
return nil, fmt.Errorf("ssh handshake: %w", ctx.Err())
|
|
}
|
|
|
|
return ssh.NewClient(sshConn, chans, reqs), nil
|
|
}
|
|
|
|
func closeHandshake(c io.Closer, label string) {
|
|
if err := c.Close(); err != nil {
|
|
log.Debugf("ssh: close %s: %v", label, err)
|
|
}
|
|
}
|
|
|
|
func handshakeError(ctx context.Context, err error) error {
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
return fmt.Errorf("ssh handshake: %w: %w", ctxErr, err)
|
|
}
|
|
return fmt.Errorf("ssh handshake: %w", err)
|
|
}
|