tailscale / tailscale/tailscale
macos: race with other DNS-managing apps can wedge DNS forwarding on a stale resolver
- Dominant language
- Go
- Stars
- 36.5k
- Forks
- 3.2k
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 132
Description
## Human description
Tailscale and the infoblox endpoint (BloxOne) software both need to mess with DNS settings. BloxOne wants to send all queries to a CoreDNS instance listening on 127.0.0.2 which then hits their SaaS for threat detection etc. Tailscale wants to intercept everything, catch any MagicDNS queries, and pass the rest through. There are short windows during which they race against each other, leading to a situation where MagicDNS works but other queries don't get directed to BloxOne.
I have a pending PR on the BloxOne side to mitigate this by triggering a DNS config change, which wakes up Tailscale. It does this a few times hoping the race lands favorably.
I also have a PR ready for Tailscale, described in the AI sloptext below.
## AI description
### What is the issue?
On macOS when "Use Tailscale DNS settings" is on, we install 100.100.100.100 as the primary catch-all resolver and forwards non-tailnet queries to a **snapshot** of the OS base DNS configuration (`rcfg.Routes["."]`, populated from `GetBaseConfig()` in `net/dns/manager.go`), refreshed only on DNS-change/link events (`RecompileDNSConfig`; its call site in `wgengine/userspace.go` acknowledges "notable timing issues").
DNS-filtering endpoint agents (in our case Infoblox BloxOne Endpoint) point interface DNS at a loopback proxy (e.g. `127.0.0.2`) and briefly restore the original DHCP resolvers during restarts and self-heal cycles. If Tailscale re-samples inside one of those transient windows, it latches the DHCP resolver as its `Routes["."]` upstream, and all non-tailnet DNS bypasses the agent's proxy from then on.
This is a **stable wrong equilibrium**: the agent's desired interface DNS already matches the actual system state, so it never issues another `SCPreferencesApplyChanges` — precisely the event Tailscale needs to re-sample. And since the healthy and broken states have **byte-identical `scutil --dns` output** (the only difference is Tailscale's internal snapshot), it's effectively undiagnosable in the field.
### Steps to reproduce
1. macOS, Tailscale GUI app connected, MagicDNS on (default).
2. Install any agent that points per-service interface DNS at a local loopback
proxy (we used BloxOne Endpoint; a script flipping interface DNS between
the DHCP resolver and a loopback reproduces the shape).
3. Restart the agent repeatedly — the race window is the sub-second interval
where interface DNS shows the DHCP resolver.
4. Once Tailscale samples inside the window, non-tailnet queries go to the
DHCP resolver instead of the loopback proxy and nothing ever corrects it:
`tailscale dns status` shows the stale fallback, `scutil --dns` looks
completely normal.
We confirmed both the latch and the stable equilibrium with a deterministic mock of the snapshot/re-sample semantics; happy to share details.
### What did you expect to happen?
The quad-100 fallback converges to the OS base configuration once the system settles, rather than latching a transient intermediate state indefinitely.
### Proposed fix (we can send a PR if the direction is agreeable)
A "settle re-sample" in `net/dns/manager.go`: when `setLocked` consumed `GetBaseConfig()`, schedule a single delayed re-check (~2–3 s) — re-read the base config and recompile once if the nameserver set differs from the snapshot that was used. Debounced, one-shot per compile, no behavior change when the base config is stable. Testable with the existing fake-OSConfigurator patterns in `net/dns/manager_test.go`.
### OS
- macOS 26.6 (arm64)
### OS version
- macOS 26.6 (arm64)
### Tailscale version
- Tailscale 1.102.2 (GUI, macOS); code references against main @ `5201273ae`
### Other software
- Infoblox Endpoint 2.6.1
Contributor guide
Research direction
Start in net/dns/manager.go at setLocked and GetBaseConfig(), then inspect RecompileDNSConfig and the timing note in wgengine/userspace.go. Use the fake-OSConfigurator patterns in net/dns/manager_test.go to verify a delayed, one-shot re-sample corrects a transient snapshot without changing behavior for a stable base configuration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, macos
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100