fix(dialer): evict dead SSHD sessions before reuse to prevent channel-open failure (#382)
When an SSH session behind an SSHD dialer dies silently (NAT timeout, server disconnect, idle TCP drop), IsClosed() may still return false because the health-check goroutine hasn't detected the failure yet. Returning this dead cached session to the connector causes: ssh: unexpected packet in response to channel open: Add a Ping(req)-based liveness check before returning a cached session. If the ping fails, evict the session so the next Dial rebuilds a fresh connection. This matches the dead-session eviction pattern already used by the SSH, KCP, QUIC, and ICMP dialers. Also export the ping-as-health-check as Session.Ping(timeout) so it can be used by external health probes. Retain the existing ping() helper unmodified.
This commit is contained in:
@@ -63,6 +63,18 @@ func (d *sshdDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialO
|
|||||||
delete(d.sessions, addr) // session is dead
|
delete(d.sessions, addr) // session is dead
|
||||||
ok = false
|
ok = false
|
||||||
}
|
}
|
||||||
|
if ok {
|
||||||
|
// Health check: verify the cached session is still alive before
|
||||||
|
// returning it. A dead-but-not-yet-detected session (e.g. idle
|
||||||
|
// connection silently dropped by NAT) would cause "ssh: unexpected
|
||||||
|
// packet in response to channel open" later in the connector.
|
||||||
|
if err := session.Ping(d.md.keepaliveTimeout); err != nil {
|
||||||
|
d.options.Logger.Debugf("sshd session ping failed: %v, recreating", err)
|
||||||
|
session.Close()
|
||||||
|
delete(d.sessions, addr)
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
var options dialer.DialOptions
|
var options dialer.DialOptions
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
|||||||
@@ -114,6 +114,25 @@ func (s *Session) Keepalive(interval, timeout time.Duration, retries int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ping checks if the SSH session is alive by sending a keepalive request.
|
||||||
|
// It blocks until a response is received or the timeout expires.
|
||||||
|
// A nil error indicates the session is healthy.
|
||||||
|
func (s *Session) Ping(timeout time.Duration) error {
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = defaultKeepaliveTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-s.ping():
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) ping() <-chan error {
|
func (s *Session) ping() <-chan error {
|
||||||
ch := make(chan error, 1)
|
ch := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
Reference in New Issue
Block a user