From Pi-hole v6 Log Anxiety to DNS Zen: The Real, Slightly Messy Journey to Unbound
- Shannon

- Jan 2
- 6 min read
This post starts the way a lot of my homelab stories start: Everything worked, but my brain refused to accept it due to log files indicating there was a problem. What came about was an interesting journey that I figured a good blog might help others out with.
So picture it: Pi-hole was blocking ads. The internet was fine. Nobody in the house was complaining. And yet Pi-hole v6 kept throwing a message into the logs like it was trying to get my attention with a tiny air horn.
dnsmasq warning: nameserver 1.1.1.1 refused to do a recursive query
It was not a one-off. Rather, it was the steady drip of, "Hey, something is weird, are you sure you trust this?"
If you are reading this because your Pi-hole v6 logs look similar, here is the punchline up front. You did not necessarily misconfigure anything. Pi-hole v6 is simply much more transparent than earlier versions, and public resolvers sometimes refuse certain types of recursive questions. Pi-hole retries, resolution succeeds, but now you see the refusal in the logs.
The longer version is that the noise made me want a setup I could trust without constantly translating logs in my head. That is why I installed unbound.
What follows is the full journey, including the mistakes and the Debian-specific potholes that most guides do not mention. I am including the exact commands, what they told me, which files I changed, and why.
The starting symptoms and the rabbit holes
The first confusing part was the Pi-hole v6 DNS UI itself. I selected upstream DNS servers using the GUI checkboxes and Pi-hole kept auto-populating “custom DNS servers” underneath. That felt backwards if you are used to older Pi-hole versions where custom entries were always manual.
In Pi-hole v6, that behavior is the new normal. The UI is more explicit and it materializes your selections as actual upstream IP addresses. Now, the problem was not that the UI performed that task. The problem was that even with “reasonable” upstream choices, I kept seeing the same log warning.
I tried the normal things. I reduced upstream providers. I tried a single provider. I tried Cloudflare. I still saw refusals.
At some point you stop trying to win checkbox bingo and you ask a better question. Who should be doing recursion on my network? If you want recursion to be predictable and quiet, you run a local recursive resolver.
Enter unbound.
Why unbound, specifically
Unbound is a real recursive DNS resolver. It walks the DNS tree itself, talks to the root servers, and validates DNSSEC properly. It is not a public resolver optimized for scale and policy. It is the thing you run when you want your network’s DNS to be boring.
The end state looks like this:
Clients ask Pi-hole. Pi-hole blocks what it should and forwards everything else to unbound on localhost. Unbound does the recursion and hands back answers. Once this is in place, the “refused to do a recursive query” log spam disappears because Pi-hole is not poking public resolvers with requests they might refuse.
The actual installation, the way it happened
Step 1: Install unbound
Installing is simple.
sudo apt update
sudo apt install unbound -yThen I checked service status.
sudo systemctl status unboundThis is where the fun started. Unbound failed. Instead of guessing, I pulled the real error.
sudo journalctl -u unbound --no-pager -n 50The key message was essentially this: unbound was trying to bind to ::1 port 53 and the address was already in use, thanks to you guessed it...Pi-hole. Pi-hole owns port 53. So unbound could not start because it was trying to act like a normal DNS server on the default port. That was not what I wanted.
Step 2: Create a Pi-hole-specific unbound config (listen on localhost:5335)
I created a dedicated config file for Pi-hole.
sudo nano /etc/unbound/unbound.conf.d/pi-hole.confI used this configuration.
server:
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
root-hints: "/var/lib/unbound/root.hints"
harden-glue: yes
harden-dnssec-stripped: yes
qname-minimisation: yes
prefetch: yes
cache-min-ttl: 3600
cache-max-ttl: 86400
hide-identity: yes
hide-version: yes
verbosity: 0At this point I validated that the config itself was syntactically correct.
sudo unbound-checkconf /etc/unbound/unbound.conf.d/pi-hole.confValidation passed, but unbound still failed.
Step 3: Root hints (the next failure that actually matters)
Unbound needs a root hints file. If the file is missing, unbound will not start, even if the rest of your config is perfect.
Unbound told me exactly that when I validated the config. The file did not exist.
So I created it.
sudo mkdir -p /var/lib/unbound
sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
sudo chown unbound:unbound /var/lib/unbound/root.hintsNow unbound-checkconf was happy! But unbound still failed. This is where Debian packaging quirks show up.
Step 4: The chroot trap and why I disabled it
On Debian-based systems, unbound often runs chrooted by default. That can make paths that look correct during config validation point somewhere else at runtime.
Rather than fight that, I disabled chroot explicitly in my Pi-hole config.
I edited the same file again.
sudo nano /etc/unbound/unbound.conf.d/pi-hole.confAnd added this line under server.
chroot: ""So the top now looked like this.
server:
chroot: ""
interface: 127.0.0.1
port: 5335Unbound still failed.
At this point I had a validated config that should have worked, and a service that stubbornly acted like it was ignoring it.
Step 5: The Debian gotcha that actually fixed everything
This was the turning point. The unbound systemd unit on Debian expects a top-level config file to exist at /etc/unbound/unbound.conf. If it does not exist, unbound logs that it cannot open it and then continues with default configuration settings. Those default settings include binding to port 53 on ::1. Pi-hole owns port 53. So unbound would crash even though my real config lived in unbound.conf.d.
The fix was simply to create /etc/unbound/unbound.conf and include the directory where my real config lived.
sudo nano /etc/unbound/unbound.confContents.
server:
chroot: ""
include: "/etc/unbound/unbound.conf.d/*.conf"Once that file existed, unbound stopped falling back to defaults and started loading my actual config.
Then the service finally came up.
sudo systemctl restart unbound
sudo systemctl status unboundI also verified that it was listening on the correct port.
ss -tulnp | grep unboundI wanted to see 127.0.0.1:5335 and I did not want to see port 53.
Step 6: Test unbound directly
Before involving Pi-hole, I tested unbound on its own.
dig @127.0.0.1 -p 5335 google.comIf you get a normal answer and NOERROR, recursion is working.
Wiring Pi-hole to unbound
This part is almost anticlimactic. In the Pi-hole web UI under Settings, DNS, I did three things:
First, I unchecked all upstream DNS providers.
Second, I set the only custom upstream to: 127.0.0.1#5335
Third, I disabled DNSSEC in Pi-hole. Unbound is doing DNSSEC validation now, so Pi-hole does not need to.
Then I restarted Pi-hole FTL.
sudo systemctl restart pihole-FTLValidation, the satisfying part
This is the part that gives you confidence instead of vibes.
I ran the standard DNSSEC tests that are intentionally designed to fail or succeed.
This one should fail with SERVFAIL.
dig @127.0.0.1 -p 5335 sigfail.verteiltesysteme.netThis one should succeed with NOERROR.
dig @127.0.0.1 -p 5335 sigok.verteiltesysteme.netWhen you see that behavior, you know unbound is validating DNSSEC correctly.
Then I tailed Pi-hole logs for a minute.
pihole -tThe best part was what I did not see anymore. No more “refused to do a recursive query” spam.
The files I changed and why
Here is the simple inventory of what changed during this journey:
I created or edited /etc/unbound/unbound.conf.d/pi-hole.conf because I needed unbound to listen only on localhost and a non-conflicting port, and to behave like a recursive resolver that validates DNSSEC.
I created /etc/unbound/unbound.conf because Debian’s unbound service expects it. Without it, unbound falls back to defaults and tries to bind to port 53, which conflicts with Pi-hole.
I created /var/lib/unbound/root.hints because unbound requires a root hints file to perform recursion from the root.
I changed Pi-hole DNS settings in the web UI so Pi-hole forwards to 127.0.0.1#5335 and does not run DNSSEC itself.
That is it...with all the blood, guts, and goriness along the way (I want to be as transparent as possible as I had to Google-fu my way through this madness).
The takeaway
Pi-hole v6 did not make DNS worse. It made upstream behavior visible.
Now, if you are fine ignoring the log spam, public upstreams can still work. I just did not want to live in “everything works but I do not trust it” mode. That's when I've been bitten badly by something not working right and I'm invariably traveling or something.
Fortunately, unbound fixed that log entry!
SO...Pi-hole blocks. Unbound resolves. DNS becomes boring again.
And boring DNS is exactly what I wanted.




Comments