I’ve been working with DNS a lot recentlyMainly in Boulder, the backend behind the free CA Let’s Encrypt. and in doing so have had the fun task of setting up and tearing down a whole range of DNS servers in the quest to find one that would actually make writing and testing DNS clients and resolvers even somewhat easy and pain-free.
The first two servers that I’m sure jump to everyone’s minds are BIND and Unbound. If you’ve ever had to do any kind of DNS administration you can probably get up and running with one of these servers relatively quickly. But using them in a programmatic fashion for testing where you may want to quickly change the contents of zones, add zones, delete zones, or quickly setup/teardown these setups on various different machines becomes quite annoying to handle almost immediately.
Instead of spending a couple of hours trying to setup some convoluted harnessDon’t even get me started on
nsupdate for BIND or Unbound using TSIG + DDNS updates or something to simplify various test procedures I instead turned to
github.com/miekg/dns, a Go DNS library that I’ve used a number of times beforeBoulder heavily uses this library, including for possibly one of the most useless DNS servers ever written, outside of our specific test case of course!.
miekg/dns is the basis for Cloudflares various DNS server/parser implementations and has proven to be quite resilient parser and, bonus, it also comes with a great basic server implementation that follows Go’s general server/handler interface.
It was on top of this server implementation that I decided to build my testing server,
dns-workbench. This server didn’t really have to be extremely complex or, entirely, RFC-compliant, my only real requirements were
- It should only pretend to be a authoritative server, nothing else fancy DNS-wise
- It should support a simple way to define zones and records, and should support as many record types as possible (thanks to
dns/miekgthe second part it taken care of for us)
- Quick and clean setup, all non-zone related configuration should be provided on the command line, the only file required should be a definition file
- A simple method to gracefully switch out the zones being served without having to restart the server each time
The combination of Go and
miekg/dns makes these tasks pretty easyTaking just over 300 lines in version
0.0.1. Now we’ve got that out of the way lets get on to how
dns-workbench actually works!
The zone definition file is written in YAML (and read using
gopkg.in/yaml.v2), in order to be somewhat easier on the eyes, and takes the following form
zones: bracewel.net: bracewel.net: a: - 220.127.116.11 - 18.104.22.168 aaaa: - FF01:0:0:0:0:0:0:FB caa: - 0 issue "letsencrypt.org" www.bracewel.net: a: - 22.214.171.124 - 126.96.36.199 aaaa: - FF01:0:0:0:0:0:0:FB other.com: other.com: mx: - 10 mail.other.com
A quick method takes these definitions and creates a map of all the records for each domain. Record values are all in their DNS traditional presentation formatLike the format used in BIND style zone files, but minus all the
domain. TTL IN TYPE cruft, just the good stuff! and will have a default TTL (
3600) added, additionally the parser will also generate
NS authority records for each respective zone pointing back to the servers DNS name (
localhost by default).
dns-workbench allows one of these definition files to be loaded at runtime or can gracefully reload the definitions via a built in HTTP API provided by
net/http. This API exposes a single endpoint
api/reload which accepts a JSON zones object by
POST. If the zones object can be properly parsed the DNS server will wait until all current queries have been responded to before switching out the zones and blocking any new queries until this is doneThanks, mostly, to
sync.RWMutex.. This process can be done with the
dns-workbench reload command or, since it’s just JSON, from pretty much anywhere.
The server itself
The DNS server is run with the
dns-workbench run command. All server configuration is handled by a small hand full of command line parameters, more information on these can be found by using
dns-workbench help [command]More about this on the repo page., that the set the address and port to listen on for the DNS and API servers as well as various timeouts and a few other things.
Now why don’t we take a quick look at how it actually runs
Figure 3: My reaction Yeah not really sure to be honest, It’s extremely lightweight so in theory it should be pretty fast, I’ve only been using it locally under relatively low load so I have no concrete data. I could run a load tester against it but I can’t really be bothered… If you do let me know I guess?
There are a bunch of cool features that I’d like to implement, namely much finer grained control over the zone reloading code, but but I don’t really have time at the moment so if anyone else wants to take a crack at adding things feel free to send a pull request!