/dev/posts/

Recursive DNS over TLS over TCP 443

Published:

Updated:

You might want to use an open recursive DNS servers if your ISP's DNS server is lying. However, if your network/ISP is intercepting all DNS requests, a standard open recursive DNS server won't help. You might have more luck by using an alternative port or by forcing the usage of TCP (use-vc option in recent versions of glibc) but it might not work. Alternatively, you could want to talk to a (trusted) remote recursive DNS server over secure channel such as TLS: by using DNS over TLS over TCP port 443 (the HTTP/TLS port), you should be able to avoid most filtering between you and the recursive server.

Update (2017-05-17): This was written before DNS over TLS was a thing and before it was natively implemented in resolvers. See DNS Privacy for up-to-date instructions.

Update (2017-04-08): All those solutions use one TCP (and TLS) connection per DNS request which is quite inefficient.

Update (2016-05-18): RFC7858, Specification for DNS over TLS describes the use TLS over DNS (using TCP port 853).

Warning

You might not want to use DNS/TLS to bypass state censorship. You probably want some sort of stealthy VPN.

If someone is able to censor your DNS requests, it could detect that you are communicating to forbidden hosts. Moreover, it is quite easy to check that the remote TLS server is not a web server (or not only a webserver) but a DNS server (by making DNS requests) unless you add client authentication.

Table of content

Summary

On the server-side:

On the client-side:

Generic solution:

              cache      verify TLS
                ↓               ↓
[DNS ]<----->[DNS     ]<->------------------------------->[DNS]
                              [TLS]----------->[TLS]
[UDP*]<----->[UDP*|TCP]<->[TCP    ]<---------->[TCP   ]<->[TCP]
[IP  ]<----->[IP      ]<->[IP     ]<---------->[IP    ]<->[IP ]
Stub         Forwarder   TLS Init.   Internet   TLS Term.  Recursive
Resolver     (unbound)    (stunnel)             (stunnel)

*: or TCP if the reply is too long

Unbound can be use directly for TLS on the recursive side:

          cache      verify TLS
            ↓               ↓
[DNS ]<->[DNS     ]<->-------------------->[DNS]
                          [TLS]----------->[TLS]
[UDP*]<->[UDP*|TCP]<->[TCP    ]<---------->[TCP]
[IP  ]<->[IP      ]<->[IP     ]<---------->[IP ]
Stub      Forwarder   TLS Init.  Internet  Recursive
Resolver  (unbound)   (stunnel)            (unbound)

However, it is currently not safe to use unbound to DNS/TLS on the client-side because unbound does not verify the remote certificate[1] (MITM attack). This solution is not safe:

          cache       MITM!

[DNS ]<->[DNS     ]<--------->[DNS]
              [TLS]<--------->[TLS]
[UDP*]<->[UDP*|TCP]<--------->[TCP]
[IP  ]<->[IP      ]<--------->[IP ]
Stub     Forwarder  Internet Recursive
Resolver (unbound)           (unbound)

Software used

stunnel

stunnel can be used to add/remove TLS layers:

           verify TLS
                ↓
[DNS]<---------------------------------->[DNS]
             [TLS]<---------->[TLS|  ]
[TCP]<--->[TCP   ]<---------->[TCP   ]<->[TCP]
[IP ]<--->[IP    ]<---------->[IP    ]<->[IP ]
Stub      TLS Init. Internet  TLS Term.  Recursive
Resolver  (stunnel)           (stunnel)
Protocol stack for DNS over TLS using stunnel on both sides

The issue is that usually the resolver will first try to make the query over UDP. If their is not UDP reply, the resolver will not switch to TCP. We need a way to force the resolver to use TCP.

libc

The GNU libc resolver has a (undocumented) option, use-vc (see resolv/res_init.c) to force the usage of TCP for DNS resolutions. This option is available since glibc v2.14 (available since Debian Jessie, since Ubuntu 12.04).

In /etc/resolv.conf:

options use-vc
nameserver 2001:913::8

With the RES_OPTIONS environment variable:

RES_OPTIONS="use-vc"
export RES_OPTIONS

Example:

$ # Using UDP (SOCK_DGRAM):
$ strace getent hosts www.ldn-fai.net 2>&1 | grep -e PF_INET
socket(PF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3
socket(PF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 4
socket(PF_INET, SOCK_DGRAM|SOCK_NONBLOCK, IPPROTO_IP) = 3

$ #Using TDP (SOCK_STREAM):
$ RES_OPTIONS=use-vc strace getent hosts www.ldn-fai.net 2>&1 | \
  grep -e PF_INET
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3

Other libc implementations:

Similar libraries:

Truncate all answers

The option to force the usage of TCP for DNS resolution is not available everywhere (many stub resolvers do not handle this option and some sofware do not use the system resolver). A hack to force the stub resolver to use TCP would be to have a simple local DNS/UDP service which always replies with the truncated bit set (TC=1): this should force most implementations to switch to TCP (and talk to the local stunnel process):

Sequence diagram of the truncating DNS service
Sequence diagram of the truncating DNS service for DNS over TLS
Sequence diagram of the truncating DNS service for DNS over TLS (ASCII art version)
[Resolver] [Fake service] [local stunnel] // [Remote recursive]
    |          |             |                 |
    |--------->|             |                 |  Query over UDP
    |<---------|             |                 |  Response over UDP (TC=1)
    |----------------------->|---------------->|  Query over TCP
    |<-----------------------|<----------------|  Response over TCP

TruncateDNSd is a proof-of-concept implementation of this idea: I am not sure there is a clean way to do this so it might remain a proof-of-concept.

Unbound

The correct solution is to have a local DNS recursive server which is able to delegate to a remote recursive DNS over TCP: Unbound can talk (either as a server or as a client) over TCP (tcp-upstream) or over TLS/TCP (ssl-upstream, ssl-service-key, ssl-service-pem, ssl-port).

However, it seems it cannot validate the certificate (v1.5.1):

Those two limitations can be mitigated by using a dedicated TLS encapsulation daemon such as stunnel or socat.

Server-side configuration

Using stunnel

stunnel configuration:

; /etc/stunnel/dns.conf
setuid=stunnel4
setgid=stunnel4
pid=/var/run/stunnel4/dns.pid
output=/var/log/stunnel4/dns.log
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1

[dns]
cert=/etc/stunnel/dns.pem
accept=443
connect=53

Key pair and certificate generation:

openssl req -days 360 -nodes -new -x509 -keyout key.pem -out cert.pem \
  -subj "/CN=$MY_IP" -sha256 -newkey rsa:2048
(cat key.pem ; echo ; cat cert.pem ; echo ) > dns.pem
sudo chmod root:root dns.pem
sudo chmod 440 dns.pem
sudo mv dns.pem /etc/stunnel/
[DNS]<----------------------->[DNS]
[TLS]<------------>[TLS]
[TCP]<------------>[TCP   ]<->[TCP]
[IP ]<------------>[IP    ]<->[IP ]
Resolver Internet  TLS Term.  Recursive
                   (stunnel)
Protocol stack for DNS over TLS using stunnel (recursive side)

Using unbound

Unbound can be configured to use TLS directly with ssl-port, ssl-service-key, ssl-service-pem.

Client-side configuration

Using socat

sudo socat \
  TCP4-LISTEN:53,bind=127.0.0.1,fork,nodelay,su=nobody \
  OPENSSL:80.67.188.188:443,verify=1,cafile=dns.pem,nodelay

With /etc/resolv.conf:

options use-vc
nameserver 127.0.0.1
      verify TLS
              ↓
[DNS]<--------------------[DNS]
           [TLS]<-------->[TLS]
[TCP]<->[TCP   ]<-------->[TCP]
[IP ]<->[IP    ]<-------->[IP ]
Stub     socat   Internet  Recursive
Resolver
Protocol for DNS/TLS using socat (resolver side)

Programs and libraries trying to parse resolv.conf directly without using the res_ (for example lwresd) functions will usually ignore the use-vc and fail to work if no DNS server replies on UDP.

Using stunnel

This is the client side stunnel configuration:

setuid=stunnel4
setgid=stunnel4
pid=/var/run/stunnel4/dns.pid
output=/var/log/stunnel4/dns.log
client=yes
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1

[dns]
CAfile=/etc/stunnel/dns.pem
accept=127.0.0.1:53
connect=80.67.188.188:443
verify=4

with the same resolv.conf.

            verify TLS
                ↓
[DNS]<------------------------>[DNS]
              [TLS]<---------->[TLS]
[TCP]<---->[TCP   ]<---------->[TCP]
[IP ]<---->[IP    ]<---------->[IP ]
Stub      TLS Init. Internet  Recursive
Resolver  (stunnel)
Protocol stack for DNS over TLS using stunnel (resolver side)

Using unbound

Warning

This configuration is vulnerable to MITM attacks[1:1]. Use the unbound + stunnel configuration instead.

A better solution would be to install a local unbound. The local unbound instance will cache the results and avoid a higher latency due to TCP and TLS initialisation:

server:
  # verbosity: 7
  ssl-upstream: yes
forward-zone:
  name: "."
  forward-addr: 80.67.188.188@443
# /etc/resolv.conf
nameserver 127.0.0.1
       DNSSEC valid.
           cache
             ↓
[DNS]<--->[DNS   ]<---------->[DNS]
             [TLS]<---------->[TLS]
[TCP]<--->[TCP   ]<---------->[TCP]
[IP ]<--->[IP    ]<---------->[IP ]
Stub     Forwarder Internet  Recursive
Resolver (unbound)
Protocol stack for DNS over TLS using unbound (resolver side)

As a bonus, you can enable local DNSSEC validation.

Using unbound and stunnel

Unbound currently does not verify the validity of the remote X.509 certificate. In order to avoid MITM attacks, you might want to add a local stunnel between unbound and the remote DNS server.

The unbound configuration uses plain TCP:

server:
  # verbosity:7
  tcp-upstream: yes
  do-not-query-localhost: no
forward-zone:
  name: "."
  forward-addr: 127.0.0.1@1234

Issues:

A local stunnel instance handles the TLS encapsulation (with remote certificate verification):

setuid=stunnel4
setgid=stunnel4
pid=/var/run/stunnel4/dns.pid
output=/var/log/stunnel4/dns.log
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1

[dns]
client=yes
CAfile=/etc/stunnel/dns.pem
accept=127.0.0.1:1234
connect=80.67.188.188:443
verify=4
     DNSSEC validation
         cache       verify TLS
           ↓               ↓
[DNS]<->[DNS   ]<------------------------>[DNS]
                         [TLS]<---------->[TLS]
[TCP]<->[TCP   ]<---->[TCP   ]<---------->[TCP]
[IP ]<->[IP    ]<---->[IP    ]<---------->[IP ]
Client   Forwarder    TLS Init. Internet   Recursive
         (unbound)    (stunnel)
Protocol stack for DNS over TLS using both unbound and stunnel (resolver side)

Verifying that the setup is correct:

# We whould see the local traffic to your unbound instance:
sudo tcpdump -i lo "port 53"

# We should see the traffix from unbound to local stunnel instance:
sudo tcpdump -i lo "port 1234"

# We should not see outgoing DNS traffic:
sudo tcpdump -i eth0 "port 53"

# Make DNS requests and see if everything works as expected:
dig attempt45.example.com

# Flush the cache:
sudo unbound-control flush_zone .

# Make DNS requests directly on the tunnel (bypass unbound):
dif +tcp @127.0.0.1 -p 1234 attempt46.example.com

# Display the list of forward servers:
sudo unbound-control list_forwards

What about DNSSEC?

If your local resolver verify the authenticity of the DNS reply with DNSSEC, it will be able to detect a spoofed DNS reply and reject it. But it will still not be able to get the correct reply. So you should use DNSSEC but you might still want to use DNS/TLS.

TLS configuration

See the Mozilla SSL Configuration Generator:

stunnel

[dns]
# Append this to the service [dns] section:
options = NO_SSLv2
options = NO_SSLv3
options = NO_TLSv1
options = CIPHER_SERVER_PREFERENCE
ciphers = ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK

References

TLS for DNS:

Open recursive DNS server:

DNS censorship:

DNS monitoring:


  1. In the unbound code, the TLS outgoing connections are setup in void* connect_sslctx_create(char* key, char* pem, char* verifypem). This function only calls SSL_CTX_set_verify() if the verifypem parameter is not NULL. However, connect_sslctx_create() is always called with verifypem set to NULL.

    You can verify this by configuring a local DNS/TLS service:

    openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 256 -nodes
    socat -v OPENSSL-LISTEN:1234,fork,certificate=./cert.pem,key=./key.pem TCP:80.67.188.188:53
    

    and configure unbound to use it as a TLS upstream:

    server:
      # verbosity:7
      ssl-upstream: yes
      do-not-query-localhost: no
    forward-zone:
      name: "."
      forward-addr: 127.0.0.1@1234
    
    ↩︎ ↩︎