The 101st IETF meeting is in London this coming week, and it starts with the IETF 101 Hackathon.
I thought I could do some useful work on DNS privacy. There is lots of work going on in this area, part of which is adding lots of new transport options for DNS - as well as the traditional DNS-over-UDP and DNS-over-TCP, there is now DNS-over-TLS and (soon) DNS-over-HTTPS, and maybe DNS-over-DTLS and DNS-over-QUIC.
My idea was to set up a proxy that could provide DNS-over-TLS and DNS-over-HTTPS in front of a trad DNS server (for my purposes, specifically BIND, but the proxy does not need to care).
Choice of proxy
There are a number of proxies that make sense as a base for this work: I need TLS support and HTTP support and a reasonably light-weight implementation - HAProxy is one, but I chose to use NGINX. There’s a variant of NGINX called OpenResty which includes LuaJIT and a bunch of other libraries and plugins.
Lua is a really nice language for scripting an event-driven server like NGINX, since Lua has coroutines. This means you can write straight-line Lua, and whenever it calls into a potentially-blocking OpenResty API, it actually suspends the coroutine and drops into the NGINX event loop.
Existing infrastructure
I have a little virtual machine cluster on my workstation for development and prototyping - I use it for my work porting the IP Register database to PostgreSQL. The relatively unusual part of the setup is that I have a special DNS zone (signed with DNSSEC) for these VMs, and the VMs and their provisioning scripts have DNS UPDATE privileges on this DNS zone. When a VM boots and gets IP addresses form DHCP and SLAAC, it UPDATEs its entry in my dev zone. When it generates its ssh host keys, it puts the corresponding SSHFP records in the zone.
Plan of attack
My prototypes are configured using Ansible, so that there’s relatively little required to bring them up to production quality. So the basic plan was to write a playbook to install OpenResty, configure NGINX, and add a bit of Lua to massage DNS-over-HTTPS requests into DNS-over-TLS.
I have a bit of previous experience with Lua, but I have never used NGINX before, so there will be a certain amount of learning…
Installation
There are
pre-built OpenResty packages
which are quite convenient - very similar to the existing setup I have
for PostgreSQL. Happily the OpenResty packages include a SysVinit rc
script so they are compatible with my non-systemd
VMs.
TLS certificates
To get a certificate for my service I needed to use Let’s Encrypt, which I have not done before (since we have an easy enough TLS certificate service at work). I chose to use the dehydrated ACME client since I have friends who report that it is very satisfactory. Since I’m a DNS geek, I thought it would be fun to use the ACME DNS-01 challenge. (I realised later that this was a lucky choice, since my VMs have RFC 1918 IPv4 addresses.)
One simple script (basically copied off the dehydrated
wiki) and two
lines of configuration, and I could run dehydrated -c
and get a TLS
certificate within a few seconds. Absolutely brilliant. My first
experience of really using Let’s Encrypt was really pleasing.
DNS over TLS
This is fairly easy to set up with a bit of Googling StackOverflow to
work out how to configure NGINX. The trick is to use a stream {}
section instead of the usual http {}
section.
stream {
upstream dns {
server 131.111.57.57:53;
}
server {
listen [::]:853 ssl;
proxy_pass dns;
ssl_certificate cert.pem;
ssl_certificate_key cert.key;
}
}
Getting to grips with OpenResty
I spent a while reading around the OpenResty web site looking for documentation, but I wasn’t having much luck beyond the “hello world” example. What I needed was some example code that showed how to pick apart an HTTP request and put together a response.
I found OpenResty’s Lua DNS library which I thought might be useful for cribbing from, but it did not help at all with HTTP.
Introspecting Lua
One of the coolest talks at the LuaConf I went to several years ago was about how Olivetti use (used?) Lua to write test code for their scanner/printer devices. The bulk of the firmware was written in C++, but it included a Lua interpreter which was able to dig through the RTTI, allowing a test engineer to tab-complete through the device’s internal data structures and APIs.
So, being unable to find anything else, I thought I could get some idea of what the OpenResty API looked like by doing a bit of reflection.
I found a nice script called
inspect.lua
which pretty-prints a Lua data structure. You can dump all the global
variables recursively with inspect(_G)
, which gave me a nice listing
of the Lua standard library and some OpenResty bits.
I added the following variant of “hello world” to nginx.conf
so I
could conveniently curl
a list of the OpenResty ngx
module.
location /ngx {
default_type text/plain;
content_by_lua_block {
ngx.say(require("inspect")(ngx))
}
}
Writing an HTTP request handler
There were some fairly promising functions called things like
ngx.req.get_method()
and ngx.req.get_headers()
which turned out to
do reasonably obvious things. I also dug through the OpenResty Lua
module sources that were installed on my dev VM to get a better idea
of how they worked.
This was just about enough that I could write a handler to implement most of the DNS-over-HTTPS requirements.
The main stumbling point came when I needed to do base64url
decoding
of DNS query packets embedded in HTTP GET requests.
Whither base64url
After a bit of grepping it became evident that OpenResty has support
for normal base64
- I found its
resty.core.base64
module, but disappointingly it does not include base64url
support.
However, running strings
on nginx
revealed that nginx
does have
base64url
support, though OpenResty does not expose it to Lua.
The base64.lua
module uses the LuaJIT FFI to call some OpenResty
C wrapper functions that convert vanilla C types to NGINX’s internal
types, then call NGINX’s base64
functions.
The LuaJIT FFI is a glorious thing: as well as calling C functions, you can directly access C structures from Lua. So OpenResty’s C wrapper layer is not in fact needed.
So, after a bit of clone-and-hack (and an embarrassing diversion
spending ages working out that I had mistyped a variable name) I was
able to write a 50 line base64url.lua
module which called
ngx_decode_base64url()
directly.
DNS time
At this point my HTTP handler was able to get a DNS wire format query from an HTTP GET or POST request, so I needed to forward it to a DNS server, and return the response to the client.
During my base64url
adventures I had worked out that the
documentation I had been looking for belongs to the
OpenResty Lua Nginx Module,
so it was straightforward to write the back half of the proxy.
This back end is the bare minimum: just 20 lines of code including error checking and tracing. The whole DNS-over-HTTPS handler is less than 100 lines of Lua.
This is where it becomes obvious that OpenResty shines, because all the front-end POST reading and back-end socket calls are potentially blocking, but I can write straight-line sequential code without any inversion of control.
DoH clients
At this point I had a server, but no client for testing it!
I was sitting next to the author of
doh-proxy
who told me that he has one. I installed it, but found that it would
hang after sending a request. (I did not debug this problem.)
So I decided to go low-tech.
The doh-client
printed its base64url
-encoded query, so I could
curl
it myself. I just needed something that could pretty-print the
response.
First attempt was with drill
which can dump DNS packets and print dumped packets. However its dump
format is an ASCII hex dump, not binary, so I found myself writing a
complicated curl | perl | drill
pipeline and it was getting silly.
So fairly rapidly I moved on to the second attempt in pure perl, using
Net::DNS for query packet construction and
response packet pretty-printing,
MIME::Base64
for that troublesome base64url
encoding,
LWP::UserAgent
for performaing the HTTPS request.
20 lines of code, and I have a client that works with my server!
Tomorrow
There are a couple of obvious next steps.
Firstly, I should extract this work from my prototyping setup, so that it can be published in a form that’s plausibly useful for other people. Edited to add: Done!
Secondly, my trivial DoH back end needs some work. It uses one brief DNS-over-TCP connection per HTTP request, which is rather wasteful. It would be a lot cooler to keep one or more persistent TCP connections open to the DNS server, and multiplex DNS-over-HTTP requests onto these DNS-over-TCP connections. It looks like NGINX and OpenResty have lots of support for connection pooling, so I should work out how I can make good use of it.
Today has been pleasingly successful, so I hope tomorrow will have more of the same!