DIY Linux Router Part 5: DNS with Unbound
The following is the fifth part of a multipart series describing how I build (software not hardware) my own Linux router from scratch, based on Debian 11.
- Part 1: Hardware
- Part 2: Interfaces, DHCP and VLAN
- Part 3: PPPOE and Routing
- Part 4: Firewall and Port Forwards
- Part 6: WireGuard VPN
- Part 7: WiFi
- Part 8: NetFlow / IPFIX
The next important functions a router offers is DNS resolution. My currently preferred recursive DNS Server is Unbound, which is what we will be using.
Unbound is included the in Debian package repository and can easily be installed with:
apt install unbound
The default configuration can be extended by creating .conf files inside the /etc/unbound/unbound.conf.d/
directory.
I will go over the important parts of the configuration bellow. A full explanation of every option can be found in the documentation: https://www.nlnetlabs.nl/documentation/unbound/unbound.conf/
My main configuration looks like this:
server:
# Logs
verbosity: 1
use-syslog: yes
log-queries: yes
log-replies: yes
log-tag-queryreply: yes
log-servfail: yes
# ACL
access-control: 0.0.0.0/0 allow
access-control: ::/0 allow
# Interfaces / DNS over TLS
interface: 0.0.0.0@53
interface: ::0@53
port: 53
interface: 0.0.0.0@853
interface: ::0@853
tls-port: 853
tls-service-key: "/etc/certificates/sherbers.de.key"
tls-service-pem: "/etc/certificates/sherbers.de.cer"
# Protocols
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
# Hardening
use-caps-for-id: yes
hide-identity: yes
hide-version: yes
prefetch: yes
aggressive-nsec: yes
rrset-roundrobin: yes
tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
harden-glue: yes
harden-dnssec-stripped: yes
# DNS Overwrites
local-zone: sherbers.de typetransparent
local-data: "kibana.sherbers.de A 192.168.144.10"
local-data: "zabbix.sherbers.de A 192.168.144.10"
# Upstream Server
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 1.0.0.1@853#one.one.one.one
forward-addr: 1.1.1.1@853#one.one.one.one
forward-addr: 2606:4700:4700::1111@853#one.one.one.one
forward-addr: 2606:4700:4700::1001@853#one.one.one.one
forward-addr: 8.8.4.4@853#dns.google
forward-addr: 8.8.8.8@853#dns.google
forward-addr: 2001:4860:4860::8888@853#dns.google
forward-addr: 2001:4860:4860::8844@853#dns.google
Logs
I like to enable response logging out of curiosity to see what my clients are doing. I then ingest this logs into Elasticsearch where I can easily filter them. This option creates a lot of log lines. For example, in just one week Unbound logged 550.000 lines.
ACL
With access-control
we control which IPs can actually use the DNS Server. Normally I would only allow my local networks to access Unbound, but recently I added DNS over TLS which allows me to connect my Android mobile phone from anywhere to my Unbound Server. Because of this I have to allow access from the whole internet.
Further down I limit the default DNS port 53 to local networks only and open the DNS over TLS Port 853 to the internet. Anyone who would scan for open Ports 853 could use my Unbound, that is also a reason I log every request to see if this will happen. If I detect abuse in the future I will have to reconsider this.
Interfaces / DNS over TLS
The first block opens the default DNS Port 53 on all interfaces.
The second block opens the DNS over TLS port on all interfaces. 853 is the default port in the specification, but you could use any other port, if your DNS over TLS clients allow you to change it. I don't think Android does. To use DNS over TLS you need valid TLS certificates. I recommend using Let's Encrypt to get free certificates that can be renewed automatically. There are many ACME clients, I like to use acme.sh.
Unbound comes with a strict AppArmor policy that denies reading of most parts of the filesystem, you can either put the certificates inside the Unbound configuration directory or modify the AppArmor profile. Since my certificates are located in /etc/certficates
I opted to modify the AppArmor profile. First create the following file: /etc/apparmor.d/local/usr.sbin.unbound
/etc/certificates/** r,
and then run the following command:
apparmor_parser --replace /etc/apparmor.d/usr.sbin.unbound
The will add our modification to the baseline profile, it will not overwrite the baseline profile. Also make sure that the unbound unix user can read the certificate and private key files.
Protocols
Here I tell Unbound to use both IPv4 and IPv6 and also both UDP and TCP.
Hardening
Here are all the option that the unbound documentation recommends to enable to increase security and privacy. Some of them might already be on by default nowadays. I just read the whole documentation and enabled everything that sounded useful without causing problems.
DNS Overwrites
With the local-zone
and local-data
options it is possible to overwrite DNS replies to the clients. For example I have a Kibana and Zabbix Webinterface running on my Linux server at home. Both DNS records point to Cloudflare. I don't want my connection to go from my PC out to the Internet, through Cloudflare, just to come back to my home and to my Linux server. This just adds unnecessary latency. Using these options I change the DNS records to point directly to my Linux server. The argument typetransparent
means that only the exact DNS record I have in the configuration will be overwritten, all others will work normally.
Upstream Server
Without the follow option, Unbound would not use another recursive DNS resolver and would instead resolve queries by contacting the root DNS servers and authoritative DNS servers directly. This adds unnecessary load (even if it is very little) to these servers and it is recommended to use a public DNS resolver. I use Cloudflare's and Google's public DNS servers. Both are very fast, highly reliable and don't block anything or add ADs on sites that don't exist. To forward every request to these upstream servers we add name: "."
which menas forward everything. They also both offer DNS over TLS. To establish a secure DNS over TLS connection to the upstream servers, you have to include one of the Subject Alternatives Names from the certificates these servers use and append it at the end of the line with a # in front of it. Without this Unbound would accept any valid certificate for any domain.
Firewall
As mentioned above I open Port 53 only on my local networks (and wireguard) and open the DNS over TLS port from everywhere.
#!/bin/bash
iptables -F UNBOUND-INPUT
iptables -X UNBOUND-INPUT
ip6tables -F UNBOUND-INPUT
ip6tables -X UNBOUND-INPUT
iptables -N UNBOUND-INPUT
iptables -A UNBOUND-INPUT -i br0 -p udp --dport 53 -j ACCEPT
iptables -A UNBOUND-INPUT -i br0 -p tcp --dport 53 -j ACCEPT
iptables -A UNBOUND-INPUT -i vlan222 -p udp --dport 53 -j ACCEPT
iptables -A UNBOUND-INPUT -i vlan222 -p tcp --dport 53 -j ACCEPT
iptables -A UNBOUND-INPUT -i wg0 -p udp --dport 53 -j ACCEPT
iptables -A UNBOUND-INPUT -i wg0 -p tcp --dport 53 -j ACCEPT
iptables -A UNBOUND-INPUT -p tcp --dport 853 -j ACCEPT
iptables -A MAIN-INPUT -j UNBOUND-INPUT
ip6tables -N UNBOUND-INPUT
ip6tables -A UNBOUND-INPUT -i br0 -p udp --dport 53 -j ACCEPT
ip6tables -A UNBOUND-INPUT -i br0 -p tcp --dport 53 -j ACCEPT
ip6tables -A UNBOUND-INPUT -i vlan222 -p udp --dport 53 -j ACCEPT
ip6tables -A UNBOUND-INPUT -i vlan222 -p tcp --dport 53 -j ACCEPT
ip6tables -A UNBOUND-INPUT -i wg0 -p udp --dport 53 -j ACCEPT
ip6tables -A UNBOUND-INPUT -i wg0 -p tcp --dport 53 -j ACCEPT
ip6tables -A UNBOUND-INPUT -p tcp --dport 853 -j ACCEPT
ip6tables -A MAIN-INPUT -j UNBOUND-INPUT
I also use the following iptables rules, to stop misbehaving clients to ignore my Unbound server. Every connection that tries to connect to another DNS server on the internet is instead redirected to my local Unbound server. The client has no idea this happens. I already caught my NVIDIA Shield doing this. I don't do it on my guest network. If they want to use their own server they should be able to.
#!/bin/bash
iptables -t nat -F DNS-REDIRECT-PREROUTING
iptables -t nat -X DNS-REDIRECT-PREROUTING
iptables -t nat -N DNS-REDIRECT-PREROUTING
iptables -t nat -A DNS-REDIRECT-PREROUTING -i br0 -p udp --dport 53 -j REDIRECT
iptables -t nat -A DNS-REDIRECT-PREROUTING -i br0 -p tcp --dport 53 -j REDIRECT
iptables -t nat -A MAIN-PREROUTING -j DNS-REDIRECT-PREROUTING
Ad Blocking
This next part is completely optional and up to you. I like to add DNS level Ad blocking to my Unbound server. This is the same thing that Pi-hole does, I even use the same default filter list: https://github.com/StevenBlack/hosts
While using an Ad-block extensions on my PC's web browser is very easy, not all clients support something like this. Using DNS level Ad-blocking also gives me less Ads on my Android phone. And since I'm connecting my phone with DNS over TLS, I even get the Ad-blocking when I'm not at home.
To do this, I download the block list with curl and use grep and awk to create a file that Unbound can read as a configuration.
#!/bin/bash
echo "server:" > /etc/unbound/unbound.conf.d/ads.conf
curl -s -o - "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" | grep -v '^0\.0\.0\.0 0\.0\.0\.0' | grep '^0\.0\.0\.0' | awk '{print "local-zone: \""$2"\" inform_redirect\nlocal-data: \""$2" A 0.0.0.0\""}' >> /etc/unbound/unbound.conf.d/ads.conf
The file then looks like this:
server:
local-zone: "wizhumpgyros.com" inform_redirect
local-data: "wizhumpgyros.com A 0.0.0.0"
local-zone: "coccyxwickimp.com" inform_redirect
local-data: "coccyxwickimp.com A 0.0.0.0"
[...]
The inform_redirect
argument replaces all queries to these domain names with 0.0.0.0 and also logs the redirect. The logging allows me to see what domains have been blocked and what clients tried to contact these domains.
I run the script daily through systemd timer. And as always, the service has as much sandboxing as possible.
[Unit]
Description=service
After=network.target unbound.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/unbound-ad-blocking.sh
ExecStartPost=/bin/systemctl reload unbound
# filesystem access
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ReadWritePaths=/etc/unbound/unbound.conf.d/
# network
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# misc
NoNewPrivileges=true
PrivateUsers=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
ProtectKernelLogs=true
LockPersonality=true
ProtectHostname=true
RemoveIPC=true
RestrictSUIDSGID=true
ProtectClock=true
RestrictNamespaces=true
# capabilities
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
[Unit]
Description=Update ads blocklist
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Up next: Wireguard