What?
DNS-over-HTTPS is a protocol for making DNS requests (which are not encrypted) via HTTPS (which is encrypted), keeping your DNS lookups from the prying eyes of your ISP.
However, while your queries are safe from your ISP, it's still possible for DoH providers to keep track of who's looking up what domains -- no matter how vehement they are about privacy, I'd rather have control in my own hands. This proxy setup sends requests to a round-robin-selected list of providers, effectively masking your traffic from any single provider and providing tumbling with other users' data.
Why should I trust you to run this?
You shouldn't, so don't. Keep reading and go set your own up!
Setting up a DoH Proxy
In Brief
- Procure and harden a VPS.
- Point a domain at the VPS.
- Install nginx and an SSL certificate.
- Run the nginx config below.
- ???
-
ProfitGive back to the community and foster a free and open internet
Server Configuration
You'll need a lightweight server to get this set up on. This page runs on a tiny AWS Lightsail instance; I find it's a nice balance between control (you can configure load balancers, static IPs, etc.) and ease of use, but any server you can get root on will do (these instructions are oriented towards Ubuntu 18.04 but are fairly portable). If you go the Lightsail route, you'll need to provision an instance and then attach a static IP to it.
Hardening is a Good Thing™; at the least, disable root and password SSH login. There are plenty of hardening guides on the web so give it a Google. Set your firewall up for the holy trinity of 80/443/22. Update your packages. You know the drill.
Domain & SSL Setup
You'll need a domain pointing at your box; my registrar of choice Hover. Update the domain's DNS records and set the A/AAA record to your server's static IP address.
We'll be using nginx and Let's Encrypt via Certbot, the EFF's easy tool to automate SSL certificate generation and renewal. I've duplicated the instructions for nginx on Ubuntu 18.04 below:
sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot python-certbot-nginx
sudo certbot certonly --nginx # actual cert generation
Take note of the location of the certificate chain and the key; we'll need those for our nginx setup. You may also bump into issues if nginx is already listening; if certbot complains just stop the nginx service and bring it back up afterwards (sudo service nginx stop
).
Once the certificate is generated, it's good practice to test the renewal with sudo certbot renew --dry-run
. Note that depending on your OS, you may need to manually set up a scheduled task to automate renewal; check the instructions for your setup on the Certbot instructions page.
Nginx Configuration
Diffie-Helman Prime Generation
We want a bullet-proof SSL setup, so get started by generating fresh Diffie-Helman primes:
sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096
This will take a while (the lowest grade lightsail instance takes nearly an hour); open a second SSH session and let's keep going.
Static Site
Optionally, create a folder to serve a static page so that your proxy has a web presence (just like this one). Feel free to use the content and styling of this page as a reference (see git repo); it's generally drawn from Better Motherfucking Website and is a single page with just a few lines of styling (this page is licensed under WTFPL).
sudo mkdir /srv/proxy_static
sudo vim /srv/proxy_static/index.html # set the page contents
sudo chmod 644 /srv/proxy_static/index.html
Base Server Configuration
We'll replace the default configuration nginx ships with (/etc/nginx/sites-enabled/default
). Open that file, empty it, and dump the following config in. There are inline comments, and we'll break it down in more detail below (find a raw copy at this site's git repo).
##
# Individual DoH server entries, one server per resolver.
# These establish proxy ports that the upstream resolvers
# can be reached via.
##
server {
listen 8001 default_server;
server_name _;
location / {
proxy_pass https://dns.google;
add_header X-Resolved-By $upstream_addr always; # optional debugging header
}
}
server {
listen 8002 default_server;
server_name _;
location / {
proxy_pass https://cloudflare-dns.com;
add_header X-Resolved-By $upstream_addr always; # optional debugging header
}
}
server {
listen 8003 default_server;
server_name _;
location / {
proxy_pass https://doh.opendns.com;
add_header X-Resolved-By $upstream_addr always; # optional debugging header
}
}
server {
listen 8004 default_server;
server_name _;
location / {
proxy_pass https://doh.42l.fr/dns-query;
add_header X-Resolved-By $upstream_addr always; # optional debugging header
}
}
##
# Aggregate our resolver proxies into a single upstream
##
upstream dohproviders {
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
server 127.0.0.1:8004;
}
server {
listen [::]:443 ssl http2 ipv6only=on;
listen 443 ssl http2;
server_name _;
root /srv/proxy_static; # Changeme: if you put your static site root elsewhere, change that here
##
# SSL Configuration
# Changme: you'll need to change these to reflect your actual cert and key location
##
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# not all of these are compatible with all nginx versions
# sourced from https://cipherli.st/
ssl_protocols TLSv1.3 TLSv1.2; # Requires nginx >= 1.13.0 else use TLSv1.2
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
##
# Actual DNS endpoint
##
location /dns-query {
proxy_pass http://dohproviders;
}
##
# Secondary ".well-known" endpoint
##
location /.well-known/dns-query {
rewrite ^/\.well-known/(.*) /$1 break;
proxy_pass http://dohproviders;
}
##
# Default greeting page for web browsers
##
location / {
index index.html;
}
}
##
# HTTP => HTTPS redirect
##
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
Whew, that's a big chunk of config. Let's break this down step by step.
First, we set up a server for each DoH resolver we're going to route to and set them to listen on a unique port. Essentially, these are proxied portals to those providers. When adding new resolvers, you'll need a new server block for each one. As an added advantage, this allows you to do per-server request rewriting if they don't respond on the semi-standard /dns-query
. These port 800x endpoints are not publically accessible; they're purely for our own internal routing. I also added a X-Resolved-By
header to indicate what resolver actually responded when debugging. It's completely optional and can be removed.
The upstream
block aggregates these endpoints into a single traffic-ready block. As specified, it will route traffic in a round-robin fashion to each resolver/server in turn. Nginx has documentation on the upstream
module that allows for all kinds of interesting options such as weighting, connection-count limiting, and other options that may be of interest if you're routing to smaller resolvers or want to shape your traffic at all.
Finally, we put it all together in our hefty public-facing server block. Make sure you change your ssl_certificate
and ssl_certificate_key
locations to reflect your key location. The sizable chunk of SSL options are mostly sourced from Cipherli.st and are modern best-practices SSL settings (you'll even get an A+ from SSL Labs' test!).
The actual /dns-query
endpoint routes to our upstream provider collection set up above; I also add a .well-known/
endpoint to mirror Cloudflare and because RFC 8615 is cool. Then there's the static page if you have one as well as an HTTP => HTTPS redirect. Make sure you modify the root
directive to point to your static files folder if it's not in /srv/proxy_static
.
You can double check you've made all necessary changes by grepping for Changeme
; there's a marker each place you will or might need to make modifications before going live.
Finally, check your configuration and bring the new config up:
sudo nginx -t # run a configuration sanity check
sudo nginx -s reload # send a reload signal to nginx
Adding Servers
When adding new DNS servers, you'll need to create a server block for it:
server {
listen 8001 default_server; # make sure ports are unique
server_name _;
location / {
proxy_pass https://your-resolver-url.com;
add_header X-Resolved-By $upstream_addr always;
}
}
And make sure you update your upstream
block with the new address (e.g. server 127.0.0.1:8001;
).
Testing & Final Words
For testing, I'd highly recommend Daniel Stenberg's fantastic doh tool. It compiles nearly dependency-free out of the box (just clone and make
; you may need to sudo apt-get install libcurl4 libcurl4-openssl-dev -y
) and provides easy testing (just run ./doh www.example.com https://yourdomain.com/dns-query
).
In the spirit of anonymization, I'd suggest disabling access logging in nginx which can be disabled by commenting out this line in /etc/nginx/nginx.conf
:
access_log /var/log/nginx/access.log;
Curl maintains a list of publicly available DoH resolvers; that's a great place to start populating your upstream resolvers. Note that not all resolvers on this are still live or compatible with this setup; make sure to test before you throw them in your config.
Next Steps
I'm currently looking into options for running this as a standalone script free of Nginx, or possibly via beefier AWS tools. This documentation and site contents are open-source on GitHub; feel free to offer improvements or bring up issues there.Here's to a free and open internet!