Pi-hole + Unbound: Self-Hosted Recursive DNS
The most surprising thing about running your own recursive DNS resolver is how much less you’re actually doing, not more.
Let’s see it in action. Imagine your browser wants to resolve example.com.
- Browser: "Hey, what’s the IP for
example.com?" - Pi-hole: "Okay, I’ll ask my upstream resolver."
- Unbound (running on the same machine as Pi-hole): "Hmm, I don’t have that cached. I’ll ask the root servers."
- Unbound to
.(root) server: "Where can I find.com?" - Root server to Unbound: "Here’s the IP of a
.comname server." - Unbound to
.comname server: "Where can I findexample.com?" .comserver to Unbound: "Here’s the IP ofexample.com’s authoritative name server."- Unbound to
example.com’s authoritative server: "What’s the IP forexample.com?" - Authoritative server to Unbound: "It’s 93.184.216.34."
- Unbound caches
93.184.216.34forexample.comand returns it to Pi-hole. - Pi-hole caches it and returns it to your browser.
This is the "recursive" part: Unbound does all the legwork, following the chain of delegation from the root servers down to the authoritative server for the domain. Pi-hole’s job is simply to block ads by checking its blocklists before it even asks Unbound to resolve anything. If it’s blocked, Pi-hole returns 0.0.0.0 immediately. If not, it forwards the request to Unbound.
This setup solves a few problems. First, privacy: you’re not sending your DNS queries to Google (8.8.8.8) or Cloudflare (1.1.1.1), which can log and analyze your browsing habits. Second, security: Unbound can be configured to use DNSSEC for authenticated denial of existence and validated responses, ensuring you’re talking to the real server. Third, speed (potentially): by caching frequently accessed domains locally, Unbound can serve them faster than fetching them from external resolvers every time.
The core configuration for Unbound typically lives in /etc/unbound/unbound.conf. Here’s a minimal example for running it alongside Pi-hole, assuming both are on the same server (e.g., 192.168.1.10):
server:
verbosity: 1
interface: 127.0.0.1 # Listen on localhost for Pi-hole
interface: 192.168.1.10 # Listen on the server's LAN IP for other devices
port: 53 # Standard DNS port
access-control: 127.0.0.0/8 allow # Allow localhost
access-control: 192.168.1.0/24 allow # Allow your LAN subnet
do-ip4: yes
do-ip6: no # Disable IPv6 if not used
do-udp: yes
do-tcp: yes
hide-identity: yes
hide-version: yes
harden-dnssec-stripped: yes
use-caps-for-id: yes
cache-min-ttl: 3600 # Keep records for at least 1 hour
cache-max-ttl: 86400 # Keep records for at most 24 hours
prefetch: yes # Proactively refresh expired records
prefetch-key: yes # Proactively refresh DNSSEC keys
num-threads: 4 # Adjust based on your CPU cores
so-rcvbuf: 4m # Increase buffer size
so-sndbuf: 4m # Increase buffer size
logfile: "/var/log/unbound/unbound.log" # Ensure this directory exists and unbound has write permissions
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
control-key-file: "/etc/unbound/unbound.key"
control-cert-file: "/etc/unbound/unbound.pem"
Make sure to create the log directory (sudo mkdir /var/log/unbound) and set ownership (sudo chown unbound:unbound /var/log/unbound). You’ll also need to generate SSL certificates for remote-control if you plan to use it.
In Pi-hole’s web interface, go to Settings -> DNS and uncheck all public DNS servers. Then, under "Upstream DNS Servers," enter the IP address of your Unbound server (e.g., 192.168.1.10#53 or 127.0.0.1#53 if Pi-hole and Unbound are on the same machine) and check the "Listen on all interfaces" box if Unbound is configured to listen on its LAN IP.
The prefetch and prefetch-key options are crucial for performance. When a DNS record is about to expire from the cache, Unbound will proactively fetch a fresh copy before it’s actually needed. This means the next time Pi-hole asks for that record, Unbound can serve it instantly from its cache, avoiding the round trip to the upstream servers. This is especially effective for domains with short TTLs (Time To Live) or during periods of high network activity.
The so-rcvbuf and so-sndbuf settings are often overlooked. Increasing the socket receive and send buffer sizes allows Unbound to handle more concurrent queries and responses more efficiently, preventing dropped packets and improving throughput, especially under load. Setting them to 4m (4 megabytes) is a common recommendation for home servers.
Most people don’t realize that Unbound, when configured correctly for recursion, doesn’t actually need to know about the entire DNS hierarchy. It only needs to know the IP addresses of the root DNS servers. From there, it dynamically discovers the IP addresses of the TLD servers (like .com, .org) and then the authoritative servers for the specific domain being queried. This dynamic discovery is what makes the recursive process work without needing a massive, pre-populated database of the entire internet.
Once you have this working, the next logical step is exploring DNS over TLS (DoT) or DNS over HTTPS (DoH) for encrypting your own DNS traffic from your Pi-hole/Unbound server to external DNS providers, or even setting up Unbound to act as a DoT/DoH server for your clients.