Skip to content

DNS over TLS

ronnylov edited this page Jul 5, 2019 · 34 revisions

Wikipedia describes DNS over TLS like this:

DNS over TLS (DoT) is a security protocol for encrypting and wrapping Domain Name System (DNS) queries and answers via the Transport Layer Security (TLS) protocol. The goal of the method is to increase user privacy and security by preventing eavesdropping and manipulation of DNS data via man-in-the-middle attacks.

Normally DNS queries and answers are not encrypted which makes it possible for ISP, goverments or other people that want to spy to do man-in-the middle attacks, for instance replace or block answers from DNS servers. By using protocols with encryption it becomes much harder to do this. DNS over TLS is one way to do it. DNS over HTTPS is another way.

Stubby is an application that acts as a local DNS Privacy stub resolver (using DNS-over-TLS). Stubby encrypts DNS queries sent from a client machine (desktop or laptop) to a DNS Privacy resolver increasing end user privacy.

Unbound is a validating, recursive, caching DNS resolver. It is designed to be fast and lean and incorporates modern features based on open standards.

We are going to use Stubby in combination with Unbound - Unbound provides a local cache and Stubby manages the upstream TLS connections (since Unbound cannot yet re-use TCP/TLS connections). You can get the same result by combining Stubby with dnsmasq as described here.

Installing Stubby on debian involves compiling from source code and may be a bit complicated for normal users. Combining it with Unbound also involves some configuration. A much easier way to do it is to use precompiled docker images. Matthew Vance has developed a docker solution that sets this configuration up. You can find them on dockerhub too - Stubby and Unbound.

This allows you to run a Stubby for better DNS over TLS support than Unbound provides without losing the performance benefits of having a local caching DNS resolver.

Install Docker and Docker Compose

You need docker and docker-compose to follow this guide. If you have not already installed it you can take a look at our Docker and Docker Compose guide.

Install stubby-docker

First we create the directory structure. I make a hidden directory .docker-compose and then switch to this directory:

$ mkdir .docker-compose
$ cd .docker-compose

If you don't already have git installed you must install it:

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install git

Now we clone the github repository for stubby-docker. It will download the files from github.

$ git clone https://github.com/MatthewVance/stubby-docker.git

It should have created a sub-directory stubby-docker. So whe change to this directory and take a look.

$ cd stubby-docker
$ ls
docker-compose.yml  LICENSE  README.md  stubby  unbound

So the files are there. Let's look at README It say:

Run these containers with the following command:

docker-compose up -d

Next, point your DNS to the IP of your Docker host running the Unbound container.

So we try it:

$ docker-compose up -d
Creating network "stubby-docker_dns" with the default driver
Pulling stubby (mvance/stubby:latest)...
latest: Pulling from mvance/stubby
54f7e8ac135a: Pull complete
66170bcb01a5: Pull complete
5881139a0be6: Pull complete
a03aecab42b3: Pull complete
55c847a40f60: Pull complete
Digest: sha256:e93b3268e8a07b7cce38d22e8b4edfe6cd54b0087edd21692419005e601262c8
Status: Downloaded newer image for mvance/stubby:latest
Pulling unbound (mvance/unbound:1.9.1-stubby)...
1.9.1-stubby: Pulling from mvance/unbound
54f7e8ac135a: Already exists
3eccb92b6ccd: Pull complete
a70020235b32: Pull complete
87c7b21b9491: Pull complete
f8d023a3f8fb: Pull complete
4cdd69e842f9: Pull complete
7fe28bf832ab: Pull complete
Digest: sha256:31154ca953ed07cfd0b1c411f5fe745aae3f29f496691cb6f89c21ec5a408db4
Status: Downloaded newer image for mvance/unbound:1.9.1-stubby
Creating stubby-docker_stubby_1 ... done
Creating stubby-docker_unbound_1 ... error

ERROR: for stubby-docker_unbound_1  Cannot start service unbound: driver failed programming external connectivity on endpoint stubby-docker_unbound_1 (fd86825918aa736aa63edf8392b2c7f745b32ae8e95b621a1d1d1b3c57a8d9ad): Error starting userland proxy: listen udp 0.0.0.0:53: bind: address already in use

ERROR: for unbound  Cannot start service unbound: driver failed programming external connectivity on endpoint stubby-docker_unbound_1 (fd86825918aa736aa63edf8392b2c7f745b32ae8e95b621a1d1d1b3c57a8d9ad): Error starting userland proxy: listen udp 0.0.0.0:53: bind: address already in use
ERROR: Encountered errors while bringing up the project.

It downloads docker images from docker-hub and tried to start the containers. I got an error when starting unbound container because I already have unbound running on this machine. Docker is forwarding the container to localhost and it conflicts with local unbound. I could stop unbound.service but I don't want it to forward to localhost so I edit docker-compose.yml and remove ports section. First make sure container is stopped:

$ docker-compose down

Then edit docker-compose.yml file

$ nano docker-compose.yml

Made it look like this:

version: '3'
services:
  stubby:
    image: "mvance/stubby:latest"
    networks:
     - dns
    restart: unless-stopped
  unbound:
    image: "mvance/unbound:1.9.1-stubby"
    depends_on:
      - "stubby"
    networks:
     - dns
    volumes:
      - ./unbound/a-records.conf:/opt/unbound/etc/unbound/a-records.conf:ro
    restart: unless-stopped

networks:
  dns:

The difference between original is that I removed this:

    ports:
     - "53:53/udp"

These lines tells docker to forward port 53 to localhost and it cause the conflict with my unbound.service. So by removing the lines it won't forward anything to localhost and there won't be any more conflicts.

So we try to start it again (this time it does not need to download containers, already done):

$ docker-compose up -d
Creating network "stubby-docker_dns" with the default driver
Creating stubby-docker_stubby_1 ... done
Creating stubby-docker_unbound_1 ... done

It looks like it started successfully! OK then how to use it? According to the README

Next, point your DNS to the IP of your Docker host running the Unbound container.

How do I know "the IP of your Docker host running the Unbound container"? It is possible to look it up but I want to set a specific known IP-address and to be sure it is used every time. I fiddlered around quite a bit to make this work and ended up creating a dedicated network bridge in docker for DNS servers and set fixed ip addresses for each container. All this is controlled by editing the docker-compose.yml file.

So first stop the containers docker-compose down and edit the docker-compose.yml file until it looks like this:

version: '3'
services:
  stubby:
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1
      - net.ipv6.conf.default.disable_ipv6=1
      - net.ipv6.conf.lo.disable_ipv6=1
      - net.ipv6.conf.eth0.disable_ipv6=1
    image: "mvance/stubby:latest"
    networks:
      dns:
        ipv4_address: 172.28.0.111
    volumes:
      - ./stubby/stubby.yml:/opt/stubby/etc/stubby/stubby.yml:ro
    restart: unless-stopped
  unbound:
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=1
      - net.ipv6.conf.default.disable_ipv6=1
      - net.ipv6.conf.lo.disable_ipv6=1
      - net.ipv6.conf.eth0.disable_ipv6=1
    image: "mvance/unbound:1.9.1-stubby"
    depends_on:
      - "stubby"
    networks:
      dns:
        ipv4_address: 172.28.0.11
    volumes:
      - ./unbound/a-records.conf:/opt/unbound/etc/unbound/a-records.conf:ro
    restart: unless-stopped

networks:
  dns:
    driver: bridge
    ipam:
      driver: default
      config:
      -
        subnet: 172.28.0.0/24
    driver_opts:
      com.docker.network.bridge.name: br_dns

Changes we made:

  • sysctls was added to disable ipv6 inside the containers. Used as tweak to maybe get better performance and maybe less problems if this is used on a server without ipv6 networks enabled.

  • volumes added mount for stubby configuration files in order to customize the setup (to make it possible change DNS host)

  • networks was customized in order to set static IPv4 addresses to each container

NOTE: We need to setup iptables to allow the docker containers on the new br_dns bridge adapter to talk with internet and local services. See firewall setup.

In iptables.sh these lines should be uncommented to allow outgoing traffic from the docker interfaces:

iptables -A fw-interfaces -i docker0 -j ACCEPT
iptables -A fw-interfaces -i br_dns -j ACCEPT

Also these lines in iptables.sh to forward connections to internet from the docker interfaces:

# Docker
iptables -t nat -A POSTROUTING -s 172.17.0.1/16 -o $EXTIF -j MASQUERADE

# Special docker bridge br_dns used for DNS servers and tinyproxy containers
iptables -t nat -A POSTROUTING -s 172.28.0.1/24 -o $EXTIF -j MASQUERADE

Now let's try it. First we go to the directory where we placed the docker-compose.yml file. For example:

$ cd /home/lthn/.docker-compose/dns/stubby-docker

Now we start the docker-compose containers in daemon mode. This will also make them autostart after reboot.

$ docker-compose up -d
Creating network "stubby-docker_dns" with driver "bridge"
Creating stubby-docker_stubby_1 ... done
Creating stubby-docker_unbound_1 ... done

Now we use the drill command (or dig if you prefer that) to ask the DNS server on IP-address 172.28.0.11 (which was the IP address we set up for the unbound docker container) to lookup information about lethean.io. The drill utility can be installed on debian by installing the package ldnsutils.

$  drill @172.28.0.11  lethean.io
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 59466
;; flags: qr rd ra ; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; lethean.io.  IN      A

;; ANSWER SECTION:
lethean.io.     300     IN      A       198.185.159.145
lethean.io.     300     IN      A       198.49.23.144
lethean.io.     300     IN      A       198.49.23.145
lethean.io.     300     IN      A       198.185.159.144

;; AUTHORITY SECTION:

;; ADDITIONAL SECTION:

;; Query time: 351 msec
;; SERVER: 172.28.0.11
;; WHEN: Thu Jun 27 14:30:23 2019
;; MSG SIZE  rcvd: 92

We can try the same command again and see if unbound is using cashe to make repeated requests faster. Use up arrow in terminal to get last command then press enter. Take a look at Query time:

;; Query time: 0 msec

It works!

If we have problems we can check if stubby is working. Stubby is configured to use port 8053 here. Which port a container is using can be checked by docker ps command. We have set the stubby container to use IP address 172.28.0.111 in docker-compose.yml. Unbound is talking with stubby over port 8053 but we can do it manyally directly with drill command making the request on IP and port of stubby:

$ drill @172.28.0.111 -p 8053  lethean.io
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 60074
;; flags: qr rd ra ; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; lethean.io.  IN      A

;; ANSWER SECTION:
lethean.io.     300     IN      A       198.49.23.145
lethean.io.     300     IN      A       198.185.159.144
lethean.io.     300     IN      A       198.185.159.145
lethean.io.     300     IN      A       198.49.23.144

;; AUTHORITY SECTION:

;; ADDITIONAL SECTION:

;; Query time: 70 msec
;; EDNS: version 0; flags: do ; udp: 1452
;; SERVER: 172.28.0.111
;; WHEN: Thu Jun 27 14:40:30 2019
;; MSG SIZE  rcvd: 143

To use stubby-docker as default system DNS resolver we set the nameserver in /etc/resolv.conf to address 172.28.0.11. This will make tinyproxy use this nameserver. To make OpenVPN services use it you can set the dns in the dispatcher.ini file. Another option is using iptables to forward all DNS requests from VPN clients to only use this IP address if you want to make sure only secure DNS is used.

Change DNS provider

There are a number of public DNS providers that support DNS over TLS. You can check which providers support DNS over TLS on Wikipedia. In this example we want to use CleanBrowsing Security Filter to get a basic protection against phishing, malware and malicious domains. They also have Family Filter and Adult Filter options if you want to give your VPN clients additional protection options. Another provider that have malware proteqtion is Quad9.

To change stubby configuration we need to edit the stybby.yml file:

$ sudo nano /home/lthn/.docker-compose/stubby-docker/stubby/stubby.yml

The default configuration in stubby-docker setup is set to use cloudflare DNS over TLS servers, both ipv4 and ipv6. Since we have disabled ipv6 in the docker-compose.yml we should remove The ipv6 upstreams in stubby.yml. So remove all lines below following comment:

####### IPv6 addresses ######

A quick way to remove a line in nano is Ctrl-K when cursor is placed on the line to be removed.

Then we replace all the lines under IPv4 upstream addresses to use CleanBrowsing Security Filter DNS over TLS:

####### IPv4 addresses ######
## Cleanbrowsing Security Filter 185.228.168.9 and 185.228.169.9
  - address_data: 185.228.169.9
    tls_auth_name: "security-filter-dns.cleanbrowsing.org"
  - address_data: 185.228.168.9
    tls_auth_name: "security-filter-dns.cleanbrowsing.org"

Save the file in nano with Ctrl-O and exit nano with Ctrl-X.

To start using the new settings we need to restart stubby-unbound with docker-compose:

lthn@pl01:~/.docker-compose/stubby-docker/stubby$ docker-compose down
Stopping stubby-docker_unbound_1 ... done
Stopping stubby-docker_stubby_1  ... done
Removing stubby-docker_unbound_1 ... done
Removing stubby-docker_stubby_1  ... done
Removing network stubby-docker_dns
lthn@pl01:~/.docker-compose/stubby-docker/stubby$ docker-compose up -d
Creating network "stubby-docker_dns" with driver "bridge"
Creating stubby-docker_stubby_1 ... done
Creating stubby-docker_unbound_1 ... done

Let's test it:

lthn@pl01:~/.docker-compose/stubby-docker/stubby$ drill lethean.io
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 4166
;; flags: qr rd ra ; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; lethean.io.  IN      A

;; ANSWER SECTION:
lethean.io.     300     IN      A       198.185.159.144
lethean.io.     300     IN      A       198.49.23.145
lethean.io.     300     IN      A       198.185.159.145
lethean.io.     300     IN      A       198.49.23.144

;; AUTHORITY SECTION:

;; ADDITIONAL SECTION:

;; Query time: 791 msec
;; SERVER: 172.28.0.11
;; WHEN: Thu Jun 27 16:04:43 2019
;; MSG SIZE  rcvd: 92

And if we try it again the Query time should decrease to 0 ms when it is cached by unbound.

Nice, it works! Now reboot and check if you can drill it again after rebooting. Everything should work.