For years my homelab's DNS pointed at someone else's resolver. First my ISP's, then a public one when the ISP's got flaky, then a Pi-hole forwarding to that public one for ad blocking. It worked. It mostly always works, right up until the moment a third party's resolver decides to have a bad afternoon, and then nothing in your house can find anything and you spend twenty minutes convinced your own network is broken.
So I did the thing I had been putting off, and stood up my own recursive resolver. Not a forwarder that asks someone else, an actual recursive resolver that walks the DNS tree itself, from the root servers down. I want to talk about why, how, and the new problems I bought along with the new independence, because there are always new problems.
Forwarding versus recursing
The distinction matters and it is worth being precise about it. A forwarding resolver, which is what most home setups run including a default Pi-hole, takes your query and hands it to an upstream resolver, say 1.1.1.1 or your ISP's box, and relays back whatever that upstream says. You are trusting that upstream to do the actual work and to be honest about the answer.
A recursive resolver does the work itself. It asks a root server "who handles .com", asks that server "who handles example.com", asks that server for the actual record, and caches the lot. No third party sits in the path between you and the authoritative source. You become the thing your ISP used to be.
The trade is straightforward. Forwarding is simpler, often faster on a cache miss because big public resolvers have enormous warm caches, and someone else carries the operational burden. Recursing gives you independence, privacy from any single upstream seeing all your queries, and the slightly smug satisfaction of owning the whole path. For a homelab the burden is small and the independence is the point.
Unbound
I used Unbound, which is the obvious choice and has been for years. It is small, it is fast, it does DNSSEC validation properly, and the configuration is readable. The core of it is unremarkable:
server:
interface: 0.0.0.0
access-control: 192.168.0.0/16 allow
access-control: 127.0.0.0/8 allow
# cache tuning for a small but busy network
cache-min-ttl: 300
cache-max-ttl: 86400
prefetch: yes
prefetch-key: yes
# validate against the root trust anchor
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# don't leak internal queries upstream
private-address: 192.168.0.0/16
private-domain: "home.arpa"
That is genuinely most of it. prefetch: yes is the line that makes it feel fast in daily use, because Unbound refreshes popular records before they expire rather than after, so the records you actually hit are almost always warm in cache. The private-address and private-domain lines are the ones that stop a misconfigured response from rebinding an internal name to an external address, which is a real attack and worth blocking by default.
I kept the Pi-hole, but reversed the arrangement. Pi-hole still does the ad and tracker blocking, because that is what it is good at, but instead of forwarding to a public resolver it now forwards to Unbound on localhost. So the chain is: client, then Pi-hole for blocking, then Unbound for resolution, then the actual internet. Blocking and resolving are separate jobs and I like keeping them separate.
DNSSEC, and the day it bit me
Recursing means I now validate DNSSEC myself, which is the right thing to do, and which introduced a failure mode I had been comfortably insulated from when a public resolver handled it.
DNSSEC validation depends on time. The signatures on records have validity windows, and if your clock is wrong, valid records look expired or not-yet-valid and Unbound, correctly, refuses to return them as bogus. The first time I rebooted the resolver host after a power cut, its clock came up wrong, NTP had not yet corrected it, and Unbound started SERVFAILing perfectly good domains because their signatures appeared to be from the future. From the client's point of view, the entire internet had vanished.
It took me longer than I would like to connect "DNS is down" to "the clock is wrong", because those two facts do not obviously belong together until you have been bitten once. The fix was to make sure NTP synced before Unbound started accepting queries, a simple ordering dependency, and to add a sanity alert if validation failure rates spiked. Owning the whole path means owning this class of problem too. When you forward to someone else, their clock being right is their problem. When you recurse, it is yours.
Was it worth it
Yes, though not for the reason I expected. The independence is nice, the privacy is nice, and not having my house's DNS dependent on a third party's good afternoon is genuinely nice. But the real value was understanding. I had used DNS for twenty years as a thing that mostly works, and running the resolver myself forced me to actually learn how recursion, caching and validation fit together, because when it broke there was nobody else to blame and nobody else to fix it.
The homelab is, in the end, mostly an excuse to learn things by breaking them somewhere it does not matter. This one taught me DNS properly, cost me one confusing evening with a wrong clock, and now resolves every name in the house without asking anyone's permission. I will take that trade.