DNS Privacy for up-to-date instructions.
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.
On the server-side:
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)
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 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.
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:
The OpenBSD libc seems to have a tcp
option for this.
Neither the FreeBSD libc, nor the DragonFlyBSD libc, nor the NetBSD libc, not the bionic libc (used by Android and FirefoxOS), nor the Mac OS/X / Darwin libresolv, seem to have a similar option.
dietlibc does not handle the options
at all (and does not support RES_USEVC
and DNS/TCP).
uclibc and musl do not have the option and does not handle DNS/TCP at all.
klibc do not have real DNS resolution.
Similar libraries:
options
field at all.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.
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.
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)
Unbound can be configured to use TLS directly with ssl-port
, ssl-service-key
, ssl-service-pem
.
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.
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)
Warning
This configuration is vulnerable to MITM attacks1. 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.
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)
# 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
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.
See the Mozilla SSL Configuration Generator:
[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
TLS for DNS:
An IETF working group working on privacy issues of DNS exchanges with drafts:
Using TLS and ALPN for Privacy Between DNS Stub and Recursive Resolvers
Propose to used the "dns" string to identify the protocol DNS when negociating a protocol with ALPN. ALPN is an extension for TLS for negociation of the protocol encapsulated over a TLS connection.
Using HTTPS for Privacy Between DNS Stub and Recursive Resolvers
Encapsulation of DNS requests over HTTP over TLS with:
https://${resolver}:${port}/.well-known/dns-in-https/${base64url(request)}
The response is sent with Content-Type: application/dns-response
.
Using TLS on a New Port for Privacy Between DNS Stub and Recursive Resolvers
Propose reserving a new port dedicated to DNS over TLS.
A mechanism for upgrading DNS over TCP to DNS over TLS over TCP.
DNS over TCP and TLS (slides)
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.
summary of RFC7858 by Stéphane Bortzmeyer (in French)
Open recursive DNS server:
DNS censorship:
VPN et anonymisation par delĂ la censure from PSES2012.
DNS monitoring:
NSA's MORECOWBELL: Knell for DNS, detailed article with informations about DNSSEC, DNS/TLS, DNSCurve, DNSCrypt, Namecoin,the GNU Name System;
MoreCowBell: Nouvelles révélations sur les pratiques de la NSA.
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 ↩↩