Building a DNS over TLS (DoT) server
WARNING: This article is outdated and has been superseded by Configuring Unbound for Downstream DoT.
Since Unbound 1.6.7 there's a better way than the one described here
I previously posted some documentation on how to build a DNS over HTTPS (DoH) Server running Pi-Hole and/or Unbound.
There's another standard available, however - RFC 7858 DNS over TLS (DoT)
DoT isn't as censorship resistant as DoH (as it's easier to block), but does provide you with additional privacy. It also has the advantage of being natively supported in Android Pie (9), so can be used to regain control of your queries without needing to run a dedicated app link Intra, with all the issues that might entail.
In this documentation we're going to trivially build and place queries against a DoT server.
Initial Setup
This documentation assumes you've already built a DoH server, in which case we're simply bolting additional functionality on.
If that's not the case, you need to do something approaching the following sections as a minimum (if you don't want to setup DoH that's fine)
- Install Nginx
- Unbound install, or
- Pi-Hole install - alongside unbound or instead of unbound
Essentially where you want to end up is that you have Nginx running (and configured with a cert, LetsEncrypt or otherwise) and a DNS stack running on your server.
Proxying DoT
All we're going to do is use NGinx's Stream module to terminate the SSL connection and pass plain TCP over loopback to the DNS stack
So, we need to edit /etc/nginx/nginx.conf and append the following to the end (remembering to replace the SSL certificate path with the details of your cert)
stream {
server {
listen *:853 ssl;
proxy_pass 127.0.0.1:53;
proxy_connect_timeout 1s;
preread_timeout 2s;
}
ssl_certificate /etc/letsencrypt/live/dns.bentasker.co.uk/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dns.bentasker.co.uk/privkey.pem;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# If you're new enough to support DoT you're new enough not to support old broken ciphers
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256;
ssl_session_cache shared:DoT:10m;
log_format dot '$remote_addr\t-\t-\t[$time_local]\t$ssl_protocol\t'
'$ssl_session_reused\t$ssl_cipher\t$ssl_server_name\t$status\t'
'$bytes_sent\t$bytes_received';
access_log /var/log/nginx/dot.log dot;
}
What we're doing here is more or less boilerplate, we're binding a server to port 853 (the DoT port) using SSL/TLS with a fairly restricted set of ciphers.
We've also enabled TLSv1.3 - keep in mind it needs a fairly recent Nginx for that to work, so if you don't have the means to use that, just enable TLSV1.2 for now
Now we can reload Nginx and verify it's bound the the appropriate port
systemctl reload nginx
netstat -lnp | grep nginx
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 6009/nginx: master
tcp 0 0 0.0.0.0:853 0.0.0.0:* LISTEN 6009/nginx: master
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 6009/nginx: master
tcp6 0 0 :::853 :::* LISTEN 6009/nginx: master
We also need to punch a hole in the firewall so that clients can use the service
iptables -I INPUT -p tcp --dport 853 -j ACCEPT
iptables-save > /etc/iptables/rules.v4
Testing
We should now be in a position where we can place some test queries against the server.
At time of writing, this is easier said than done, as although (at a technical level) DoT is a saner solution that DoT it really suffers from a lack of adoption and lack of available tooling.
If you've got an Android Pie device, then support is baked in, so you can configure your DNS resolver on your device and it should just work.
Your best bet if it's available for your system is to install Stubby, but failing that clone down something like phpdnstls
$ php dnstls.php www.google.com dns.bentasker.co.uk A
www.google.com has address 216.58.210.196
We can see the connection at the far end in /var/log/nginx/dot.log
86.138.218.68 - - [13/Jun/2019:14:25:43 +0000] TLSv1.2 . ECDHE-RSA-AES256-GCM-SHA384 dns.bentasker.co.uk 200 50 34
You can also test at https://getdnsapi.net/query/ using the following config
- Domain name: A name to query (e.g. www.google.com)
- Extensions: return_call_reporting
- Transport Order: TLS
- TLS Resolver IP: The IP of your DoT server
- TLS Auth name: The name on your SSL Certificate
Cert status is under "call_reporting"
[
"tls_auth_status": <bindata of "Success">,
"tls_peer_cert": <bindata of 0x3082055d30820445a003020102021203...>,
"tls_version": <bindata of "TLSv1.2">,
]
and result is "just_address_answers"
[
{
"address_data": <bindata for 216.58.208.164>,
"address_type": <bindata of "IPv4">
}
],
Downstream Unbound Config
Assuming you're running a recent enough version of Unbound on a downstream server (for example on your LAN), it's possible to configure it to use DoT for upstream connections, It should be noted, though, that at time of writing (Unbound v1.9.1) Unbound cannot currently re-use TCP/TLS connections so will open a new one for every query that needs to be sent upstream).
There are two main steps to this, the first is to provide it with the path to your OS's cert bundle so that it validates certs. We do this by adding tls-cert-bundle to the server section of /etc/unbound/unbound.conf. The path to the bundle differs depending on your OS (you'll also need to have installed the package ca-certificates)
Debian
tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt
CentOS
tls-cert-bundle: /etc/pki/tls/certs/ca-bundle.crt
The next thing to do is to tell Unbound to use our DNS server as an upstream by setting a forward zone for "."
forward-zone:
name: "."
forward-tls-upstream: yes
forward-first: yes
forward-addr: 1.2.3.4@853#dns.bentasker.co.uk
Where the format of forward-addr is [dns server ip]@[port]#[name to check on certificate]
We set forward-first to yes so that Unbound will fallback to resolving names itself if the TLS connection fails. This is a silent failure though and will lead to plaintext queries hitting the wire without notice if it triggers, so you need to think carefully about whether you actually want that set (I've removed it from my server).
Alternatively, you can set multiple forwarding addresses, for example this would spread our queries across Google and our own server
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 1.2.3.4@853#dns.bentasker.co.uk
forward-addr: 8.8.8.8@853#dns.google.com
However, this has a downside. Unbound spreads queries across forwarders initially, but then starts to prefer the fastest responding server (which will normally be Google rather than your own one). So you'll start finding your queries no longer get the benefit of your remote Pi-Hole. If your intention is simply to hide your queries from your ISP, though, this is fine.
Then just reload Unbound
systemctl reload unbound
Job Done. Assuming you followed the earlier DoH article too, you should now have a DNS resolver running Pi-Hole that can be used for DoT by Android Pie devices and DoH for (mostly) everything else.