DoHProxy.com

This site proxies your DNS-over-HTTPS (DoH) lookups. Use it by setting your DoH client to use https://DoHProxy.com/dns-query.

DNS traffic is currently resolving via:

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

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!