Recursive DNS over TLS over TCP 443

computer network dns internet tls

Next episode:

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 (2015-02-26): Add some informations about some issues when setting up the client unbound+stunnel configuration and some helpful commands to debug the configuration.

Update (2015-04-24): Updated the stunnel configuration snippet. The french translation of the stunnel4 package was broken and was documenting many service-level options as global options. AFAIK, it was still working correctly however.

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;

  • 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.

Summary

On the server-side:

  • either unbound with native TLS;

  • or stunnel → another recursive DNS server.

On the client-side:

  • unbound → stunnel (in client mode) if possible;

  • force DNS/TCP (use-vc in resolv.conf for glibc; tcp for OpenBSD) → stunnel (but programs which do not rely on the libc resolver functions will probably ignore the option).

Generic solution:

          cache      verify TLS

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

*: or TCP if the reply is too long

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

          cache      verify TLS

[DNS ]<->[DNS     ]<->-------------------->[DNS]
[    ]   [        ]<->[   |TLS]----------->[TLS]
[UDP*]<->[UDP*|TCP]<->[TCP    ]<---------->[TCP]
[IP  ]<->[IP      ]<->[IP     ]<---------->[IP ]
Stub R.   Forwarder   TLS Init.  Internet  Recursive
          (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 certificate1 (MITM attack). This solution is not safe:

          cache       MITM!

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

Software used

stunnel

stunnel can be used to add/remove TLS layers:

  • it can be used on the DNS server side to wrap the DNS/TCP service into a DNS/TLS/TCP service;

  • it can be used on the client side to unwrap the DNS/TLS/TCP and provide a local DNS/TCP service which can be consumed by most resolvers and other DNS clients.

Protocol stack:

       verify TLS

[DNS]<-------------------------------->[DNS]
[   ]   [  |TLS]<---------->[TLS|  ]   [   ]
[TCP]<->[TCP   ]<---------->[TCP   ]<->[TCP]
[IP ]<->[IP    ]<---------->[IP    ]<->[IP ]
Stub R. TLS Init. Internet  TLS Term.  Recursive
        (stunnel)           (stunnel)

The issue is that usually the resolver will firsst 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 an (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:

  • getdns does not handle the options field at all.

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):

[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'm 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):

  • when used as the local client, it cannot by itself protect against MITM attacks;

  • when used as the TLS server, it cannot handle TLS-based client authentication.

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

Keypair 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/

Protocol stack:

[DNS]<----------------------->[DNS]
[TLS]<------------>[TLS|  ]   [   ]
[TCP]<------------>[TCP   ]<->[TCP]
[IP ]<------------>[IP    ]<->[IP ]
Resolver Internet  TLS Term.  Recursive
                   (stunnel)

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

Protocol stack:

      verify TLS

[DNS]<--------------------[DNS]
[   ]   [  |TLS]<-------->[TLS]
[TCP]<->[TCP   ]<-------->[TCP]
[IP ]<->[IP    ]<-------->[IP ]
Stub R.  socat   Internet  Recursive

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.

Protocol stack:

       verify TLS

[DNS]<--------------------->[DNS]
[   ]   [  |TLS]<---------->[TLS]
[TCP]<->[TCP   ]<---------->[TCP]
[IP ]<->[IP    ]<---------->[IP ]
Stub R. TLS Init. Internet  Recursive
        (stunnel)

Using unbound

Warning: This configuration is vulnerable to MITM attacks. 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

Protocol stack:

     DNSSEC valid.
         cache

[DNS]<->[DNS   ]<---------->[DNS]
[   ]   [  |TLS]<---------->[TLS]
[TCP]<->[TCP   ]<---------->[TCP]
[IP ]<->[IP    ]<---------->[IP ]
Stub R. Forwarder Internet  Recursive
        (unbound)

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:

  • By default, unbound will not make request on localhost. If you bind youd stunnel to a localhost address (such as 127.0.0.1), you must use do-not-query-localhost: no.

  • unbound can be shipped with a script for resolvconf which modified the forwarders which will override your tunnel configuration.

On Debian Jessie, this in handled by /etc/resolvconf/update.d/unbound and can be disabled by setting RESOLVCONF_FORWARDERS=false in /etc/default/unbound.

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

Protocol stack:

     DNSSEC valid.
         cache       verify TLS

[DNS]<->[DNS   ]<------------------------>[DNS]
[   ]   [      ]<---->[  |TLS]<---------->[TLS]
[TCP]<->[TCP   ]<---->[TCP   ]<---------->[TCP]
[IP ]<->[IP    ]<---->[IP    ]<---------->[IP ]
Client   Forwarder    TLS Init. Internet   Recursive
         (unbound)    (stunnel)

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:

An IETF working group working on privacy issues of DNS exchanges with drafts:

This setup directly connects the UDP socket with the TCP socket with socat as a consequence, the TLS stream does not transport the DNS requests in the TCP wire-format. I guess there should be framing problems when converting from TCP to UDP.

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