Running your own DNS with Unbound (and block ads)

Today we will learn how to create your own recursive DNS server using Unbound. We will also see how to add custom records to block ads, as well as internal records for the LAN. Using your own DNS server can improve performance through caching. We will also look at adblocking and encryption.

Table of contents

  1. How Caching Improves Performance
  2. Preparing Unbound
  3. Testing It Out
  4. Adding Local Data
  5. Ad-Blocking
  6. Running a Pixel Server
  7. Self-hosted DNS and Privacy
  8. Encrypted DNS with TLS
  9. Encrypted DNS with DNSCrypt

How caching improves performance

The primary advantage of running a local DNS server is caching. Let's demonstrate this by querying a record twice using my recursive server.
[root@desktop ~]# sudo systemctl restart unbound
[root@desktop ~]# dig etherarp.net | grep "Query time" 
;; Query time: 762 msec
[root@desktop ~]# dig etherarp.net | grep "Query time"
;; Query time: 0 msec

Notice how the query went from taking 0.9s to 0.001s. Why is that?
Initially, to find the answer, we performed recursive DNS resolution, a slow multi-step process. Once we have this answer, we store it in cache (contains recently seen records). I restarted unbound to ensure the cache was empty.

Answering a record from cache is fast and doesn't require any internet traffic.

This is because it doesn't need to the slow process of recursively finding the answer by contacting external nameservers, it remembers the first answer and stores it in cache.

Especially in a lan environment this can greatly reduce the number of external requests and result in a significant speed-up.

Preparing Unbound.

Downloading root hints and setting up trust anchors

$ sudo wget -qO /etc/unbound/root.hints
https://www.internic.net/domain/named.root  
$ sudo systemctl enable unbound-anchor   
$ sudo systemctl start  unbound-anchor

/etc/unbound/unbound.conf configuration file

Testing it out

$ sudo curl -fsSL https://s3.amazonaws.com/etherarp.net/unbound.conf \
>unbound.conf 

$ sudo unbound-checkconf
unbound-checkconf: no errors in /etc/unbound/unbound.conf
$ sudo service unbound start

[ ok ] Starting recursive DNS server: unbound.

$ dig +short CAA etherarp.net @127.0.0.1 
0 issue "comodoca.com"

Adding Local data

[This](https://s3.amazonaws.com/etherarp.net/hosts-to-unbound.sh) is a script I wrote to convert hostfile data into unbound local-data.

It escapes comments, blank lines, and empty local-data lines. It reads either from /etc/hosts or from a file specified by a supplied argument. It's kinda crude but it works

curl -fsSL https://s3.amazonaws.com/etherarp.net/hosts-to-unbound.sh \
| bash -s >lan.conf

local-data:"gateway.example.lan. IN A 10.0.0.1"
local-data-ptr:"10.0.0.1 gateway.example.lan"
local-data:"10-0-0-2.example.lan. IN A 10.0.0.2"
local-data-ptr:"10.0.0.2 10-0-0-2.example.lan"
local-data:"10-0-0-3.example.lan. IN A 10.0.0.3"
local-data-ptr:"10.0.0.3 10-0-0-3.example.lan"
local-data:"10-0-0-4.example.lan. IN A 10.0.0.4"
local-data-ptr:"10.0.0.4 10-0-0-4.example.lan"

Adblocking

Please see my Github repo on this.

There are two ways we can block domains.

Option 1 - Refuse lookup
We create a local-zone for the ad-domain with the option refuse.
Unbound will respond with an NXDOMAIN and the domain will appear not to exist.

Script to generate a records-file

[root@dns ~]# curl -fsSL https://s3.amazonaws.com/etherarp.net/gen-adblock-refuse.sh | bash -s 
[root@dns ~]$ unbound-checkconf<br />
unbound-checkconf: no errors in /etc/unbound/unbound.conf<br />   

Option 2 - Override IP
As an alternative to black-holing them with the refuse option, we can respond to the ad-domains with a custom IP address. This is useful for when you wish to create a pixel server that replies with a blank page to ad requests. You can also use the pixel server to log the origin of the ads etc.


Script to generate a records-file

[root@dns ~]# curl -fSsL https://s3.amazonaws.com/etherarp.net/gen-adblock-refuse.sh | bash -s
local-zone: "000free.us" redirect
local-data: "000free.us A 127.0.0.2
local-zone: "000owamail0.000webhostapp.com" redirect
local-data: "000owamail0.000webhostapp.com A 127.0.0.2"
[root@dns ~]$ unbound-checkconf
unbound-checkconf: no errors in /etc/unbound/unbound.conf

Running a pixel-server

We can run a pixelserver to redirect ad-domains to a blank page and optionally log the blocked requests. To achieve this, we run a small server in lighttpd.

Pixelserver configuration

[root@dns ~]# apt-get install lighttpd
[root@dns ~]# curl -fSsL https://github.com/rohan-molloy/unbound-adblock/blame/master/lighttpd/adblock.conf >/etc/lighttpd/adblock.conf

[root@dns ~]# mkdir -p /var/www/adblock && cd /var/www/adblock<br />
[root@dns adblock]# for ext in png html js; do wget -q  https://github.com/rohan-molloy/unbound-adblock/blame/master/lighttpd/var/www/adblock/default.$ext; done<br />
<br />
[root@dns adblock]# lighttpd -f /etc/lighttpd/adblock.conf

[root@dns adblock]# curl traffic.outbrain.com/foo/bar.js
console.log("Adblock");

Self-hosted DNS and privacy

You may think that running your own DNS server is good for privacy as third-parties like Google won't be able to see your requests; this is indeed correct. However, there is a different privacy concern.

When you self-host DNS, you are retrieving records yourself, and therefore you're exposing your public IP address to a large number of authoritative nameservers. This does not occur when using public recursive servers like Google DNS which retreive records on your behalf; the nameservers see a Google IP address rather than your own.

Let me demonstrate this (192.0.245 is my public ip address):

[user@dns ~]$ dig @8.8.8.8 whoami.akamai.net +short
74.125.41.72
[user@dns ~]$ dig @8.8.4.4 whoami.akamai.net +short 
74.125.41.84
[user@dns ~]$ dig whoami.akamai.net +short 
192.0.2.245

We can mitigate this by telling unbound to forward upstream requests to an additional server (such as Google DNS or Quad9).

Append this to /etc/unbound/unbound.conf to forward to Quad9

forward-zone:
    name: "."
    forward-addr: 9.9.9.9

Encrypted DNS with TLS

DNS traffic is unencrypted and can easily be intercepted in transit.
As a part of an intrusion detection system, some organizations (such as workplaces or campuses) may intercept and analyze every DNS request to look for malicious activity.

This is what intercepted DNS traffic looks like

20:56:19.012015 IP desktop.41440 > google-public-dns-a.google.com.domain: 36098+ [1au] A? etherarp.net. (53)

20:56:19.256672 IP google-public-dns-a.google.com.domain > desktop.41440: 36098$ 1/0/1 A 45.55.106.94 (57)

To ensure the confidentiality of DNS in the last-mile we can forward requests to a TLS capable recursive server. This provides encryption in a manner analogous to how HTTPS protects HTTP.

How to use Quad9's TLS service
Quad9 provides a TLS service on port 853
Let's replace the forward-zone we created earlier with this

forward-zone:
    name:"."
    forward-addr:9.9.9.9@853
    forward-ssl-upstream:yes

Looking in Wireshark, we can see that it performs a TLS handshake, negotiating encryption via the certificate. The application data contains unintelligible data that corresponds to the DNS requests/responds

Encrypted DNS with DNSCrypt

DNSCrypt is an arguably more complicated way to encrypt DNS requests. It uses its own custom protocol (based on TLS) that hasn't been standardized with an RFC. It is not directly supported in Unbound, and instead requires a client side application known as dnscrypt-proxy

The server side implementation of dnscrypt is known as dnscrypt-wrapper.
We will talk about this in another post.

Using dnscrypt-proxy
It is available in most package repositories

A list of resolvers is found in /usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv. This is where the resolver-name option gets its entries from. Be warned, the file may be out-of-date, get the latest from here

Creating a systemd unit
Create /etc/systemd/system/dnscrypt-proxy.service with the following

[Unit]
Description=DNSCrypt Proxy
After=syslog.target network.target

[Service]
Type=forking
ExecStart=/usr/sbin/dnscrypt-proxy --user=nobody --resolver-name=dnscrypt.ca-1 --local-address=127.53.53.53 --daemonize

[Install]
WantedBy=multi-user.target

Then start it with
sudo systemctl daemon-reload && sudo systemctl start dnscrypt-proxy

Check it works by running
dig @127.53.53.53 example.com +short

Then add the following to /etc/unbound/unbound.conf (replacing any of the earlier forward rules)

forward-zone:
    name: "."
    forward-addr:127.53.53.53