-
Notifications
You must be signed in to change notification settings - Fork 37
Description
Description
When a VPN client (Windscribe) disconnects on macOS, it wipes all DNS settings from the Wi-Fi interface. networksetup -getdnsservers Wi-Fi returns "There aren't any DNS Servers set on Wi-Fi." ctrld's built-in dns_watchdog does not restore DNS in this scenario, even with dns_watchdog_enabled = true and dns_watchdog_interval = "2s".
Environment
- ctrld version: 1.4.9
- OS: macOS 15.3 Sequoia (also reproducible on macOS 26.x Tahoe)
- VPN client: Windscribe (AmneziaWG protocol)
- ctrld mode:
--cd(API-managed) - Listener:
0.0.0.0:53
Steps to Reproduce
- Install ctrld with
sudo ctrld start --cd <resolver_id> - Verify DNS is set to
127.0.0.1:networksetup -getdnsservers Wi-Fi - Connect Windscribe VPN (any protocol)
- Disconnect Windscribe VPN
- Check DNS:
networksetup -getdnsservers Wi-Fi
Expected: DNS should be 127.0.0.1 (restored by watchdog)
Actual: DNS is empty ("There aren't any DNS Servers set on Wi-Fi")
Running sudo ctrld restart manually restores DNS, but the watchdog does not recover automatically.
Analysis
Looking at the source code, the dnsChanged() comparison logic in prog.go is correct - an empty slice vs ["127.0.0.1"] would return true. The issue appears to be in the watchdog's ticker loop:
case <-ticker.C:
if p.recoveryRunning.Load() {
return // This kills the watchdog goroutine permanently
}When the VPN disconnects, it triggers a network change event that sets recoveryRunning = true. The watchdog's next tick sees this flag and executes return, which permanently terminates the watchdog goroutine. After the recovery process completes, the watchdog is dead and DNS changes are never detected again.
The return should likely be continue (to skip one tick and retry on the next interval) rather than terminating the goroutine entirely.
TOML Config
[service]
cache_enable = true
cache_serve_stale = true
dns_watchdog_enabled = true
dns_watchdog_interval = "2s"
[listener]
[listener.0]
ip = '0.0.0.0'
port = 53
[listener.0.policy]
name = 'Failover'
failover_rcodes = ['SERVFAIL']
networks = [
{'network.0' = ['upstream.0', 'upstream.1']}
]
[upstream]
[upstream.0]
name = 'ControlD'
type = 'doh'
endpoint = 'https://dns.controld.com/<resolver_id>'
bootstrap_ip = '76.76.2.22'
timeout = 5000
[upstream.1]
name = 'Cloudflare'
type = 'doh'
endpoint = 'https://dns.cloudflare.com/dns-query'
bootstrap_ip = '1.1.1.1'
timeout = 5000Workaround
Custom LaunchAgent that polls networksetup -getdnsservers Wi-Fi every 5 seconds and restores 127.0.0.1 if DNS is empty or changed. This works reliably but shouldn't be necessary given the built-in watchdog feature.
Related Issues
- DNS resolving fails when restarting or awaking the computer #34 - DNS resolving fails after restart/wake (similar category: DNS not recovered after state change)
- After suspend on laptop ctrld isn't working #223 - ctrld not working after suspend on Linux (same symptom, different platform)