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.

Download

  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

Install

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


Usage:
  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: 0.0.0.0)
  -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: 8.8.8.8:53)
  -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
-----BEGIN CERTIFICATE-----
MIIIATCCBumgAwIBAgIQBvh1HrF2iDB2UqiMN/IgOTANBgkqhkiG9w0BAQsFADBN
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E
aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwOTIxMDAwMDAwWhcN
MjAwOTI0MTIwMDAwWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
YTERMA8GA1UEBxMIQmVya2VsZXkxDjAMBgNVBAoTBVF1YWQ5MRQwEgYDVQQDDAsq
LnF1YWQ5Lm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANWkVeEG
JU13QD+GzecANP0bKj2p/MM2QR7bIazghl47sAuTKxflC0lFMeD1hEDmU0wZQAiY
ztU3JMNmN497IiAUlwdQojpWhR43lj2nGpoVY4ao/ffIqznDsITVuvrtQNSV3yUV
oHmYI4087tCgldnsWBpYgAAnV18xo145hVXPE0GDMKjeaDPKEjOMEZKNT0SaaBWd
TwUqFB9Xu1UUZw8jxI2p9zwMqERI0MlQDYydSq2Xi9FPGnqhaxBGoa9V8HmKpsOk
JTwZjMIyM/npg+3iweJu45+cqiVX2+0FLHLD2h9DhYaZPLiUH+ry5E7HXVcdzH4P
xxp6CzMX46IwXBUCAwEAAaOCBM0wggTJMB8GA1UdIwQYMBaAFA+AYRyCMWHVLyjn
jUY4tCzhxtniMB0GA1UdDgQWBBSXLF1lL9zq1tcm/bnCoPzGNkPELTCCAY0GA1Ud
EQSCAYQwggGAggsqLnF1YWQ5Lm5ldIIJcXVhZDkubmV0hwQJCQkJhwQJCQkKhwQJ
CQkLhwQJCQkMhwQJCQkNhwQJCQkOhwQJCQkPhwSVcHAJhwSVcHAKhwSVcHALhwSV
cHAMhwSVcHANhwSVcHAOhwSVcHAPhwSVcHBwhxAmIAD+AAAAAAAAAAAAAAAJhxAm
IAD+AAAAAAAAAAAAAAAQhxAmIAD+AAAAAAAAAAAAAAARhxAmIAD+AAAAAAAAAAAA
AAAShxAmIAD+AAAAAAAAAAAAAAAThxAmIAD+AAAAAAAAAAAAAAAUhxAmIAD+AAAA
AAAAAAAAAAAVhxAmIAD+AAAAAAAAAAAAAAD+hxAmIAD+AAAAAAAAAAAA/gAJhxAm
IAD+AAAAAAAAAAAA/gAQhxAmIAD+AAAAAAAAAAAA/gARhxAmIAD+AAAAAAAAAAAA
/gAShxAmIAD+AAAAAAAAAAAA/gAThxAmIAD+AAAAAAAAAAAA/gAUhxAmIAD+AAAA
AAAAAAAA/gAVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwawYDVR0fBGQwYjAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQu
Y29tL3NzY2Etc2hhMi1nNi5jcmwwL6AtoCuGKWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0
LmNvbS9zc2NhLXNoYTItZzYuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAEBMCow
KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EM
AQICMHwGCCsGAQUFBwEBBHAwbjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
aWNlcnQuY29tMEYGCCsGAQUFBzAChjpodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
b20vRGlnaUNlcnRTSEEyU2VjdXJlU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAw
ggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB2AKS5CZC0GFgUh7sTosxncAo8NZgE
+RvfuON3zQ7IDdwQAAABZfmE6IIAAAQDAEcwRQIgcsXdG0gLecu2TPcQ0iOEgOa5
+mC7FlH9qEVYFFpn6w0CIQDN/g1Vv6JIS2Ll1H/p+PGURBFRuALncbGq/v8YdHuV
1QB2AId1v+dZfPiMQ5lfvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABZfmE6WAAAAQD
AEcwRQIhAMCPW6HXNEVuFKIEadGP+Eiu6Cal4XgmEJBbTloSAzx2AiBpr5mhPDZC
7HCQyDcNmoLKN3u+2ljc1rJXrn+XbR3kMQB2ALvZ37wfinG1k5Qjl6qSe0c4V5UK
q1LoGpCWZDaOHtGFAAABZfmE6KoAAAQDAEcwRQIhAPneEWFfc069BqoC5tXIuAsY
X66IMf1jgPCrldZu/mOgAiA9xb/G4XoydfzblhKepZvn8WsFRlA/hfaFbIOp18sZ
KTANBgkqhkiG9w0BAQsFAAOCAQEAf/x8F/gkTfqFHMRtG/HAymzlE8lWZ5l17mF2
DDirmI6zoN/zJBC4+tc95In4CQoU59C1VQv77AoI4UF+q/CFxDPrREGAKc+QMaOh
LfmDRNZplOAAvmU5E5shwgIYBtKWnBCF0KG0tSUTip1O8ZoU8PC3tDcxQ5h8Tuqx
93zlf4osrDAiuOl8x6f5H2TEjDBoyqSbuyzMUbOgOhvXPcEEE2S4noGCSIvrC+Qt
gF0dzIkXdaLgfleafQwhsmUAfxWl98ZrMAMP7hqxoDiziIa02Xjs14s9HHvPya6W
bbpPdTrlyYV8PUU82XyoQaO4di0naAZsTac51L65DjPT0Jo2JA==
-----END CERTIFICATE-----

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

c3342a06f465123be8dc0dd48a132fc50cb15874e1e4722549ccc39c1ebc140a

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:
  {parse,plain,dnscrypt,doh,dot}
                        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
                        filtered)
  -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
                        5876fd83fda3db838)
  -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.:
                        1.1.1.1,1.0.0.1)

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 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: icmp_seq=0 ttl=57 time=4.092 ms
^C
--- 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 8.8.8.8 -n dns.google -p /dns-query -t c3342a06f465123be8dc0dd48a132fc50cb15874e1e4722549ccc39c1ebc140a

Here is the output and the sdns stamp:


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

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

sdns://AgcAAAAAAAAABzguOC44LjggwzQqBvRlEjvo3A3UihMvxQyxWHTh5HIlSczDnB68FAoKZG5zLmdvb2dsZQovZG5zLXF1ZXJ5

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 127.0.0.1 -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 127.0.0.1 -p 53 -u sdns://AgcAAAAAAAAABzguOC44LjggwzQqBvRlEjvo3A3UihMvxQyxWHTh5HIlSczDnB68FAoKZG5zLmdvb2dsZQovZG5zLXF1ZXJ5 --all-servers

Any of these two commands will start the DoH DNS proxy server using the local IP 127.0.0.1 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 127.0.0.1 -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://127.0.0.1:53
2019/08/15 17:53:59 [info] Creating the TCP server socket
2019/08/15 17:53:59 [info] Listening to tcp://127.0.0.1:53
2019/08/15 17:53:59 [info] Entering the UDP listener loop on 127.0.0.1:53
2019/08/15 17:53:59 [info] Entering the tcp listener loop on 127.0.0.1:53

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 127.0.0.1, 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:

com.dnsproxy.doh.plist

 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">
<dict>
    <key>KeepAlive</key>
    <true/>
    <key>Label</key>
    <string>com.mteam7.doh</string>
    <key>LaunchOnlyOnce</key>
    <true/>
    <key>Program</key>
    <string>/usr/local/bin/dnsproxy</string>
    <key>ProgramArguments</key>
    <array>
        <string>dnsproxy</string>
        <string>-l</string>
        <string>127.0.0.1</string>
        <string>-p</string>
        <string>53</string>
        <string>-u</string>
        <string>sdns://AgcAAAAAAAAABzguOC44LjggwzQqBvRlEjvo3A3UihMvxQyxWHTh5HIlSczDnB68FAoKZG5zLmdvb2dsZQovZG5zLXF1ZXJ5</string>
        <string>--all-servers</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

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 (127.0.0.1) 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 }}