Pihole With Unbound
Setting up an ad blocking DNS server using PiHole and unbound
I have been running PiHole as an adblocking DNS server for quite a few years. It lives happily in a Podman container on my Tailnet, and runs DNS for the vast majority of my devices and services.
By default, PiHole uses a forwarding DNS service (FTL) - meaning that it checks it’s own internal cache, and if nothing is found it will forward the request to a separate (authoritative) DNS server. In my previous brilliance I had thankfully decided to use the privacy-focused DNS servers run by Quad9, but that is really just outsourcing trust to a third party. I had intended to set up unbound as a recursive DNS server, but apparently that was a project that had not yet seen the light of day. Today was a snow day, so I decided to get it going. I’m certain that I will need the documentation again in the future, so I’m tossing it here for my own reference - and who knows, maybe someone else will pretend to benefit.
Docker-Compose
I use Podman compose via docker-compose.yaml files to manage a bunch of my services, so step one is to get the core file set up
services:
pihole: # Main PiHole image
container_name: pihole
image: pihole/pihole:latest
environment:
TZ: 'America/New_York' # Set to your timezone or leave blank for UTC
PIHOLE_DNS_: unbound # Use unbound as the DNS server
DNSSEC: 'true'
VIRTUAL_HOST: 'pihole'
WEBTHEME: 'default-dark'
DNSMASQ_LISTENING: 'all'
SKIPGRAVITYONBOOT: 'true'
volumes:
- './volumes/pihole/etc..pihole:/etc/pihole'
- './volumes/pihole/etc..dnsmasq.d:/etc/dnsmasq.d'
cap_add:
- NET_ADMIN
restart: unless-stopped
network_mode: service:pihole-tailscale # Use the Tailscale container as network namespace manager
unbound: # Unbound DNS image
image: klutchell/unbound # klutchell/unbound is an unofficial distroless container for unbound
network_mode: service:pihole-tailscale # Use the Tailscale container as network namespace manager
healthcheck:
test: ['CMD', 'drill-hc', '@127.0.0.1', 'dnssec.works']
interval: 30s
timeout: 30s
retries: 3
start_period: 30s
volumes:
- ./unbound_config/:/etc/unbound/custom.conf.d:ro # Mount config file directory as read only
restart: unless-stopped
unbound-redis: # REDIS cache for unbound
image: redis:latest
container_name: redis
hostname: redis
network_mode: service:pihole-tailscale # Use the Tailscale container as network namespace manager
restart: unless-stopped
volumes:
- ./redis:/data
healthcheck:
test: "[ $$(redis-cli ping) = 'PONG' ]"
interval: 30s
timeout: 3s
retries: 3
start_period: 30s
pihole-tailscale: # Tailscale container with reverse proxy and network services
image: tailscale/tailscale:latest
container_name: pihole-tailscale
hostname: pihole
environment:
- TS_AUTHKEY=<obfuscated>
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=false
- TS_HOSTNAME=pihole
volumes:
- ./volumes/pihole-tailscale/var..lib..tailscale:/var/lib/tailscale
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
- SYS_TIME
restart: unless-stopped
Configure unbound
In the unbound_config folder, create a file called ‘custom.conf’. This will enable the unbound server on port 5335, listening on the network localhost. Verbosity defaults to 1 (errors only). Including cachedb will enable a Redis cache to help enable faster lookups.
server:
module-config: "validator cachedb iterator"
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
# Performance and privacy settings
hide-identity: yes
hide-version: yes
minimal-responses: yes
prefetch: yes
# Logging
verbosity: 1
cachedb:
backend: "redis"
redis-server-host: 127.0.0.1
redis-server-port: 6379
redis-expire-records: yes
Settings changes
Once you bring up the compose stack, navigate to the PiHole dashboard and verify the following settings
Settings -> DNS
Upstream DNS Servers
Disable all upstream DNS providers.
Custom DNS Servers
Add 127.0.0.1#5335 to the first line of this text box to use your unbound server. You can add additional upstream DNS providers as desired; FTL will use them in descending order.
Interface Settings
For this setup, we can safely select Permit all origins, as the PiHole container is only listening to the Tailscale network interface.
Advanced DNS settings
- Never forward non-FQDN queries: true
- Never forward reverse lookups for private IP ranges: true
- Use DNSSEC: true
Tailscale
Account settings
We did not cover using ACLs and tags to add container sidecars to your Tailnet, but assuming you did so correctly you should see the pihole container listed among your Tailscale machines. Grab the Tailnet IP address (should be in the 100.0.0.0 range) and head to the DNS tab. Under Nameservers there is a toggle switch to allow Tailscale to override client DNS resolution settings. Click it to enabled, and add the IP address of your new PiHole server.
Client settings
On each client ensure that Tailscale is set up to accept DNS. For Linux clients you can use the following:
~
❯ tailscale set --accept-dns
~
❯ tailscale dns status
=== 'Use Tailscale DNS' status ===
Tailscale DNS: enabled.
Tailscale is configured to handle DNS queries on this device.
Run 'tailscale set --accept-dns=false' to revert to your system default DNS resolver.
=== MagicDNS configuration ===
This is the DNS configuration provided by the coordination server to this device.
MagicDNS: enabled tailnet-wide (suffix = <redacted>.ts.net)
Other devices in your tailnet can reach this device at nixlap.<redacted>.ts.net.
Resolvers (in preference order):
- 100.74.175.8
Split DNS Routes:
- ts.net. -> 199.247.155.53
- ts.net. -> 2620:111:8007::53
Search Domains:
- <redacted>.ts.net
=== System DNS configuration ===
This is the DNS configuration that Tailscale believes your operating system is using.
Tailscale may use this configuration if 'Override Local DNS' is disabled in the admin console,
or if no resolvers are provided by the coordination server.
(reading the system DNS configuration is not supported on this platform)
[this is a preliminary version of this command; the output format may change in the future]
~
❯
Validation
That’s it - you should be able to go to the PiHole dashboard and see DNS queries from any of your enabled devices. If you set unbound verbosity to anything higher than 1, you can run the following to see the results of queries:
nick@gondor:~/services/pihole$ podman compose logs unbound --tail 25
unbound-1 | [1769375428] unbound[1:0] debug: cache memory msg=72299 rrset=123994 infra=25576 val=73911 subnet=0
unbound-1 | [1769375428] unbound[1:0] debug: iterator[module 2] operate: extstate:module_wait_reply event:module_event_noreply
unbound-1 | [1769375428] unbound[1:0] info: iterator operate: query connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] info: processQueryTargets: connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] info: sending query: connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] debug: sending to target: <ubuntu.com.> 2620:2d:4000:1::44#53
unbound-1 | [1769375428] unbound[1:0] debug: cache memory msg=72299 rrset=123994 infra=25576 val=73911 subnet=0
unbound-1 | [1769375428] unbound[1:0] debug: iterator[module 2] operate: extstate:module_wait_reply event:module_event_noreply
unbound-1 | [1769375428] unbound[1:0] info: iterator operate: query connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] info: processQueryTargets: connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] info: sending query: connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] debug: sending to target: <ubuntu.com.> 2620:2d:4000:1::44#53
unbound-1 | [1769375428] unbound[1:0] debug: cache memory msg=72299 rrset=123994 infra=25576 val=73911 subnet=0
unbound-1 | [1769375428] unbound[1:0] debug: iterator[module 2] operate: extstate:module_wait_reply event:module_event_noreply
unbound-1 | [1769375428] unbound[1:0] info: iterator operate: query connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] info: processQueryTargets: connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] info: sending query: connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] debug: sending to target: <ubuntu.com.> 2620:2d:4000:1::43#53
unbound-1 | [1769375428] unbound[1:0] debug: cache memory msg=72299 rrset=123994 infra=25576 val=73911 subnet=0
unbound-1 | [1769375428] unbound[1:0] debug: iterator[module 2] operate: extstate:module_wait_reply event:module_event_noreply
unbound-1 | [1769375428] unbound[1:0] info: iterator operate: query connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] info: processQueryTargets: connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] info: sending query: connectivity-check.ubuntu.com. AAAA IN
unbound-1 | [1769375428] unbound[1:0] debug: sending to target: <ubuntu.com.> 185.125.190.66#53
unbound-1 | [1769375428] unbound[1:0] debug: cache memory msg=72299 rrset=123994 infra=25576 val=73911 subnet=0
nick@gondor:~/services/pihole$
Because of the way caching and recursive DNS work, your initial queries about a given TLD may take up to 1 second (though rarely); subsequent requests will be served in < 0.5ns.