Cloud / SRE
When dig Works but the Browser Does Not: Debugging macOS Split DNS
A practical way to diagnose stale per-domain DNS rules on macOS when one device reaches a site, dig succeeds, but browsers and curl time out.
On this page
A phone can open a website while the same hostname fails in browsers on one Mac. A successful dig lookup does not settle the DNS question: the application resolver may select a different server. The useful starting point is a comparison of lookup paths:
- A phone resolves and opens the site.
digon the Mac returns valid public IP addresses.curl https://example.comstalls during name resolution.- Connecting to a known edge IP with
curl --resolvereaches the site.
Together, these signals narrow the investigation to the Mac’s lookup path. A stale split-DNS rule is one hypothesis to test; the symptoms alone do not identify which resolver or VPN setting is responsible.
Terms used here
| Term | Meaning |
|---|---|
| System resolver | The macOS DNS routing layer used by normal applications, including browsers and curl. |
| Split DNS | A configuration that sends queries for selected domains to a specific DNS server, often through a VPN. |
/etc/resolver | A macOS directory that can define per-domain DNS servers. A file named example.com applies to that domain and its subdomains. |
dig | A DNS diagnostic client. Its result does not necessarily prove that the macOS application resolver is using the same DNS path. |
utun | A macOS tunnel interface commonly created by VPN software. |
curl --resolve | A way to provide a temporary hostname-to-IP mapping for one request while preserving the original hostname for TLS and HTTP. |
The misleading symptom
Start with a public lookup:
dig +short example.com A
dig +short example.com AAAA
If this returns public addresses, it proves that the DNS server queried by dig can resolve the name. It does not prove that Safari, Chrome, or other macOS applications use that answer.
Test the application path separately:
curl -sS -I \
--connect-timeout 8 \
--max-time 15 \
https://example.com/
A result such as this narrows the problem:
curl: (28) Resolving timed out after 8000 milliseconds
The timeout happened before TCP, TLS, or HTTP. Changing Cloudflare rules, origin security groups, or certificates will not fix this stage of the request.
Compare public DNS with the macOS resolver
Use dscacheutil to exercise the system lookup path:
dscacheutil -q host -a name example.com
If dig returns addresses but dscacheutil returns nothing or waits until timeout, inspect the macOS resolver graph:
scutil --dns
Look for a domain-specific resolver:
resolver #8
domain : example.com
nameserver[0] : 192.0.2.53
This domain-specific configuration can take precedence over the default resolver for matching names. Apple documents the multiple-resolver search strategy and domain matching for files in /etc/resolver.[1] The address is from a documentation-only range and does not identify a real resolver.
Now inspect persistent resolver files:
if [ -d /etc/resolver ]; then
find /etc/resolver -maxdepth 1 -type f -print \
-exec sed -n '1,20p' {} \;
fi
A file like this creates the rule:
/etc/resolver/example.com
with contents:
nameserver 192.0.2.53
Test that DNS server directly:
dig @192.0.2.53 example.com A \
+time=2 \
+tries=1 \
+noall \
+answer \
+comments
If it times out, inspect the route:
route -n get 192.0.2.53
A route through an inactive or unhealthy utun interface supports this failure chain when the configured DNS server also times out:
Browser
-> macOS system resolver
-> /etc/resolver/example.com
-> private DNS server
-> stale or broken VPN path
-> timeout
The phone works because it does not have the Mac’s local resolver file or VPN route.
Test HTTPS independently of DNS
Take one public IP returned by dig and bypass DNS for a single HTTPS request:
curl -sS -I \
--resolve example.com:443:203.0.113.10 \
--connect-timeout 8 \
--max-time 15 \
https://example.com/
Replace 203.0.113.10 with an address actually returned for the domain.
The --resolve option supplies an address for the hostname and port without changing the URL host.[2] If the HTTPS request completes, that network and TLS path works for the tested address. Other addresses or application routes may still fail.
A CDN may return a challenge or 403 to command-line curl. That is still useful evidence: DNS, TCP, and TLS succeeded, and the response came from the application-security layer. Treat that challenge as a separate issue from the original resolver timeout.
Recover safely
First record the current state:
networksetup -getdnsservers "Wi-Fi"
networksetup -getsearchdomains "Wi-Fi"
scutil --dns
Create a recoverable backup:
network_backup_dir="$(mktemp -d /private/tmp/macos-dns-backup.XXXXXX)"
echo "Backup directory: ${network_backup_dir}"
For one stale domain, move only that rule:
sudo mkdir -p "${network_backup_dir}/resolver"
sudo mv \
/etc/resolver/example.com \
"${network_backup_dir}/resolver/example.com"
If the machine should have no custom per-domain resolvers at all, back up the entire directory:
if [ -d /etc/resolver ]; then
sudo mv /etc/resolver "${network_backup_dir}/resolver"
fi
Moving the entire directory also removes legitimate internal DNS rules. Use it only when restoring the machine to a fully default resolver configuration is intentional.
If Wi-Fi has manually configured DNS servers and should return to DHCP-provided DNS:
sudo networksetup -setdnsservers "Wi-Fi" empty
Reload the resolver state:
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
If a VPN is still connected but its tunnel is unhealthy, disconnect and reconnect it through the VPN client. Avoid deleting tunnel routes by hand unless the VPN client’s normal cleanup has failed and you fully understand the route ownership.
Validate the recovery
Confirm that Wi-Fi no longer has manual DNS servers:
networksetup -getdnsservers "Wi-Fi"
The expected output is:
There aren't any DNS Servers set on Wi-Fi.
Inspect the active resolver graph again:
scutil --dns
The stale per-domain resolver should be gone. The default resolver will normally show DNS servers supplied by the local network, such as the router address.
Finally, test both the system resolver and HTTPS:
dscacheutil -q host -a name example.com
curl -sS -I \
--connect-timeout 8 \
--max-time 15 \
https://example.com/
An HTTP response, including a deliberate CDN challenge, shows that this request passed name resolution. Verify the normal browser path as well before closing the incident.
Roll back if the private resolver was required
Restore the backed-up directory:
sudo mv "${network_backup_dir}/resolver" /etc/resolver
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
scutil --dns
For a single-domain backup, recreate /etc/resolver first if needed and restore only that file.
Design split DNS around the VPN lifecycle
The durable fix is not a recurring cache flush. It is to make resolver ownership match tunnel ownership:
- Install private resolver rules when the VPN becomes usable.
- Remove those rules when the VPN disconnects.
- Scope rules to the narrowest internal domain instead of an entire public parent domain.
- Check private DNS reachability after the tunnel comes up.
- Keep a rollback path for manually installed resolver files.
Adding a public DNS server as a fallback inside a private-domain resolver file is tempting, but it can leak internal names and create inconsistent negative responses. Lifecycle-aware installation is safer than mixing private and public resolvers for the same namespace.
The next diagnostic decision
When one Mac fails while another device works, compare layers instead of clearing every cache:
dig works
+ system lookup fails
+ curl reports "Resolving timed out"
+ curl --resolve reaches the edge
= inspect the macOS application resolver path
Identify the selected resolver and verify its reachability before changing it. After removing a confirmed stale rule, repeat the system lookup and normal HTTPS request; keep the backup until required private names also resolve correctly.
References
[1] Apple. “resolver(5): resolver configuration file format.”