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): RFC 7858, 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:
- unbound → stunnel (in client mode) if possible;
- force DNS/TCP (
use-vc
inresolv.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 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:
- 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.
verify TLS ↓ [DNS]<---------------------------------->[DNS] [TLS]<---------->[TLS| ] [TCP]<--->[TCP ]<---------->[TCP ]<->[TCP] [IP ]<--->[IP ]<---------->[IP ]<->[IP ] Stub TLS Init. Internet TLS Term. Recursive Resolver (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.
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:
- 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 supportRES_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:
- 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):
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):
- 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
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)
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
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)
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)
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 forresolvconf
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 settingRESOLVCONF_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
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)
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:
-
DNS PRIVate Exchange (dprive), a IETF working group working on privacy issues of DNS exchanges with drafts:
- DNS privacy considerations
- Using TLS and ALPN for Privacy Between DNS Stub and Recursive Resolvers,
propose to use the "dns" string to identify the protocol DNS when
negotiating a protocol with ALPN
(Application-Layer Protocol Negotiation).
ALPN is an extension for TLS for negotiation 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 withContent-Type: application/dns-response
.
- Using HTTPS for Privacy Between DNS Stub and Recursive Resolvers,
Encapsulation of DNS requests over HTTP over TLS with
- Using TLS on a New Port for Privacy Between DNS Stub and Recursive
Resolvers,
propose reserving a new port dedicated to DNS over TLS. - Starting TLS over DNS draft, a mechanism for upgrading DNS over TCP to DNS over TLS over TCP.
- DNS over TCP and TLS (slides)
-
TLS-DNS: 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 RFC 7858 by Stéphane Bortzmeyer (in French)
Open recursive DNS server:
- Comment mettre en place un serveur DNS récursif-cache ouvert dans de bonnes conditions
- LDN's open recursive server.
DNS censorship:
- DNS Great Wall of China;
- Hijacking of public DNS servers in Turkey, through routing;
- 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;
- Davantage de cloche à vache : la NSA espionne aussi le DNS;
- MoreCowBell: Nouvelles révélations sur les pratiques de la NSA;
- Vie privée : et le DNS alors ?
In the unbound code, the TLS outgoing connections are setup in
void* connect_sslctx_create(char* key, char* pem, char* verifypem)
. This function only callsSSL_CTX_set_verify()
if theverifypem
parameter is notNULL
. However,connect_sslctx_create()
is always called withverifypem
set toNULL
.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