DIY Linux Router Part 5: DNS with Unbound

DIY Linux Router Part 5: DNS with Unbound
Photo by engin akyurt / Unsplash

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.

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: [email protected]
  interface: ::[email protected]
  port: 53

  interface: [email protected]
  interface: ::[email protected]
  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: [email protected]#one.one.one.one
  forward-addr: [email protected]#one.one.one.one
  forward-addr: 2606:4700:4700::[email protected]#one.one.one.one
  forward-addr: 2606:4700:4700::[email protected]#one.one.one.one
  forward-addr: [email protected]#dns.google
  forward-addr: [email protected]#dns.google
  forward-addr: 2001:4860:4860::[email protected]#dns.google
  forward-addr: 2001:4860:4860::[email protected]#dns.google

# Allow systemctl reload
remote-control:
  control-enable: yes
  control-use-cert: no
/etc/unbound/unbound.conf.d/unbound.conf

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,
/etc/apparmor.d/local/usr.sbin.unbound

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.

Allow systemctl reload

Lastly I enable the remote control interface on localhost. Without it, running systemctl reload is not possible. My ACME client uses this for example.

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
/usr/local/etc/firewall/unbound.fw

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
/usr/local/etc/firewall/dns-redirect.fw

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
/usr/local/sbin/unbound-ad-blocking.sh

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"
[...]
/etc/unbound/unbound.conf.d/ads.conf

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=
[email protected]
SystemCallErrorNumber=EPERM
/etc/systemd/system/unbound-ad-blocking.service
[Unit]
Description=Update ads blocklist

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
/etc/systemd/system/unbound-ad-blocking.timer

Up next: Wireguard