Unbound Plus

As detailed in a previous post, I'm running Unbound as local DNS resolver. I've configured it to use DNS over TLS, and while things were a little shaky with the few available servers supporting this security protocol in the beginning, I didn't need to switch back to plain DNS for at least a year. And I'm not using the commercial providers that have decided to jump on the bandwagon, namely, Google (8.8.8.8), Cloudflare (1.1.1.1), and Quad9 (9.9.9.9). I wouldn't touch them except for testing purposes.

However, my initial motive to run a local resolver was not security or privacy, but latency, and DNS over TLS, being based on TCP instead of UDP as plain DNS, definitely doesn't help with that. In fact, unencrypted queries over UDP are generally way faster than encrypted ones over TCP, but the actual latency can strongly vary depending on the server queried.

Dnsperf is the standard tool for measuring the performance of an authoritative DNS server, but it doesn't support TCP, and the patched version is seriously outdated. Flamethrower is a brand-new alternative looking very promising, but I've got inconsistent results from it (I'm pretty sure that was entirely my fault).

The standard DNS query tools dig (part of bind) and drill (part of ldns) don't support TLS, but kdig (part of knot) supposedly does. An alternative is pydig, which I've used already two years ago to check if an authoritative server offers DNS over TLS, and which turned out to be just as helpful in determining the latency of a list of DNS servers (one IP per line). After updating ('git pull origin master'), I've fed this list (called, let's say, dns-servers.txt) to pydig using

while read p; do ./pydig @$p +dnssec +tls=auth ix.de | grep 'TLS response' | awk '{print substr($0, index($0, $10))}'; done < dns-servers.txt

with an explicit (+) requirement for DNSSEC and TLS (or without for plain DNS).

I've got a few really interesting results this way. For example, Cloudflare is invariably the fastest service available, with a latency of 9 and 60 ms for plain and encrypted UDP queries here at home, respectively. From pdes-net.org, the situation is different: Cloudflare takes 4 and 20 ms, while dnswarden returns results within 1 and 9 ms, respectively. Insanely fast!

This latter service (where the hell did it come from all of a sudden?) is also very competitive compared to Google or Quad9 here at home: all of them require about a 100 ms to answer TLS requests. That seems terribly slow, but it's not as bad as it sounds. First, I've configured Unbound to be a caching resolver, so many, if not most, requests are answered with virtually zero latency. Second, I minimize external requests by making the root zone local – which is also known as the hyperlocal concept.

Due to this added functionality, I've found it necessary to revamp the configuration. All main and auxiliary configuration files of my current Unbound installation are attached below.

Main configuration files

/etc/unbound/...

.../unbound.conf

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

.../unbound.conf.d/01_Basic.conf

server:
        verbosity: 1
        do-ip4: yes
        do-ip6: yes
        do-udp: yes
        do-tcp: yes

        use-syslog: yes
        do-daemonize: no
        username: "unbound"
        directory: "/etc/unbound"

        root-hints: root.hints

        #trust-anchor-file: trusted-key.key
        auto-trust-anchor-file: trusted-key.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

        ## reduce edns packet size to help big udp packets over dumb firewalls
        #edns-buffer-size: 1232
        #max-udp-size: 1232

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

        include: /etc/unbound/adservers

.../unbound.conf.d/02_Forward.conf

server:
          interface: ::0
          interface: 0.0.0.0
          access-control: ::1 allow
          access-control: 2001:DB8:: allow
          #access-control: fd00:aaaa:bbbb::/64 allow
          access-control: 192.168.178.0/16 allow
          verbosity: 1
          ssl-upstream: yes

forward-zone:
# forward-addr format must be ip "@" port number "#" followed by the valid public
# hostname in order for unbound to use the tls-cert-bundle to validate the dns
# server certificate.
          name: "."
          # Servers support DNS over TLS, DNSSEC, and (partly) QNAME minimization
          # see https://dnsprivacy.org/jenkins/job/dnsprivacy-monitoring/

           ### commercial servers for tests
                #forward-addr: 1.1.1.1@853                      #cloudflare-dns.com
                #forward-addr: 8.8.8.8@853                      #dns.google
                #forward-addr: 9.9.9.9@853                      #dns.quad9.net

                ### fully functional (ordered by performance)
                forward-addr: 116.203.70.156@853                #dot1.dnswarden.com
                forward-addr: 116.203.35.255@853                #dot2.dnswarden.com
                #forward-addr: 185.49.141.37@853                #getdnsapi.net
                #forward-addr: 185.95.218.43@853                #dns.digitale-gesellschaft.ch
                #forward-addr: 146.185.167.43@853               #dot.securedns.eu
                #forward-addr: 145.100.185.15@853               #dnsovertls.sinodun.com
                #forward-addr: 145.100.185.16@853               #dnsovertls1.sinodun.com
                #forward-addr: 46.182.19.48@853                 #dns2.digitalcourage.de
                #forward-addr: 80.241.218.68@853                #fdns1.dismail.de
                #forward-addr: 89.233.43.71@853                 #unicast.censurfridns.dk

                ### temporarily (2019/11/05) or permanently broken
                #forward-addr: 145.100.185.17@853               #dnsovertls2.sinodun.com
                #forward-addr: 145.100.185.18@853               #dnsovertls3.sinodun.com
                #forward-addr: 158.64.1.29@853                  #kaitain.restena.lu
                #forward-addr: 199.58.81.218@853                #dns.cmrg.net
                ##forward-addr: 81.187.221.24@853               #dns-tls.bitwiseshift.net
                ##forward-addr: 94.130.110.185@853              #ns1.dnsprivacy.at
                ##forward-addr: 94.130.110.178@853              #ns2.dnsprivacy.at
                #forward-addr: 89.234.186.112@853               #dns.neutopia.org

.../unbound.conf.d/03_Performance.conf

# https://www.unbound.net/documentation/howto_optimise.html
server:
                # use all cores
                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

.../unbound.conf.d/04_Rootzone.conf

# “Hyperlocal“ configuration.
# see https://forum.turris.cz/t/undbound-rfc7706-hyperlocal-concept/8761
# furthermore
# https://forum.kuketz-blog.de/viewtopic.php?f=42&t=3067
# https://tools.ietf.org/html/rfc7706#appendix-A
# https://tools.ietf.org/html/rfc7706#appendix-B.1
# https://www.iana.org/domains/root/servers

auth-zone:
 name: .
 for-downstream: no
 for-upstream: yes
 fallback-enabled: yes
  #master: 198.41.0.4                   # a.root-servers.net
 master: 199.9.14.201                   # b.root-servers.net
 master: 192.33.4.12                    # c.root-servers.net
  #master: 199.7.91.13                  # d.root-servers.net
  #master: 192.203.230.10               # e.root-servers.net
 master: 192.5.5.241                    # f.root-servers.net
 master: 192.112.36.4                   # g.root-servers.net
  #master: 198.97.190.53                # h.root-servers.net
  #master: 192.36.148.17                # i.root-servers.net
  #master: 192.58.128.30                # j.root-servers.net
 master: 193.0.14.129                   # k.root-servers.net
  #master: 199.7.83.42                  # l.root-servers.net
  #master: 202.12.27.33                 # m.root-servers.net
 master: 192.0.47.132                   # xfr.cjr.dns.icann.org
 master: 192.0.32.132                   # xfr.lax.dns.icann.org

 zonefile: "root.zone"

Auxiliary configuration files

/etc/cron.weekly/...

.../adserver_updates

#!/bin/bash
# Updating Unbound resources.
# Place this into e.g. /etc/cron.weekly

###[ 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
  else
        echo "Warning: Errors in newly downloaded adservers file probably due to incomplete download:"
        unbound-checkconf
        mv /etc/unbound/adservers /etc/unbound/adservers.new
        mv /etc/unbound/adservers.bak /etc/unbound/adservers
  fi
else
  echo "Download of unbound adservers failed!"
fi

.../roothint_updates

#!/bin/bash
# Updating Unbound resources.
# Place this into e.g. /etc/cron.weekly


###[ 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
  else
        echo "Warning: Errors in newly downloaded root.hints file probably due to incomplete download:"
        unbound-checkconf
        mv /etc/unbound/root.hints /etc/unbound/root.hints.new
        mv /etc/unbound/root.hints.bak /etc/unbound/root.hints
  fi
else
  echo "Download of unbound root.hints failed!"
fi

/etc/systemd/system/unbound.service.d

I've discarded my custom snippet for systemd to get the DNS anchor. Archlinux does provide the anchor automatically as a dependency of unbound (dnssec-anchors), so why complicate things. For other distributions, however, the snippet may still be useful, so here it is:

[Service]
ExecStartPre=sudo -u /usr/bin/unbound-anchor -a /etc/unbound/trusted-key.key