I've been running a local DNS resolver for the last decade. I do that for two reasons: first of all, to bypass censorship and surveillance, and second, to profit from the essentially instantaneous answers of a local DNS cache. The latter point is becoming increasingly important in the last years, since modern websites tend to invoke links to dozens of other domains all of which have to be resolved. A satisfactory web experience thus requires, first of all, a low-latency connection to the DNS server we use.

To specify “low”, let's have a look at the typical connections we have at home. With my vanilla ADSL2+, the latency to the fastest DNS servers around amounts to 8 ms for my desktop, which is connected to the router via a GB switch, or 12 ms for all devices connected via WIFI (802.11g). These values are not too bad, but not what I'd associate with “low latency”. At work, for example, we use a dedicated DNS server available in the intranet with a latency of 0.3 ms. Now we're talking.

I have the same speed at home since 2009, when I started using pdnsd. Unfortunately, the development of this caching DNS proxy has stopped 2012. In addition, DNSSEC was initiated 2010 and is now an indispensable part of the modern internet. To keep up with this development, I hence needed a local recursive DNS resolver that not only caches, but also validates. The article in c't 12/2017 about the validating, recursive and caching DNS resolver Unbound thus came just in time.

The setup provided by c't applied to Ubuntu, and proved to be incomplete anyway (see the comments at the end of the article). With the help of the Archwiki and Calomel, I came up with the following configuration that works as desired on Archlinux. On Debian or Fedora/CentOS, some of the initial steps may be not be necessary.

We first install unbound

pacman -S unbound

and enable the service

systemctl enable unbound.service

We need to edit the service unit, and do that by issuing

systemctl edit unbound.service

to create a drop-in_snippet. The command above automatically opens your $EDITOR, i.e., in my case vim. The content of the snippet should be:

ExecStartPre=sudo -u unbound /usr/bin/unbound-anchor -a /etc/unbound/root.key

After saving the file, give it a meaningful name:

cd /etc/systemd/system/unbound.service.d
mv override.conf update_rootkey.conf

We can now turn to the configuration of Unbound. Replace the default configuration file /etc/unbound/unbound.conf by a file with the following content:

# Unbound configuration file
# See the unbound.conf(5) man page.
# See /etc/unbound/unbound.conf.example for a commented
# reference config file.
# The following line includes additional configuration files from the
# /etc/unbound/unbound.conf.d directory.

include: "/etc/unbound/unbound.conf.d/*.conf"

Next, we create this directory:

mkdir /etc/unbound/unbound.conf.d

Let's put the following four files in this directory:


## Basic configuration
        interface: ::0
                access-control: ::1 allow
                access-control: 2001:DB8:: allow
                # Beispiel f. ULA
                # access-control: fd00:aaaa:bbbb::/64 allow
                access-control: allow
                verbosity: 1

          name: "."
          # hopefully free of censoring and logging, definitely with DNSSEC Support:
          forward-addr:            # dns.as250.net (CCC)
          forward-addr:             # omni.digital.udk-berlin.de (Universität der Künste)
          forward-addr:              # h1768020.stratoserver.net (Digitalcourage e.V.)
          forward-addr:               # dnsc1.dtfh.de (CCC)
          forward-addr:                # ns.n-ix.net (Nürnberger Internet eXchange)
          forward-addr:               # resolver1.ihgip.net (DNS Watch)
          forward-addr:               # resolver2.ihgip.net (DNS Watch)
          forward-addr:             # xiala.net
          forward-addr:             # xiala.net
          forward-addr:             # anycast.censurfridns.dk (UncensoredDNS)
          forward-addr:               # unicast.censurfridns.dk (UncensoredDNS)
          forward-addr:               # dnscache.berlin.ccc.de (CCC)
          forward-addr:              # secondary.server.edv-froehlich.de (OpenNIC)
          forward-addr:              # OpenNIC


## Advanced configuration
verbosity: 1
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes

root-hints: /etc/unbound/root.hints

auto-trust-anchor-file: /etc/unbound/root.key

hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: yes

minimal-responses: yes
prefetch: yes
qname-minimisation: yes
rrset-roundrobin: yes
use-caps-for-id: yes

cache-min-ttl: 3600
cache-max-ttl: 604800

include: /etc/unbound/adservers


## reduce edns packet size to help big udp packets
# over dumb firewalls

edns-buffer-size: 1232
max-udp-size: 1232


# Performance optimization
# `https://www.unbound.net/documentation/howto_optimise.html <https://www.unbound.net/documentation/howto_optimise.html>`_
                # use all CPUs
                num-threads: 8

                # power of 2 close to num-threads
                msg-cache-slabs: 8
                rrset-cache-slabs: 8
                infra-cache-slabs: 8
                key-cache-slabs: 8

                # more cache memory, rrset=msg*2
                rrset-cache-size: 200m
                msg-cache-size: 100m

                # more outgoing connections
                # depends on number of cores: 1024/cores - 50
                outgoing-range: 100

                # Larger socket buffer.  OS may need config.
                so-rcvbuf: 8m
                so-sndbuf: 8m

                # Faster UDP with multithreading (only on Linux).
                so-reuseport: yes

We're almost done now. Two cronjobs in /etc/cron.weekly complete the configuration:


# Updating root hints.

###[ root.hints ]###

curl -sS -L --compressed -o /etc/unbound/root.hints.new `https://www.internic.net/domain/named.cache <https://www.internic.net/domain/named.cache>`_

if ` $? -eq 0  <>`_; then
  mv /etc/unbound/root.hints /etc/unbound/root.hints.bak
  mv /etc/unbound/root.hints.new /etc/unbound/root.hints
  unbound-checkconf >/dev/null
  if ` $? -eq 0  <>`_; then
        rm /etc/unbound/root.hints.bak
        systemctl restart unbound.service
        echo "Warning: Errors in newly downloaded root hints probably due to incomplete download:"
        mv /etc/unbound/root.hints /etc/unbound/root.hints.new
        mv /etc/unbound/root.hints.bak /etc/unbound/root.hints
  echo "Download of unbound root.hints failed!"


# Updating adserver list.

###[ adservers ]###

curl -sS -L --compressed -o /etc/unbound/adservers.new "`https://pgl.yoyo.org/adservers/serverlist.php?hostformat=unbound&showintro=0&mimetype=plaintext <https://pgl.yoyo.org/adservers/serverlist.php?hostformat=unbound&showintro=0&mimetype=plaintext>`_"

if ` $? -eq 0  <>`_; then
  mv /etc/unbound/adservers /etc/unbound/adservers.bak
  mv /etc/unbound/adservers.new /etc/unbound/adservers
  unbound-checkconf >/dev/null
  if ` $? -eq 0  <>`_; then
        rm /etc/unbound/adservers.bak
        systemctl restart unbound.service
        echo "Warning: Errors in newly downloaded adserver list probably due to incomplete download:"
        mv /etc/unbound/adservers /etc/unbound/adservers.new
        mv /etc/unbound/adservers.bak /etc/unbound/adservers
  echo "Download of unbound adservers failed!"

The adserver component is of course optional, but I've found it to be a very efficient way of blocking ads. I'll compare the various possibilities to block ads in a forthcoming post.

For the moment, let's concentrate on the core competences of our new DNS resolver. To do so, we first start it by issuing

systemctl start unbound.service

We can test the resolver on the command line using either dig or its near drop-in replacement drill.

dig +dnssec +multi @localhost debian.org
drill -D @localhost debian.org

What's essential here are the first two lines and the entries in rcode and flags: 'NOERROR' and 'ad', with the latter standing for 'Authenticated Data'. In other words, the DNS response is authentic because it was validated using DNSSEC. The RRSIG blocks provide, among other data, the public key of the domain as explained here.

Let's try that with a domain which is not validated by DNSSEC:

dig +dnssec +multi @localhost archlinux.org

NOERROR, but no 'ad' flag. Quite all right.

And now a domain with a broken/bogus DNSSEC record:

dig +dnssec +multi @localhost dnssec-failed.org

Status: SERVFAIL. Works as well.

Last but no least, let's test the cache of unbound:

for i in $(seq 1 5); do dig www.tuvaluislands.com | grep 'Query time' | awk '{print substr($0, index($0, $2))}'; done
Query time: 746 msec
Query time: 0 msec
Query time: 0 msec
Query time: 0 msec
Query time: 0 msec

Works. 😉

If the command line appears to be too cryptic, we can also test the basic DNSSEC functionality with a browser:

For addresses with broken/bogus DNSSEC records, such as this one, the browser should just display an ERR_NAME_NOT_RESOLVED page. It does? Excellent.

Still...that page is depressing. Let's boost our morale by visiting https://dnssec.vs.uni-due.de/ :


Thank you, Matthäus 😉 .