mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
macOS intercept mode fails totally on networks that provide no usable IPv4 DNS (e.g. IPv6-only iPhone tethering with 464XLAT): the pf ruleset blocks all outbound IPv6 port 53, and with no IPv4 DNS configured mDNSResponder emits no DNS packets at all, so pf has nothing to intercept while the Control D upstream stays provably healthy. When network-change recovery discovers no usable IPv4 DNS on the default-route service, set 127.0.0.1 as that service DNS so macOS can emit queries that land directly on the listener. The entry is removed when the network regains IPv4 DNS and on intercept shutdown; networks that provide IPv4 DNS are never modified. Runs on the already-debounced recovery path so interface flaps do not churn networksetup. Also fix the canceled-recovery state leak: the cancellation early return never reset recoveryBypass/recoveryRunning, so a flap burst ending in a canceled recovery left the daemon in bypass forever with the DNS watchdog disabled. Cleanup is generation-gated so a superseded recovery never clears state owned by its successor.
76 lines
2.7 KiB
Go
76 lines
2.7 KiB
Go
package cli
|
|
|
|
import "context"
|
|
|
|
// 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 systemNameserversForInterceptRetry() []string {
|
|
_, system := initializeOsResolverWithSystemNameserversFn(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) completeRecovery(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.completeRecovery(gen) {
|
|
// Superseded: the newer recovery owns the shared state.
|
|
return
|
|
}
|
|
mainLog.Load().Info().Msg("Recovery canceled with no successor; cleared recovery state and DHCP bypass")
|
|
}
|