dns: fix recovery race condition during rapid network transitions

When multiple network changes fire in quick succession (e.g., VPN
disconnect + interface swap), the second handleRecovery() call cancels
the first but inherits stale DoH transports, causing DNS blackouts
of up to 30 seconds.

Three changes to reduce worst-case recovery from ~30s to <3s:

1. ForceReBootstrap() on recovery entry — closes dead connections and
   creates fresh transports synchronously before probing, replacing the
   lazy ReBootstrap() flag that left stale connections for probes to hit.

2. Debounce handleRecovery() for network changes (500ms window) — only
   the recovery flow is debounced; all other state updates (IP, pf
   anchor, VPN DNS, tunnel checks) still run immediately on every event.
   This eliminates the cancel-and-restart race without missing state.

3. Combined effect: ForceReBootstrap closes old in-flight connections
   (closeTransports) and builds new ones (SetupTransport) atomically,
   so recovery probes never inherit dead connections from a prior
   recovery attempt.
This commit is contained in:
Codescribe
2026-04-21 13:49:53 -04:00
committed by Cuong Manh Le
parent 70b45710e7
commit b3c670b17e
2 changed files with 60 additions and 8 deletions
+8 -2
View File
@@ -133,6 +133,12 @@ type prog struct {
recoveryCancelMu sync.Mutex
recoveryCancel context.CancelFunc
recoveryRunning atomic.Bool
// recoveryDebounceTimer coalesces rapid NetworkChange recovery triggers
// into a single handleRecovery call. Only handleRecovery is debounced —
// all other state updates (IP, pf anchor, VPN DNS) run immediately.
recoveryDebounceMu sync.Mutex
recoveryDebounceTimer *time.Timer
// recoveryBypass is set when dns-intercept mode enters recovery.
// While true, proxy() forwards all queries to the OS/DHCP resolver
// instead of the configured upstreams. This allows captive portal
@@ -1017,10 +1023,10 @@ func (p *prog) resetDNS(isStart bool, restoreStatic bool) {
if err := p.stopDNSIntercept(); err != nil {
p.Error().Err(err).Msg("Failed to stop DNS intercept mode during reset")
}
// Clean up VPN DNS manager
p.vpnDNS = nil
return
}
netIfaceName := ""