When I was looking for something new to build I ended up building a DNS over HTTPS server. This way I can use my Pi-hole server wherever I am, without exposing port 53. I let nginx handle the encryption of the HTTPS connection, send the information to dnsdist for translation to DNS, and let Pi-hole filter the queries using my blocklists.
The following is assumed:
- You have nginx up and running
- You have a subdomain (doh.domain.tld, or dns.domain.tld) with valid certificates (Lets Encrypt, or commercial)
- You have installed dnsdist, but not yet configured it
- You have a Pi-hole server up and running, configured to your wishes
This instruction is based upon this tutorial from nginx.com, which I could not get to work.
https://www.nginx.com/blog/using-nginx-as-dot-doh-gateway
So, that’s why I adopted their configuration to use dnsdist instead of their njs script language.
nginx
The configuration of nginx (saved as dns.domain.nl):
# Proxy Cache storage - so we can cache the DoH response from the upstream
proxy_cache_path /var/run/doh_cache levels=1:2 keys_zone=doh_cache:10m;
server {
listen 80;
server_name dns.domain.nl;
return 301 https://dns.domain.nl/$request_uri;
}
# This virtual server accepts HTTP/2 over HTTPS
server {
listen 443 ssl http2;
server_name dns.domain.nl;
access_log /var/log/nginx/doh.access;
error_log /var/log/nginx/doh.error error;
ssl_certificate /etc/letsencrypt/live/dns.domain.nl/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dns.domain.nl/privkey.pem;
# DoH may use GET or POST requests, Cache both
proxy_cache_methods GET POST;
# Return 404 to all responses, except for those using our published DoH URI
location / {
try_files $uri $uri/ =404;
}
# This is our published DoH URI
location /dns-query {
# Proxy HTTP/1.1, clear the connection header to enable Keep-Alive
proxy_http_version 1.1;
proxy_set_header Connection "";
# Enable Cache, and set the cache_key to include the request_body
proxy_cache doh_cache;
proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body;
# proxy pass to dnsdist
proxy_pass http://127.0.0.1:5300;
}
}
nginx sends an 404 error when you visit the address https://dns.domain.nl/
. It is only a proxy to 127.0.0.1:5300
when data is sent to https://dns.domain.nl/dns-query
.
Check the configuration of nginx
nginx -t
It should give the following output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Restart nginx to load the new configuration:
systemctl restart nginx.service
dnsdist
The (minimal working) configuration of dnsdist (saved as dnsdist.conf)
Note: this is a minimal configuration. No measures have been taken with regard to security or abuse. Consult the documentation for more information.
-- dnsdist configuration file, an example can be found in /usr/share/doc/dnsdist/examples/
-- disable security status polling via DNS
-- setSecurityPollSuffix("")
-- fix up possibly badly truncated answers from pdns 2.9.22
-- truncateTC(true)
-- Answer to only clients from this subnet
setACL("127.0.0.1/8")
-- Define upstream DNS server (Pi-hole)
newServer({address="192.168.2.100", name="Pi-hole", checkName="dc01.domain.nl.", checkInterval=60, mustResolve=true})
-- Create local DOH server listener in DNS over HTTP mode, otherwise the information coming from nginx won't be processed well
addDOHLocal("127.0.0.1:5300", nil, nil, "/dns-query", { reusePort=true })
A few things are important here:
- I’ve set an ACL that allows dnsdist to only answer to queries from the subnet
127.0.0.1/8
.
- I’ve added an upstream (downstream according to dnsdist) DNS server with the IP address
192.168.2.100
. It’s configured with a custom checkName
and checkInterval
. Normally, dnsdist sends a query to a.root-servers.net
every second(!). With this configuration, it checks another server – my domain controller – every 60 seconds.
- I’ve added a DOH listener on the loopback address
127.0.0.1:5300
. This is configured as DNS over HTTP, because nginx takes care of the decryption of the connection.
Check the configuration of dnsdist:
dnsdist --check-config
It should give the following output:
No certificate provided for DoH endpoint 127.0.0.1:5300, running in DNS over HTTP mode instead of DNS over HTTPS
Configuration '/etc/dnsdist/dnsdist.conf' OK!
Restart dnsdist to load the new configuration:
systemctl restart dnsdist.service
To check if dnsdist is listening to 127.0.0.1:5300:
netstat -tapn | grep 5300
It should give the following output:
tcp 0 0 127.0.0.1:5300 0.0.0.0:* LISTEN 4435/dnsdist
Configuring the browser
Now it’s time to configure your browser to use your new DNS over HTTPS server. This website explains how to configure your web browser to use DNS over HTTPS:
https://developers.cloudflare.com/1.1.1.1/dns-over-https/web-browser/
Final inspection
To make sure it’s working properly, we need to inspect the logs. nginx keeps a log of access and error messages. We will look at those logs to see if the information is passed on correctly to dnsdist.
Take a look at the access logs of nginx:
cat /var/log/nginx/doh.access
It should give the following output:
192.168.2.1 - - [09/Aug/2020:11:55:05 +0200] "POST /dns-query HTTP/2.0" 200 107 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:05 +0200] "POST /dns-query HTTP/2.0" 200 107 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:05 +0200] "POST /dns-query HTTP/2.0" 200 122 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:05 +0200] "POST /dns-query HTTP/2.0" 200 102 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:05 +0200] "POST /dns-query HTTP/2.0" 200 125 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:06 +0200] "POST /dns-query HTTP/2.0" 200 102 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:06 +0200] "POST /dns-query HTTP/2.0" 200 122 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:06 +0200] "POST /dns-query HTTP/2.0" 200 125 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:08 +0200] "POST /dns-query HTTP/2.0" 200 112 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:08 +0200] "POST /dns-query HTTP/2.0" 200 112 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:19 +0200] "POST /dns-query HTTP/2.0" 200 140 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:19 +0200] "POST /dns-query HTTP/2.0" 200 152 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:20 +0200] "POST /dns-query HTTP/2.0" 200 175 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:55:20 +0200] "POST /dns-query HTTP/2.0" 200 137 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:56:15 +0200] "POST /dns-query HTTP/2.0" 200 64 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:56:15 +0200] "POST /dns-query HTTP/2.0" 200 64 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:56:15 +0200] "POST /dns-query HTTP/2.0" 200 64 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:56:15 +0200] "POST /dns-query HTTP/2.0" 200 64 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:56:21 +0200] "POST /dns-query HTTP/2.0" 200 59 "-" "-"
192.168.2.1 - - [09/Aug/2020:11:56:30 +0200] "POST /dns-query HTTP/2.0" 200 55 "-" "-"
It’s important that you see 200 (after HTTP/2.0) in the logs. This means that nginx was able to pass the information to dnsdist. Otherwise, something has gone wrong.
When something has gone wrong, it should show up in the error logs
cat /var/log/nginx/doh.error
It should (hopefully not) give the following output:
2020/08/09 11:15:26 [error] 946#946: *511 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.2.1, server: dns.domain.nl, request: "POST /dns-query HTTP/2.0", upstream: "http://127.0.0.1:5300/dns-query", host: "dns.domain.nl"