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") }