DNS-over-HTTPS (DoH) System-Wide

DNS, also known as Domain Name System, is the internet-wide service that translates fully qualified hostnames (FQDNs) such as google.com into an IP address.

Back in 1983, when DNS has just been invented, DNS requests and responses were sent over the internet in clear text, and they still are! Unlike other protocols such as HTTP and FTP, DNS never got a security upgrade.

To address this issue Paul Hoffman from ICANN and Patrick McManus from Mozilla proposed this standard, RFC 8484. Oddly enough that somehow reminds me of 19 and 84. Anyway, with DNS over HTTPS the client sends a DNS query via an encrypted HTTP request – not a shocking revelation, doh, given the name of the protocol.

There are two possible ways to send the data to be resolved – via a GET or POST request. Each has its own characteristics and advantages. The proxy we'll be using in this mini tutorial, AdguardTeam's dnsproxy, uses GET.

You can read more about DoH here: https://hacks.mozilla.org/2018/05/a-cartoon-intro-to-dns-over-https/

Is DNS Over HTTPS Secure?

While DoH may not yet be widespread, it is a good and necessary addition to DNS.

It depends on your personal use of the web whether you trust to route your DNS requests through another company. To use DoH system-wide you need to run a local proxy that will forward all DNS queries to a DoH server. To make that happen we need a DoH proxy, and maybe some support tools.


  1. DNSProxy (Windows, macOS, Linux, iOS, Android)

  2. OpenSSL (used to deal with certificates)

  3. dnsstamps.py - Python3 script to generate DNS stamps:
    pip3 install --user dnsstamps


The program is a single-file binary. So, copy the binary file to some place safe, preferably somewhere in your $path. I have Homebrew installed, then to make things simpler I've used /usr/local/bin.

DNSProxy Help

  dnsproxy [OPTIONS]

Application Options:
  -v, --verbose     Verbose output (optional)
  -o, --output=     Path to the log file. If not set, write to stdout.
  -l, --listen=     Listen address (default:
  -p, --port=       Listen port. Zero value disables TCP and UDP listeners (default: 53)
  -h, --https-port= Listen port for DNS-over-HTTPS (default: 0)
  -t, --tls-port=   Listen port for DNS-over-TLS (default: 0)
  -c, --tls-crt=    Path to a file with the certificate chain
  -k, --tls-key=    Path to a file with the private key
  -b, --bootstrap=  Bootstrap DNS for DoH and DoT, can be specified multiple times (default:
  -r, --ratelimit=  Ratelimit (requests per second) (default: 0)
  -z, --cache       If specified, DNS cache is enabled
  -e  --cache-size= Cache size (in bytes). Default: 64k
  -a, --refuse-any  If specified, refuse ANY requests
  -u, --upstream=   An upstream to be used (can be specified multiple times)
  -f, --fallback=   Fallback resolvers to use when regular ones are unavailable, can be specified multiple times
  -s, --all-servers Use parallel queries to speed up resolving by querying all upstream servers simultaneously

Help Options:
  -h, --help        Show this help message
  --version         Print DNS proxy version

Running The Proxy

A simple way to do it would be:

dnsproxy -l <LOCAL_IP> -p <PORT> -u sdns://<UPSTREAM_DNS_STAMP> --all-servers

You can actually have more than one upstream, that is, more than one resolver. Just add them to the command like this:

dnsproxy -l <LOCAL_IP> -p <PORT> -u sdns://<UPSTREAM1_DNS_STAMP> -u sdns://<UPSTREAM3_DNS_STAMP> -u sdns://<UPSTREAM3_DNS_STAMP> --all-servers

So, it will use the first one to resolve the DNS, if it fails it will use the second one, and so on, all in parallel.

Now all we need to run the proxy is the upstream DNS stamp, which is the server's stamps encoded with all the parameters required to connect to a secure DNS server as a single string. Think about stamps s QR code, but for DNS.

At the end of this article there is a large list of DNS servers and their sdns stamps. But you might want to use another, one that you have the FQDN for it but not the DNS Stamp.

If this case, to construct the DNS Stamp we'll use OpenSSL and a Python 3 script called dnsstamps.py. You can also use dnsstamps.py to decode constructed DNS stamps and read it's parameters, such as IP, domain name, and connection configuration.

Alternatively if you don't want to mess with Python or OpenSSL you can try and use DNSCrypt's online tool to create these DNS Stamps, but the DNSSEC info and some hashes are still needed.

DNS Stamps

To encode the server's DNS stamp, first we need to fetch the server's certificate and extract the DNSSEC hash from it. Our test server will be Google's DNS server. Use OpenSSL to accomplish that. macOS users: be sure to call brew's OpenSSL, not the deprecated one that is packaged with the system. Homebrew will install OpenSSL binaries in /usr/local/Cellar/openssl/<VERSION>/bin directory.

An oneliner command you can use to get the server's certificate is:

openssl x509 -in <(openssl s_client -connect SERVER:PORT -prexit 2>/dev/null)> cert.pem
openssl x509 -in <(openssl s_client -connect dns.google:443 -prexit 2>/dev/null)> cert.pem

That command will save the server's certificate in a file called cert.pem.

  root [~] $ cat cert.pem

Now we need to parse the cert.pem file and extract the DNSSEC hash from it. We'll use this hash to construct the sdns stamp. To extract the DNSSEC hash issue a:

openssl asn1parse -in cert.pem -out /dev/stdout -noout -strparse 4 | openssl dgst -sha256

Here is what that command does:

 root [~] $ openssl asn1parse -in cert.pem -out /dev/stdout -noout -strparse 4 | openssl dgst -sha256


This c3342a06f465123be8dc0dd48a132fc50cb15874e1e4722549ccc39c1ebc140a hash is Google's DNSSEC hash!

Now that we have the server's DNSSEC we can use dnsstamps.py to construct the sdns stamp. Let's first take a look at dnsstamps.py's help:

 root [~] $ dnsstamp.py --help
usage: dnsstamp.py  []

positional arguments:
                        The command to execute.

The program's commands are parse, plain, dnscrypt, doh and dot. Note that you can use parse in a constructed sdns stamp to deconstruct it, and see it's variables. But what we want now is to create a DoH dns stamp, so let's check the doh help:

  root [~] $ dnsstamp.py doh --help
usage: dnsstamp.py [-h] [-s] [-l] [-f] [-a ADDRESS] [-t HASHES] -n HOSTNAME -p
                   PATH [-b BOOTSTRAP_IPS]

Create DNS over HTTPS stamp

optional arguments:
  -h, --help            show this help message and exit
  -s, --dnssec          use if DNSSEC is supported (default: not supported)
  -l, --no-logs         use if queries are not logged (default: are logged)
  -f, --no-filter       use if domains are not filtered (default: are
  -a ADDRESS, --address ADDRESS
                        the ip address of the DNS server
  -t HASHES, --hashes HASHES
                        a comma-separated list of tbs certificate hashes
                        (e.g.: 3e1a1a0f6c53f3e97a492d57084b5b9807059ee057ab150
  -n HOSTNAME, --hostname HOSTNAME
                        the server hostname which will also be used as a SNI
                        name (e.g.: doh.example.com)
  -p PATH, --path PATH  the absolute URI path (e.g.: /dns-query)
  -b BOOTSTRAP_IPS, --bootstrap_ips BOOTSTRAP_IPS
                        a comma-separated list of bootstrap ips (e.g.:

Pay special attention to the arguments -s (DNSSEC), -l (no logs), and -f (no dns filtering). Also don't miss the -p argument, as it is the HTTPS path that the server uses to resolve DNS. In most cases that is /dns-query, so, https://dns.google/dns-query is where the "magic" happens.

We also will need the dns server's IP, so ping it:

ping dns.google

  root [~] $ ping dns.google
PING dns.google ( 56 data bytes
64 bytes from icmp_seq=0 ttl=57 time=4.092 ms
--- dns.google ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 4.092/4.092/4.092/0.000 ms

Finally, now that we have these three things; the server's IP, the path in it, and it's DNSSEC hash we get the sdns stamp with:

dnsstamp.py doh -s -l -f -a -n dns.google -p /dns-query -t c3342a06f465123be8dc0dd48a132fc50cb15874e1e4722549ccc39c1ebc140a

Here is the output and the sdns stamp:

  root [~] $ dnsstamp.py doh -s -l -f -a -n dns.google -p /dns-query -t c3342a06f465123be8dc0dd48a132fc50cb15874e1e4722549ccc39c1ebc140a
DoH DNS stamp

No logs: yes
No filter: yes
IP Address:
Hashes: ['c3342a06f465123be8dc0dd48a132fc50cb15874e1e4722549ccc39c1ebc140a']
Hostname: dns.google
Path: /dns-query
Bootstrap IPs: []


At the very bottom of the output you get the sdns stamp. That is the stamp we'll use in the dnsproxy program to create the local DNS proxy!

Running the Proxy Server

We need to run this as root in order to bind the proxy to port 53 (or any port below port 1000). To start the server type this command:

sudo dnsproxy -l -p 53 -u sdns://AgcAAAAAAAAABzguOC44LjggwzQqBvRlEjvo3A3UihMvxQyxWHTh5HIlSczDnB68FAoKZG5zLmdvb2dsZQovZG5zLXF1ZXJ5 --all-servers

You can also set the -v (verbose) flag. The server will pipe the connection's information to stdout. It is useful to debug the whole thing.

sudo dnsproxy -v -l -p 53 -u sdns://AgcAAAAAAAAABzguOC44LjggwzQqBvRlEjvo3A3UihMvxQyxWHTh5HIlSczDnB68FAoKZG5zLmdvb2dsZQovZG5zLXF1ZXJ5 --all-servers

Any of these two commands will start the DoH DNS proxy server using the local IP and binding to the default DNS port 53. The proxy server will forward DNS queries to the upstream, Google's DoH server in this example.

Here is the output of the command above:

  root [~] $ sudo dnsproxy -l -p 53 -u sdns://AgcAAAAAAAAABzguOC44LjggwzQqBvRlEjvo3A3UihMvxQyxWHTh5HIlSczDnB68FAoKZG5zLmdvb2dsZQovZG5zLXF1ZXJ5 --all-servers
2019/08/15 17:53:59 [info] Starting the DNS proxy
2019/08/15 17:53:59 [info] Upstream 0: https://dns.google:443/dns-query
2019/08/15 17:53:59 [info] Starting the DNS proxy server
2019/08/15 17:53:59 [info] Creating the UDP server socket
2019/08/15 17:53:59 [info] Listening to udp://
2019/08/15 17:53:59 [info] Creating the TCP server socket
2019/08/15 17:53:59 [info] Listening to tcp://
2019/08/15 17:53:59 [info] Entering the UDP listener loop on
2019/08/15 17:53:59 [info] Entering the tcp listener loop on

So far so good, the server is up and functional, but the system is still set to use whatever DNS you have previously configured (auto-config?), usually that is your ISPs DNS server IP.

So set your system's DNS to, flush the system's DNS cache and try to access some page (If you do not know how to do that, Google it. It's quite simple). Write down your default IPS's DNS server — or whatever DNS you have configured — just in case you want to revert to it.

Well, job done! Everything should work and the entire system should now be using YOUR local DoH proxy to resolve FQDNs.

But my guess is that you do not want to type this dnsproxy command every time you boot the computer. To work around this issue we can run it as a service. So press CTRL+C to stop the proxy.

Linux Service

Because Linux has a bunch of distros, and each use a slightly different approach on how to do this, I won't be covering the creation of the service under Linux. Search your distro's documentation.

macOS Service

For the Mac we can create a service by crafting a property list and copying it to /Library/LaunchDaemons, then is just a matter of loading this launch daemon plist.

Here is a property list file to get you started:


 root [~] $ cat /Library/LaunchDaemons/com.dnsproxy.doh.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

Note the KeepAlive, RunAtLoad and LaunchOnlyOnce boleans.

  • KeepAlive will keep the daemon running, if by some reason the proxy server crashes it will re-run it automatically.
  • RunAtLoad will run the daemon as soon as you (or the system) load the plist.
  • LaunchOnlyOnce is used to prevent the daemon from running more than once, what would cause problems since it binds to ONE specific port, in this case port 53.

Also, pay attention to the structure of the plist. Each "word" in the dnsproxy command will needs it's own <string></string> tag. Do not type the entire command in one single string tag.

Once you crafted your .plist file move it to the proper folder, then set the correct permissions and ownership for it:

mv com.myservicename.doh.plist /Library/LaunchDaemons
chown root:whell /Library/LaunchDaemons/com.myservicename.doh.plist
chmod 644 /Library/LaunchDaemons/com.myservicename.doh.plist

Finally, load the service with:

sudo launchctl load -w /Library/LaunchDaemons/com.myservicename.doh.plist

Launchctl will load and run the service. It will be running on the background as any other service.

Don't forget to set the system's DNS resolver ( in System Preferences > Network. Flush the DNS cache once more and open another page. If works, you're set! You can check the DNS resolver you are using by accessing the following page http://www.whatsmydnsserver.com.

To stop (unload) the service type:

sudo launchctl unload -w /Library/LaunchDaemons/com.myservicename.doh.plist

Launchctl will set the service to disabled and stop the proxy daemon. Again, set the proper DNS resolver in System Preferences > Network.

Windows Service

Windows users can either use the build-in sc command to create a system service, or use a more encompassing (easy) tool, such as NSSM - the Non-Sucking Service Manager to create the service. Check their page to learn how to do that: http://nssm.cc/usage

Remember to set the service to run as Local system account in the Log on tab, or it might not work.

Some DoH (and DNSCrypt) Public Servers

This is an extensive list of public DNS resolvers maintained by Frank Denis supporting the DNSCrypt and DNS-over-HTTP2 protocols.

Another nice list is curl's github DoH page: https://github.com/curl/curl/wiki/DNS-over-HTTPS.

Warning The lists might include servers that may censor content, servers that don't verify DNSSEC records, and servers that will collect and monetize your queries. Make sure you do not use shady resolvers!

{{ message }}

{{ 'Comments are closed.' | trans }}