Bug: Getting EOF on connections #21
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
keyop-go/nfq_forwarder#21
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
We are sometimes seeing connections logging an 'EOF' and failing to connect, as in the logs below:
Determine the cause and fix.
changed the description
mentioned in commit
60fbf474eamentioned in merge request !63
Root cause
sshConnections.connection()(internal/nfq_forwarder/nfq_forwarder.go) dials the SSH connection to the jump host once on first use, then caches that single*sshtunnel.Connforever - every subsequent packet on that queue reuses the same cached client, with no liveness check.sshtunnel.Dialalso set no SSH keepalive at all - nothing periodically pings the jump host, sogolang.org/x/crypto/ssh's connection has no way to notice a transport that has died silently (e.g. a NAT/firewall idle timeout dropping the TCP connection without either end sending a FIN/RST).Once that underlying connection dies, the next attempt to open a channel to the real destination (
sshClient.DialinsidestartTunnel) fails immediately withEOF- exactly the error in this ticket's log excerpt. Since nothing ever evicted the dead connection from the pool, every subsequent packet kept hitting the same broken client and logging the same EOF, forever, until the whole process was restarted.It's actually worse than "logs an error and retries": once a tunnel's DNAT rule exists for a destination, NFQUEUE is never consulted again for it (by design, for normal operation - see
deleteDNATRuleForTunnel's doc comment). So a destination whose dial failed even once stayed silently DNAT'd into a dead end forever, with no way to self-heal - not just the specific destination in the log excerpt, but any destination behind that jump host connection from that point on.Fix (MR !63)
Three pieces, working together:
sshtunnel.Connnow runs a goroutine (started byDial) that periodically sends an SSHkeepalive@openssh.comrequest and force-closes the connection if no reply arrives within one interval. Configurable viassh_keepalive_interval_secondsin a--configYAML file, default 30s.sshtunnel.Conn.Wait()blocks until the connection dies for any reason.sshConnections.connection()now spawns a goroutine that calls this right after a successful dial and removes the connection from the pool once it returns, so the next packet triggers a fresh dial instead of reusing a client whose transport has already failed.sshtunnel.Conn.SetTunnelFailedHooklets a tunnel whose destination dial fails tear itself down (closing its local listener and removing its DNAT rule), so the next connection attempt to that destination gets a fresh chance through NFQUEUE instead of piling up against a tunnel that can never succeed.Full details, including new tests (a real in-process SSH client/server test harness was added specifically to exercise all three of these against genuine SSH protocol behavior, not mocks), in MR !63.
mentioned in commit
6e320bbb1b