DIY Debian Router - Part 3: DNS infrastructure with Unbound
Introduction
This is Part 3 of the DIY Debian Router series. See Part 1 for the series introduction and links to all parts.
DNS resolution is a critical router service, translating domain names to IP addresses for all LAN clients. This part covers the deployment of Unbound, a validating recursive DNS resolver designed for security and performance.
The implementation provides:
- Recursive DNS resolution with root hint traversal (no upstream forwarding)
- DNS-based ad and tracker blocking via curated blocklists
- Local domain resolution for internal hostnames
- Cache optimization for reduced latency
DNS architecture: recursive vs. forwarding
Two common DNS resolver architectures exist:
- Forwarding resolver: Forwards queries to upstream DNS servers (e.g., ISP DNS, 8.8.8.8, 1.1.1.1)
- Recursive resolver: Queries authoritative DNS servers directly, starting from root servers
This implementation uses Unbound as a recursive resolver. You'll find many posts online about the tradeoffs between recursive vs. forwarding. We won't go into that here, feel free to configure Unbound the way you prefer.
One tradeoff to note however, is that this recursive setup will incur slightly higher latency for the first query to a domain (subsequent queries are cached). I'll be monitoring my experience with this setup over time and will look into trying a forwarding resolver in the future if needed.
Preventing DHCP client interference
By default, DHCP clients (such as dhcpcd or NetworkManager) overwrite /etc/resolv.conf with DNS servers provided by the upstream DHCP server. Since the router will run its own DNS resolver, this behavior must be disabled.
For systems using dhcpcd, append the following to /etc/dhcpcd.conf:
This prevents dhcpcd from modifying /etc/resolv.conf. Manually configure /etc/resolv.conf to point to localhost:
Root hints configuration
Recursive resolvers require a list of root DNS servers (the starting point for DNS query traversal). This list is called root hints.
Download the authoritative root hints file from IANA:
This file contains the IPv4 and IPv6 addresses of the 13 root DNS server clusters (a.root-servers.net through m.root-servers.net). The file should be updated periodically (monthly or quarterly) to reflect any changes, though root server addresses are highly stable.
To automate updates, create a systemd timer and service unit:
/etc/systemd/system/unbound-roothints-update.service
[Unit]
Update Unbound root hints file
network-online.target
network-online.target
[Service]
oneshot
/usr/bin/wget -q -O /etc/unbound/root.hints.new
/bin/mv /etc/unbound/root.hints.new /etc/unbound/root.hints
/bin/systemctl reload unbound
root
/etc/systemd/system/unbound-roothints-update.timer
[Unit]
Update Unbound root hints monthly
unbound-roothints-update.service
[Timer]
monthly
true
[Install]
timers.target
Enable and start the timer:
Verify timer status:
DNS blocklist integration
DNS-based blocking (ad/tracker blocking) is implemented by configuring Unbound to return NXDOMAIN or a null address for blocklisted domains.
Blocklist update script
The following script downloads and updates a curated DNS blocklist from OISD, a well-maintained, low-false-positive blocklist.
/etc/unbound/unbound_blocklist_update.sh
Expand to view script
#!/bin/bash
if ! ; then
fi
if ! ; then
||
fi
Make the script executable:
Run the script to download the initial blocklist:
To automate updates, create a systemd weekly timer and service unit:
/etc/systemd/system/unbound-blocklist-update.service
[Unit]
Update Unbound DNS blocklist
network-online.target
network-online.target
[Service]
oneshot
/etc/unbound/unbound_blocklist_update.sh
root
/etc/systemd/system/unbound-blocklist-update.timer
[Unit]
Update Unbound DNS blocklist weekly
unbound-blocklist-update.service
[Timer]
Sun *-*-* 04:00:00
true
[Install]
timers.target
Enable and start the timer:
Verify timer status:
This runs the script every Sunday at 4:00 AM. The script includes error handling to restore the previous blocklist if the update fails or causes Unbound to fail to restart.
To manually trigger an update:
Unbound configuration
The main Unbound configuration file is /etc/unbound/unbound.conf. I've attempted to provide an optimized configuration, with aggressive caching and reasonable security defaults.
/etc/unbound/unbound.conf
Expand to view config
# Logging: verbosity level (0-5, higher is more verbose)
# Network binding: interfaces and protocols to serve
# Access control: permit queries only from LAN and localhost
# Root hints: authoritative list of root DNS servers
# Privacy hardening: prevent resolver fingerprinting
# Security hardening: cache poisoning and downgrade attack mitigation
# DNSSEC validation: enable with automatic trust anchor updates
# Key is already included by unbound.conf.d/root-auto-trust-anchor-file.conf
# auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Performance: threading and slab configuration
# Performance: cache tuning and prefetching
# Performance: memory allocation for caches
# Performance: network buffer tuning
# Rate limiting: prevent DNS amplification attacks
# Local zones: blocklist and LAN transparent zone
# Miscellaneous: allow queries to localhost for integration
# Locally served zones configured for LAN services
Key configuration aspects:
- Network binding: LAN interface and loopback only, explicit access control for LAN subnet
- Privacy: QNAME minimization (RFC 7816), aggressive NSEC caching, hides resolver identity
- Security: Cache poisoning protections (0x20 encoding, glue/referral validation), DNSSEC downgrade prevention, rate limiting (1000 qps)
- DNSSEC: Automatic trust anchor updates (RFC 5011), trust anchor signaling (RFC 8145), sentinel detection (RFC 8509)
- Performance: 4 threads with 8-way cache sharding, aggressive caching (30 min - 24 hour TTL), prefetching, serves stale cache when upstream fails
- Cache sizing: 256 MB RRset, 128 MB message, 128 MB DNSSEC keys, 4 MB negative (scale for >50 devices)
- Network tuning: 8192 outgoing ports, 4 MB socket buffers, SO_REUSEPORT load distribution
- Local zones: Blocklist integration, custom local domain zones, IPv4/IPv6 subnet filtering (DNS rebinding protection)
Finally, start the service:
# Disable unused resolvconf service
# Start and enable Unbound
# Verify status
Testing and verification
Query a public domain:
Query a domain with DNSSEC enabled (response should include ad (authenticated data) flag and RRSIG records):
Test DNSSEC validation failure (expect SERVFAIL as response):
Test blocklist, by querying a known tracker domain (check blocklist for a valid example domains):
Query the same domain twice and compare query times:
|
|
The second query should return in <1 ms (served from cache).
Firewall integration
Unbound should only be accessible from the LAN. Never allow DNS queries from the WAN interface. Part 6 covers nftables rules to enforce this.
Next steps
With DNS resolution working, the router can resolve external domains but clients still lack network configuration (IP addresses, gateway). Part 4 covers DHCPv4 deployment with Kea, providing automatic address assignment and configuration for LAN clients.