What?
29 |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.
30 |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.
31 |Why should I trust you to run this?
32 |You shouldn't, so don't. Keep reading and go set your own up!
33 |Setting up a DoH Proxy
34 |In Brief
35 |-
36 |
- 37 | Procure and harden a VPS. 38 | 39 |
- 40 | Point a domain at the VPS. 41 | 42 |
- 43 | Install nginx and an SSL certificate. 44 | 45 |
- 46 | Run the nginx config below. 47 | 48 |
- 49 | ??? 50 | 51 |
-
52 |
ProfitGive back to the community and foster a free and open internet 53 |
54 |
Server Configuration
56 |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.
57 |58 | 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. 59 |
60 |Domain & SSL Setup
61 |62 | 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. 63 |
64 |65 | 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: 66 |
67 |sudo apt-get update
68 | sudo apt-get install software-properties-common
69 | sudo add-apt-repository universe
70 | sudo add-apt-repository ppa:certbot/certbot
71 | sudo apt-get update
72 | sudo apt-get install certbot python-certbot-nginx
73 | sudo certbot certonly --nginx # actual cert generation
74 |
75 | 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
).
76 |
78 | 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.
79 |
Nginx Configuration
82 |Diffie-Helman Prime Generation
83 |We want a bullet-proof SSL setup, so get started by generating fresh Diffie-Helman primes:
84 |sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096
85 | This will take a while (the lowest grade lightsail instance takes nearly an hour); open a second SSH session and let's keep going.
86 |Static Site
87 |88 | 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). 89 |
90 |
91 | sudo mkdir /srv/proxy_static
92 | sudo vim /srv/proxy_static/index.html # set the page contents
93 | sudo chmod 644 /srv/proxy_static/index.html
94 | Base Server Configuration
95 |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).
##
97 | # Individual DoH server entries, one server per resolver.
98 | # These establish proxy ports that the upstream resolvers
99 | # can be reached via.
100 | ##
101 | server {
102 | listen 8001 default_server;
103 | server_name _;
104 |
105 | location / {
106 | proxy_pass https://dns.google;
107 | add_header X-Resolved-By $upstream_addr always; # optional debugging header
108 | }
109 | }
110 |
111 | server {
112 | listen 8002 default_server;
113 | server_name _;
114 |
115 | location / {
116 | proxy_pass https://cloudflare-dns.com;
117 | add_header X-Resolved-By $upstream_addr always; # optional debugging header
118 | }
119 | }
120 |
121 | server {
122 | listen 8003 default_server;
123 | server_name _;
124 |
125 | location / {
126 | proxy_pass https://doh.opendns.com;
127 | add_header X-Resolved-By $upstream_addr always; # optional debugging header
128 | }
129 | }
130 |
131 | server {
132 | listen 8004 default_server;
133 | server_name _;
134 |
135 | location / {
136 | proxy_pass https://doh.42l.fr/dns-query;
137 | add_header X-Resolved-By $upstream_addr always; # optional debugging header
138 | }
139 | }
140 |
141 | ##
142 | # Aggregate our resolver proxies into a single upstream
143 | ##
144 | upstream dohproviders {
145 | server 127.0.0.1:8001;
146 | server 127.0.0.1:8002;
147 | server 127.0.0.1:8003;
148 | server 127.0.0.1:8004;
149 | }
150 |
151 | server {
152 | listen [::]:443 ssl http2 ipv6only=on;
153 | listen 443 ssl http2;
154 | server_name _;
155 | root /srv/proxy_static; # Changeme: if you put your static site root elsewhere, change that here
156 |
157 | ##
158 | # SSL Configuration
159 | # Changme: you'll need to change these to reflect your actual cert and key location
160 | ##
161 |
162 | ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
163 | ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
164 |
165 | # not all of these are compatible with all nginx versions
166 | # sourced from https://cipherli.st/
167 | ssl_protocols TLSv1.3 TLSv1.2; # Requires nginx >= 1.13.0 else use TLSv1.2
168 | ssl_prefer_server_ciphers on;
169 | ssl_dhparam /etc/nginx/dhparam.pem;
170 | ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
171 | ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
172 | ssl_session_timeout 10m;
173 | ssl_session_cache shared:SSL:10m;
174 | ssl_session_tickets off; # Requires nginx >= 1.5.9
175 | ssl_stapling on; # Requires nginx >= 1.3.7
176 | ssl_stapling_verify on; # Requires nginx => 1.3.7
177 |
178 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
179 | add_header X-Frame-Options DENY;
180 | add_header X-Content-Type-Options nosniff;
181 | add_header X-XSS-Protection "1; mode=block";
182 |
183 | ##
184 | # Actual DNS endpoint
185 | ##
186 |
187 | location /dns-query {
188 | proxy_pass http://dohproviders;
189 | }
190 |
191 | ##
192 | # Secondary ".well-known" endpoint
193 | ##
194 |
195 | location /.well-known/dns-query {
196 | rewrite ^/\.well-known/(.*) /$1 break;
197 | proxy_pass http://dohproviders;
198 | }
199 |
200 | ##
201 | # Default greeting page for web browsers
202 | ##
203 |
204 | location / {
205 | index index.html;
206 | }
207 | }
208 |
209 | ##
210 | # HTTP => HTTPS redirect
211 | ##
212 |
213 | server {
214 | listen 80 default_server;
215 | server_name _;
216 | return 301 https://$host$request_uri;
217 | }
218 | Whew, that's a big chunk of config. Let's break this down step by step.
219 |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:
225 |sudo nginx -t # run a configuration sanity check
226 | sudo nginx -s reload # send a reload signal to nginx
227 |
228 | Adding Servers
229 |230 | When adding new DNS servers, you'll need to create a server block for it: 231 |
232 |server {
233 | listen 8001 default_server; # make sure ports are unique
234 | server_name _;
235 | location / {
236 | proxy_pass https://your-resolver-url.com;
237 | add_header X-Resolved-By $upstream_addr always;
238 | }
239 | }
240 | And make sure you update your upstream
block with the new address (e.g. server 127.0.0.1:8001;
).
Testing & Final Words
242 |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
).
244 | 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
:
245 |
access_log /var/log/nginx/access.log;
247 | 248 | 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. 249 |
250 |Next Steps
251 | 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. 252 |Here's to a free and open internet!
253 |254 |