Roll back orphaned tunnel on DNAT failure, distinguish suppressed dial retries in logs #72
No reviewers
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!72
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue31_dont_leak_orphaned_tunnel_on_dnat_failure"
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?
Closes #31
Background
Found while investigating #4. The ticket originally focused on three unconditional
NfDrop-with-no-retry paths ingetNfCallback, plus the 5ssshRetryCooldownblanket-dropping packets for every destination sharing an SSH connection pool. Digging into these turned up two genuinely distinct, independently-worth-fixing issues:1. A failed DNAT insertion leaves an orphaned, silently-unencrypted tunnel (the more severe finding)
getNfCallbackcallssshConn.OpenTunnel(which registers the tunnel and starts its local listener) before callingaddDNATRulesFunc. If the DNAT insert then fails, the code dropped the current packet but never rolled the tunnel back - it stayed registered as "open". Every later packet to that same destination would then hitsshtunnel.ErrPortTunnelAlreadyOpenand get verdictedNfAccept(issue #4's mechanism, correctly assuming a DNAT rule already exists for an already-open tunnel) - except no DNAT rule was ever successfully inserted. The result: traffic to that destination would silently flow out unencrypted, direct to the real destination, bypassing the tunnel entirely, for as long as the process runs - not fail loudly, not retry, just silently misroute.Fix: on a DNAT failure, call
sshConn.ClosePortTunnelto roll back the just-opened tunnel, so the next packet gets a clean, freshOpenTunnel+addDNATRulesFuncattempt instead.2. Cooldown-suppressed dial failures were indistinguishable from fresh ones in the logs
sshConnections.connection()'s 5ssshRetryCooldowndeliberately fails every packet on the pool without even attempting a fresh dial while a previous failure is still within the cooldown window. I want to be upfront that I don't think this is itself a bug to fix: the pool has exactly one shared SSH connection, so once it's down every destination behind it is genuinely, equally unreachable - and retrying the full (up tosshDefaultTimeout-bounded) handshake on every single packet during an outage would stall this queue's single-threaded read loop far longer than a fast, clean drop. What was a real gap: both cases (a genuine fresh-attempt failure, and a cooldown-suppressed one) logged an identical message, so an operator watching logs during an outage couldn't tell whether nfq_forwarder was still actively retrying or had backed off.Fix: a new
errDialSuppressedByCooldownsentinel wraps the cached error on the suppressed path, lettinggetNfCallbacklog it distinctly (log.Infof, expected backoff behaviour) from a genuine fresh failure (log.Errorf).Testing
TestGetNfCallbackDNATFailureClosesOrphanedTunnel(renamed from...Drops) assertssshConn.ListTunnels()is empty after a failed DNAT insertion.TestSSHConnectionsDialFailureCachedDuringCooldownand the newTestGetNfCallbackCooldownSuppressedDialLogsDistinctlyassert the sentinel wraps only the suppressed call's error, and that the two cases log distinctly.make lint/make testboth clean.Docs
Added a
CLAUDE.mdDesign notes bullet covering both fixes and the reasoning for not changing the cooldown's blast radius itself.Related
Part of the same investigation as #4 (v1.1.1) and #32 (v1.1.2). Last ticket in the agreed order: #33 (
NfQeueueverdicts always target queue 0 - likely moot for the primary path after #4's fix, worth auditing any other call sites).mentioned in issue #31
assigned to @claude
requested review from @danny
This all looks sane - let me test on a real host before approving.
This works practically - no issues found in empirical testing on a real system. Approving.
approved this merge request
mentioned in commit
067ef83730