Bringing Pi-hole into the network
Why I moved the house off third-party DNS, the UniFi setting that silently blocked my firewall rule, and how the Kids network ended up filtered harder than everyone else.
Why I added it
When I set up the VLANs, DNS on the Kids network went to OpenDNS FamilyShield. It caught the obvious stuff, and I didn't have to maintain a thing. The catch was that nothing stopped a device from using a different DNS server. A curious kid with a settings menu could get round the whole filter in about ten seconds.
I also didn't love that every DNS lookup in the house, on all three networks, was going to a third party. I wanted to see what was being looked up, decide for myself what got blocked, and not have that decision live on someone else's server.
Pi-hole sorts out the second problem completely, and gives me the tools to fix the first, although, as you'll see, I haven't fully closed that one yet. It runs on a Raspberry Pi 5 on Main. This post covers how I got it talking to the other two VLANs, what runs alongside it now, and the mistakes that cost me most of an evening each.
Getting the firewall to let it through
Pi-hole sits at 10.92.30.212 on Main. IoT and Kids needed
to reach it on port 53, and at first they couldn't, because my firewall
blocks everything between networks unless a rule allows it. That's the
whole point of setting it up that way, and it did its job: nothing got
through until I said so.
So I wrote the obvious rule: from the IoT Isolation zone (which IoT and
Kids both sit in) to the Internal zone, TCP and UDP port 53, destination
10.92.30.212, allow. It didn't work. dig timed
out from every device I tried, and the rule's hit counter sat on zero
whatever I threw at it.
I spent a while assuming the rule itself was wrong: wrong port, wrong zone, or shadowed by something above it. It was none of those. Each network in UniFi has its own Isolate Network checkbox, completely separate from the firewall zones, and I'd ticked it when I first created the VLANs. With it on, a network can't reach anything else, and that's enforced before your firewall rules are even checked. My allow rule was fine; it just never got asked. Unticking it on IoT and Kids let the zone rules do their job.
With that off, and one more fix I'll come to further down, the rule
worked. I checked it with dig from a device on each VLAN,
and confirmed each query turned up in Pi-hole's own query log, rather
than just trusting a clean-looking response.
Home Assistant needed rules of its own too. I later locked it down to specific devices instead of leaving it open to the whole zone, and letting Kids reach it is where I hit the same symptom a second time. More on that below.
Setting it up properly
Being reachable isn't the same as being used. Two more things had to be right before any of this did anything.
Pi-hole had to accept queries from other networks. Under Settings → DNS → Interface settings, the default is "Allow only local requests", which quietly ignores anything not on Pi-hole's own subnet. I set it to Permit all origins. Pi-hole's own docs warn that this is dangerous, but that warning is aimed at a Pi-hole sitting directly on the internet with port 53 open. Mine isn't: the UniFi firewall decides who can reach it, so it's the right setting here.
Every VLAN had to be told to use it. I nearly skipped
this, assuming a working firewall rule was the whole job. In UniFi, each
network's DHCP Name Server was set to Auto, which hands out the
gateway's own address, not Pi-hole's. I switched IoT and Kids to Manual
with 10.92.30.212, one network at a time. Then I renewed the
lease on a test device on each network and checked which DNS server it
had picked up. On Linux, nmcli connection down then
up does it without a reboot, and
resolvectl status shows the result.
The real proof isn't a clean dig response. It's the query
turning up in Pi-hole's Query Log against the right client IP. A
dig can succeed from a cached answer or a fallback server
and tell you nothing.
One loose end: UniFi still hands out the gateway as a second DNS server alongside Pi-hole, tied to its "Auto default gateway" setting. I first wrote this off as harmless, on the basis that devices only use the second server if the first stops answering. That turns out not to be reliable. Plenty of phones and laptops will send queries to either server, or to whichever answers first. On most networks that just means the odd ad slipping through. On Kids, it's a gap in the filtering, so it's on the list at the end.
Cutting out the third party
Pi-hole decides what gets blocked, but out of the box it still forwards every allowed query to someone else's resolver. Google, in my case. Having gone to the trouble of running my own DNS, sending every lookup to Google felt like it missed the point.
The fix is Unbound, a recursive resolver. Instead of asking a third party, it looks each domain up itself, starting from the root DNS servers and working down. It's light enough that running it on the same Pi as Pi-hole makes no noticeable difference.
# /etc/unbound/unbound.conf.d/pi-hole.conf
server:
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
edns-buffer-size: 1232
prefetch: yes
private-address: 10.92.10.0/24
private-address: 10.92.20.0/24
private-address: 10.92.30.0/24
The private-address lines are DNS rebinding protection.
They tell Unbound to throw away any answer from the internet that points
at an address inside my network, which stops a dodgy public domain being
used to reach devices on my VLANs. My own local names are answered by
Pi-hole, not Unbound, so they're unaffected. If you're copying this,
Pi-hole's own Unbound guide covers all the private address ranges, not
just mine, which is the safer default.
With Unbound listening on 127.0.0.1 port 5335, I pointed
Pi-hole at it (Settings → DNS → Custom DNS servers:
127.0.0.1#5335) and unticked Google. To prove it was
actually being used, and not just coincidentally still working, I looked
up a domain that doesn't exist and watched Pi-hole's log:
forwarded nonexistent-test-domain-xyz123.com to 127.0.0.1#5335
reply nonexistent-test-domain-xyz123.com is NXDOMAIN
A made-up domain can't be sitting in a cache anywhere, and the log shows Pi-hole handing it to Unbound and Unbound coming back with NXDOMAIN (no such domain). That's about as clean a proof as you get. Unbound has no web interface, which is fine. It's meant to be invisible plumbing behind Pi-hole, and if something's wrong, Pi-hole's dashboard and log are still where I look first.
Filtering Kids harder than everyone else
This was the real point of the exercise: everyone gets a sensible baseline, and Kids gets more.
Pi-hole's Group Management does this without setting anything up per
device. I added the whole Kids subnet, 10.92.20.0/24, as a
client under Group Management → Clients, and created a Kids group for
it. Each blocklist can then be assigned to a group, instead of applying
to every network.
| Who | Blocklists |
|---|---|
| Everyone (Default group) | StevenBlack's unified hosts list: general ads and malware |
| Kids only | someonewhocares.org hosts list (broader ad and tracker blocking), plus a dedicated adult-content blocklist |
I tested it rather than trusting the config: a known adult site loads from Main and is blocked outright from a device on Kids.
I've deliberately stopped there. Every extra list means longer updates, more memory, and a higher chance of a false positive breaking a site I actually use. A few well-chosen lists beat eight overlapping ones.
What went wrong
More than usual, if I'm honest.
The same symptom twice, with two different causes. Once Isolate Network was off, my Pi-hole rule started taking hits, but queries still timed out. The rule's Connection State was set to "Return Traffic" instead of "All", so it only allowed replies to connections that already existed and never allowed a new one. An evening later, I let Kids reach Home Assistant and got exactly the same symptom: the allow rule's hit counter climbing every time I tested, and the connection still timing out. This time the rule was fine. The automatically created return rule for the reply traffic had ended up below a broader block rule, so the replies never made it back. I burned real time looking at Home Assistant itself before I worked that out.
The lesson: if the allow rule is taking hits but the connection still fails, look at the reply traffic and the rule order first. I should have gone straight there the second time, instead of starting from scratch.
A dead end: I tried testing with
homeassistant.local across VLANs. .local
names work over mDNS, which is multicast and doesn't cross VLANs on its
own, whatever your firewall rules say. It cost me a round of confusion
before I remembered that's exactly what VLANs are for.
One thing I haven't nailed down. Since switching to
Unbound, Pi-hole's diagnosis page logs the odd
CONNECTION_ERROR against 127.0.0.1#5335,
roughly every four to five hours, with nothing in Unbound's logs or the
system journal at those times. A forum thread suggested adding
incoming-num-tcp: 25 to Unbound's config for the same
symptom. I tried it, and it's still happening. But nobody in the house
has hit a single failed lookup in weeks of it recurring, so it's
starting to look like a harmless quirk in how Pi-hole hands queries to
Unbound rather than a real fault. I'm keeping an eye on it rather than
calling it closed just because nobody's complained.
What's next
- Blocking DNS to anywhere but Pi-hole. Between the gateway being handed out as a second DNS server and cheap kit that hardcodes its own, devices on IoT and Kids can still skip Pi-hole entirely. A firewall rule blocking outbound port 53 to anywhere except Pi-hole would close both. On Kids especially, I'm running out of reasons not to.
-
Naming devices properly. I've started giving static
devices local DNS records with a suffix per network
(
.main,.kids,.iot), and set up Conditional Forwarding so UniFi fills in the rest by hostname. The Query Log still shows raw IPs for anything I haven't named yet. - The Unbound errors. Watching them until I'm confident they're actually harmless, not just quiet.
- Home Assistant and the rest of IoT. Home Assistant only talks to devices on its own VLAN, which the firewall zones never see, so locking that down properly needs a different approach. That's the next post.
One thing I looked at and deliberately didn't do: running a password manager on the same Pi. It's tempting, since the box is always on, but every VLAN in the house is set up to talk to it for DNS. Putting my most sensitive data on my most reachable box would undo a fair bit of the segmentation work from the last post. If I do it, it's going on its own hardware, reached over Tailscale.
If you've hit the return-rule ordering issue yourself, or found something that actually fixes the Unbound errors, I'd like to hear about it: [email protected]