fix(hub): bound SSH handshake so a silent peer can't hang the updater

ssh.ClientConfig.Timeout only covers the TCP connect. A peer that accepts
the connection but never sends an SSH banner blocked ssh.NewClientConn
forever, wedging the system's updater goroutine and leaking the socket.
Set a deadline on the conn for the handshake and clear it afterward.
This commit is contained in:
henrygd
2026-09-28 10:18:59 -04:00
parent 65f00ae119
commit b684ac9910
2 changed files with 130 additions and 0 deletions

View File

@@ -992,6 +992,12 @@ func (s *System) createSSHClient() error {
// per-operation timeout in runSSHOperation instead (see issue #2041).
const sshKeepAliveInterval = 30 * time.Second
// sshHandshakeTimeout bounds the SSH handshake after the TCP connection is
// established. ssh.ClientConfig.Timeout only covers the TCP connect, so a peer
// that accepts the connection but never sends an SSH banner would otherwise
// block the updater forever.
var sshHandshakeTimeout = 10 * time.Second
// dialSSHWithKeepAlive dials an SSH connection like ssh.Dial, but enables TCP
// keep-alive on the underlying connection so half-open connections are
// eventually detected by the operating system.
@@ -1004,11 +1010,14 @@ func dialSSHWithKeepAlive(network, addr string, config *ssh.ClientConfig) (*ssh.
if err != nil {
return nil, err
}
_ = conn.SetDeadline(time.Now().Add(sshHandshakeTimeout))
sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
if err != nil {
_ = conn.Close()
return nil, err
}
// clear the handshake deadline so it doesn't apply to the long-lived connection
_ = conn.SetDeadline(time.Time{})
return ssh.NewClient(sshConn, chans, reqs), nil
}