The browser was lying too
The terminal was dead and the browsers were perfectly fine. Three curl commands settled it in ten seconds — and the browser working turned out to be the clue, not the counter-evidence.
The symptom was strange enough to be interesting:
Not slow. Not partially broken. curl and git hung until they gave up, while Safari and Chrome loaded everything instantly. Same machine, same tunnel, same second.
What I assumed, and why it was wrong
My first instinct was IPv6, and I had a good reason: I had documented exactly that failure mode myself. If the v6 default route points into the tunnel but the tunnel carries no v6, browsers survive because Happy Eyeballs races A and AAAA and abandons v6 in about 250ms, while curl and ssh sit through a full TCP timeout. It fits the symptom perfectly.
It was wrong. One measurement killed it: with the tunnel up, forced-v6 and forced-v4 requests behaved identically, and the v6 default route never moved. The failure mode I had personally written down was not this failure mode.
The three probes
Rather than theorise again, run three requests that differ in exactly one respect — how much name resolution each one needs.
1. No DNS at all. Tests routing, and nothing else.
curl -4 https://1.1.1.12. Resolves inside an HTTPS session — this is what a modern browser does.
curl 'https://1.1.1.1/dns-query?name=example.com&type=A' \
-H 'accept: application/dns-json'3. The system resolver — what every normal tool on the machine uses.
dscacheutil -q host -a name example.comWith the tunnel up:
| Probe | Result |
|---|---|
| HTTPS to a raw IP (no DNS at all) | 0.2s |
| Egress check — traffic leaving via home | correct |
| Far side, raw ICMP | 65–80ms |
| DNS-over-HTTPS (what a browser does) | 0.2s |
| System resolver (what everything else uses) | 60.0s |
| curl / git by name | fail |
That is not an ambiguous result. Three probes, ten seconds, and the entire space of“is it the tunnel, the routing, the firewall, IPv6, or DNS” collapses to one answer.
The actual cause
macOS attaches DNS servers to a network service, not to the machine. The venue's DHCP handed out a public resolver, scoped to the Wi-Fi service. When the full tunnel came up it installed 0/1 and 128.0/1 routes pointing into the tunnel interface — so mDNSResponder was source-binding its queries to the Wi-Fi interface while the routing table sent them into the tunnel.
The queries left with the wrong source address for the path they took. Nothing came back. Every lookup burned the full 60-second timeout before giving up.
The fix: a resolver that is actually reachable through the tunnel, rather than one pinned to the local network's interface.
networksetup -setdnsservers "Wi-Fi" <resolver-behind-the-tunnel>System resolver 60.0s → 0.2s. curl by name: fail → 0.7s. Internal names came back at the same instant, which also resolved a separate “internal DNS is refused” mystery I had been carrying. It was never separate.
Why dig made it worse
Throughout the outage, dig worked. Public names, internal names, against the local resolver and against my own router — all fine, all fast.
dig does not bind to the service interface. It builds a socket and follows the routing table like any other program, so it sails straight through the exact misconfiguration that was destroying every other tool on the machine.
The browser was the clue, not the counter-evidence
I treated “but the browser works” as evidence that the network was basically fine and something was wrong with my terminal. It was the exact opposite.
A browser with DNS-over-HTTPS resolves inside an HTTPS session to a hardcoded resolver address. It never touches the system resolver. It is structurally incapable of noticing that system DNS is broken.
So “browsers fine, terminal dead” is not a paradox to be explained away. It is a signature, and it points at one layer: the resolver, and only the resolver. Nothing else in the stack can fail that way — routing failures take the browser down too.
The cache hides the beginning
One more detail, because it explains why this felt intermittent and unreproducible. The first sample after connecting showed 2.3 seconds — slow, but working. Only the samples after that failed outright. The DNS cache still held entries from before the tunnel came up, and served them happily for about a minute while the resolver underneath was already dead.
What I'd tell myself at the start
Three probes beat one theory. The measurement that solved this was not clever. It was three curl commands chosen so each needs one more layer than the last. Ten seconds, and every theory died at once.
Your instruments have their own DNS now. The browser has DoH. dig has no service binding. Both route around the fault. It is not enough to distrust your own tooling — the tools you did not write are also lying, and they lie in the direction that makes things look healthier than they are.
Know which service owns DNS, not just which layer. A VPN's DNS = line is global on macOS; /etc/resolver is scoped; and networksetup -setdnsserversbinds to one network service. That last one has a sting: fix it for Wi-Fi and you have fixed Wi-Fi only. Dock the laptop or tether to a phone and that is a different service, which quietly keeps its old resolver. Nothing breaks, nothing warns you, and every query goes out the way you were trying to prevent.
The real fix is a VPN server that hands out DNS to its clients, so the resolver follows the tunnel automatically instead of being toggled by hand twice per network. Which had been on my list for two days when all of this happened.