Repository navigation
daemon: a dial timeout while another dial succeeds is not a transport wedge - #493
Conversation
|
One trade-off to weigh in review, found after opening this: The rule can delay the detector, not just quiet it. Peers with an established session send keepalives every 25s, and a keepalive counts as "heard from". If our outbound path is dead but a dialled peer's keepalives still arrive, roughly two thirds of the ~17s dials to it go uncounted. Uncounted timeouts do not reset the counter, so the detector still fires — after about 12 failed dials instead of 4. Whether the 2026-07-15 incident ( A stricter alternative is a few lines: skip a timeout only when another dial to the same peer completed after this one started. It is immune to keepalives, but would not cover a live peer that silently drops our SYNs for another reason. Tests added here: |
|
Converted to draft after review. The change fixes the false positive it targets, but it opens a false negative: the evidence that a peer is "answering" ( Counting only frames that answer something this node sent since the dial began (rather than any decrypted frame) would keep the fix without losing the outbound-dead case. |
9f731d9 to
9e7637b
Compare
…transport wedge The rx watchdog's partial-wedge detector trips on consecutiveDialTimeouts >= 4, a single global counter bumped by every dial that exhausts its retry budget. A burst of parallel dials to ONE peer whose SYN limiter drops the excess produces dozens of timeouts with no success in between, so a healthy node logged 'transport wedged ... reason=outbound-dial-wedged dial_timeouts=32', re-registered, and could escalate to the supervisor exit. Count a timeout only when the tunnel decrypted nothing from that peer since the dial started (TunnelManager.LastInboundDecrypt, the per-peer timestamp the path watchdog already uses). Timeouts to silent peers count as before, so the 2026-07-15 partial wedge is still detected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…traffic from the peer Review of the previous version found a false negative: it skipped a dial timeout whenever the tunnel had decrypted anything from that peer during the dial, and a node whose outbound path is dead still receives the peer's unsolicited 25s keepalives. With every outbound frame dropped the counter stayed at 0 and the watchdog reported progress; without a beacon it never recovered. A timeout is now skipped only when another dial completed while it ran. That dial's SYN reached its peer and the SYN-ACK came back, so the outbound path worked during this dial; the burst case this fixes (32 parallel dials to one peer, some answered by its SYN limiter) has such successes, a dead outbound path cannot. TestRxWatchdogPeerKeepalivesDoNotHideAWedge pins the reviewed case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the dial-wedge rule. - Whether another dial completed while a timed-out dial ran is now decided from a counter of completed dials, read when the dial starts. Comparing UnixNano timestamps followed wall-clock steps: a clock stepped back after a success hid every timeout for the size of the step. - The test for a dial completing during the burst now completes a real dial (its SYN answered with a SYN-ACK) instead of writing the field itself, and a new test checks that a dial completed before the burst does not hide it. Both fail against the matching mutant. - The keepalive-era helper and file name are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9e7637b to
0171406
Compare
Problem
The rx watchdog's
outbound-dial-wedgedcheck counted every dial timeout. A burst of parallel sends to one peer whose SYN limiter dropped some of them (dial_timeouts=32in the field) read as a wedged transport: a healthy node re-registered with the beacon and registry, and the watchdog could escalate to a restart.Fix
A dial timeout is not counted when another dial completed while it ran. That dial's SYN reached its peer and its SYN-ACK came back, so this node's outbound path worked during the timed-out dial; the timeout says something about that peer, not about this node's transport.
dialOKSeqcounts completed dials. A dial reads it when it starts and compares on timeout. It is a counter rather than a timestamp, so a wall-clock step cannot hide timeouts.Trade-off
A success over a path that does not share the broken part (a LAN or same-host peer) now exempts the WAN timeouts that overlap it. If such successes repeat more often than once per dial lifetime (~32s), a WAN-only wedge is not detected by this check. Main would still trip on dense timeouts between successes. Detection of a wedge where no dial succeeds is unchanged, and the restart-loop guard is untouched.
Tests
TestRxWatchdogDialTimeoutsWhileAnotherDialSucceedsAreNotAWedge: four timeouts to a silent peer while a real dial to a second peer completes (its SYN answered with a SYN-ACK). Fails on main (counter = 4).TestRxWatchdogDialSuccessBeforeTheBurstDoesNotHideIt: a dial completed before the burst does not hide it. Fails against "any success ever hides timeouts".TestRxWatchdogPeerKeepalivesDoNotHideAWedge: keepalives from the dialled peer do not hide a wedge.TestRxWatchdogDialTimeoutsToSilentPeerStillCount: the 2026-07-15 partial-wedge detection still fires.Each test takes ~32s (the full dial retry budget). They run in parallel and are skipped under
-short.Not covered
Retries that time out one after another against a single dead peer still count, as before. This fix only helps when some dial succeeds at the same time.
🤖 Generated with Claude Code