It was DNS!
Wow - that's a long time between posts!
So I've been daily driving CachyOS for a few days now, and on the whole it's been pretty good - easy setup, runs (mostly) stably on my Mid-2010 Mac Pro, an I can even play my Steam / Epic games on it!
Then I tried to open my UniFi controller at unifi.my-home-domain and both Firefox and Chromium refused to find it.
The odd part - dig resolved the name perfectly, and my MacBook Air on the same network opened the page without complaint. Everything in my homelab sits behind an HAProxy reverse proxy, with a pair of Pi-holes serving local DNS records for services like UniFi.
There's an old sysadmin haiku: It's not DNS. There's no way it's DNS. It was DNS. I spent most of an evening proving it right.
Suspect #1: the browsers' own DNS
Modern browsers love to bypass the system resolver. Firefox's "Default Protection" DNS-over-HTTPS will happily send your lookups to Cloudflare, which knows nothing about your Pi-hole's local records. Chromium has Secure DNS and its own async resolver too.
So I switched them all off:
- Firefox: Settings → Privacy & Security → DNS over HTTPS → Off (or
network.trr.mode = 5inabout:config) - Chromium: Settings → Privacy and security → Security → Use secure DNS off, and cleared the host cache at
chrome://net-internals/#dns
No change. Both browsers still couldn't find the host. Worth ruling out, but not our culprit.
Suspect #2: IPv6 (a detour)
Next theory: the CachyOS box was picking up IPv6 DNS servers from router advertisements that didn't point at the Pi-holes. That sent me off giving the Pi-hole hosts static ULA addresses (fd12:3456:789a:1::53 and friends) and pointing the UniFi IPv6 DNS settings at them.
When that got fiddly, I took the blunt route and disabled IPv6 on the client entirely:
sudo nmcli con mod "<conn>" ipv6.method disabled
sudo nmcli con up "<conn>"Still broken. In hindsight IPv6 was involved, just not the way I thought.
Meet 127.0.0.53
dig was reporting SERVER: 127.0.0.53#53, which is not one of my Pi-holes. It's the stub listener for systemd-resolved, running locally on the CachyOS box:
$ sudo ss -lunp 'sport = :53'
UNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=465,fd=24))
UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=465,fd=22))
$ cat /etc/resolv.conf
# Generated by NetworkManager
search my-home-domain
nameserver 127.0.0.53
$ grep ^hosts /etc/nsswitch.conf
hosts: mymachines resolve [!UNAVAIL=return] files myhostname dnsSo every lookup on the box goes through resolved. dig talks to its stub over DNS. The browsers call glibc's getaddrinfo(), which hits resolved through the resolve NSS module and, thanks to [!UNAVAIL=return], never falls back to anything else.
resolvectl status showed resolved forwarding to both Pi-holes (192.168.1.53 and 192.168.1.54). Same resolver, same upstreams. So why did one path work and not the other?
The smoking gun
The answer came from asking for one address family at a time:
$ resolvectl query -t A unifi.my-home-domain
proxy.my-home-domain IN A 192.168.1.80 -- link: enp10s0
$ resolvectl query -t AAAA unifi.my-home-domain
unifi.my-home-domain: resolve call failed: ... does not have any RR of the requested type
$ getent ahosts unifi.my-home-domain
$ # nothing at all
$ curl -sv -o /dev/null https://unifi.my-home-domain/
* Trying 192.168.1.80:443...
< HTTP/2 200unifi is a Pi-hole CNAME to the box where HAProxy lives (proxy.my-home-domain). That host had an A record but no AAAA record.
digandcurlonly asked for A records (curl, because IPv6 was off on the box), so they got an answer.getent ahostsand the browsers ask for IPv4 and IPv6 in a single call. The AAAA half came back empty, and resolved failed the whole lookup rather than returning just the IPv4 address.
The Mac's resolver queries A and AAAA separately and uses whichever comes back, which is why it never noticed.
The fix
I gave every CNAME target an AAAA record, so both halves of the lookup succeed. In each Pi-hole, under Settings → Local DNS → DNS Records, I added a ULA address for each homelab host that services are CNAMEd to. For example, proxy.my-home-domain → fd12:3456:789a:1::80. It has to go on both Pi-holes, because resolved will happily ask either one.
Use stable addresses: I gave each host a static ULA. The MAC-derived EUI-64 SLAAC address would also work, but not the temporary privacy addresses, which rotate.
Then flush and check:
resolvectl flush-caches
getent ahosts unifi.my-home-domaingetent returned addresses, and Firefox and Chromium both opened the controller straight away. With that sorted, I turned IPv6 back on for the client.
There were two other ways out, if adding AAAA records doesn't suit you:
- Replace each CNAME with a plain A record pointing at the HAProxy box.
- Change
nsswitch.conftohosts: mymachines files myhostname dns, so glibc's classic DNS client handles A and AAAA separately. This is a per-machine hack, and a package update may undo it.
Lessons learned
The big one: dig working proves very little on a systemd-resolved box. dig asks for one record type. Your applications ask glibc for every address family at once, and glibc goes a different way to get them. Always test the way your applications resolve.
The second: if you CNAME local services to a host, make sure that host has both A and AAAA records, on every Pi-hole you run.
Diagnostic cheat sheet
Command | What it tells you |
|---|---|
| What glibc, and so your browsers, actually get. Test with this, not |
| IPv4-only lookup. If this works and |
| Each record type through resolved, separately. |
| Which upstream servers resolved is using, per link. |
| Clears cached negative answers between tests. |
| What a specific Pi-hole returns, bypassing resolved. |
| Whether the |
| The browsers' own view of DNS, plus cache clearing. |