Files
ctrld/cmd/cli/recovery_state.go
T
Dev Scribe 80d1acdfd5 dns intercept: port DNS-less network recovery to master
Port !997 from v1.0 onto the context-aware master recovery lifecycle. Preserve master logging and resolver APIs while adding macOS default-route DHCP detection, temporary DNS-target cleanup, and atomic recovery ownership.

Includes parser, lifecycle, failure, and concurrency regressions plus the corrected macOS QA helper. Relates to #533 and #597.
2026-09-02 16:54:35 +07:00

81 lines
2.8 KiB
Go

package cli
import (
"context"
"github.com/Control-D-Inc/ctrld"
)
// beginRecovery atomically transfers ownership of shared recovery state. A
// network change cancels and replaces the current owner without exposing a nil
// recoveryCancel gap; other triggers are coalesced while an owner exists.
func (p *prog) beginRecovery(reason RecoveryReason) (ctx context.Context, gen uint64, intercept bool, ok bool) {
p.recoveryCancelMu.Lock()
defer p.recoveryCancelMu.Unlock()
if reason != RecoveryReasonNetworkChange && p.recoveryCancel != nil {
return nil, 0, false, false
}
if p.recoveryCancel != nil {
p.recoveryCancel()
}
ctx, cancel := context.WithCancel(context.Background())
gen = p.recoveryGen.Add(1)
intercept = dnsIntercept && p.dnsInterceptState != nil
p.recoveryCancel = cancel
p.recoveryRunning.Store(true)
p.recoveryBypass.Store(intercept)
return ctx, gen, intercept, true
}
func (p *prog) recoveryOwnsState(gen uint64) bool {
p.recoveryCancelMu.Lock()
defer p.recoveryCancelMu.Unlock()
return p.recoveryGen.Load() == gen && p.recoveryCancel != nil
}
func (p *prog) systemNameserversForInterceptRetry() []string {
loggerCtx := ctrld.LoggerCtx(context.Background(), p.logger.Load())
_, system := initializeOsResolverWithSystemNameserversFn(loggerCtx, true)
if system == nil {
return []string{}
}
return system
}
// completeRecovery releases shared state only if gen still owns it. The bypass
// reset is unconditional because live intercept state can disappear while a
// recovery is running, but a stale true flag still affects proxy routing.
func (p *prog) completeRecoveryState(gen uint64) bool {
p.recoveryCancelMu.Lock()
defer p.recoveryCancelMu.Unlock()
if p.recoveryGen.Load() != gen || p.recoveryCancel == nil {
return false
}
p.recoveryBypass.Store(false)
p.recoveryRunning.Store(false)
p.recoveryCancel = nil
return true
}
// recoveryCanceledCleanup resets shared recovery state after a canceled or
// failed recovery, but only when the recovery identified by gen was NOT
// superseded by a newer one (issue #597).
//
// A network-change cancellation is normally followed immediately by a new
// handleRecovery that owns recoveryBypass/recoveryRunning/recoveryCancel;
// clearing them here would disable the successor's bypass mid-flight and
// make it uncancellable. But when the canceled recovery is the LAST one
// (e.g. the tail of a network flap burst), nothing else will ever clear the
// flags: the daemon would stay in recovery bypass forever — every query
// detouring to the OS resolver — and the DNS-settings watchdog would stay
// permanently disabled.
func (p *prog) recoveryCanceledCleanup(gen uint64) {
if !p.completeRecoveryState(gen) {
// Superseded: the newer recovery owns the shared state.
return
}
p.Info().Msg("Recovery canceled with no successor; cleared recovery state and DHCP bypass")
}