Up The Rabbit Hole: Part I - A docker-machine DNS server
A couple weeks ago, our front-end team decided that
gulp just wasn’t providing us with a lot of
benefits and that we’d prefer to work without it. Thus began the effort of “degulping” our builds.
This has turned into quite a project that’s involved writing a new build script, learning about
headless browser testing, and overhauling our local development environment.
Before reading the rest of this post, you might want to read some of our previous posts about our dev environment:
This post is the first in a 4 part series that will describe how each of these issues was solved from the bottom up, ultimately ending with a post about our shiny new gulpless docker-composed dev environment.
Part I: Making docker-machine play nice with our devenv
The internal monologue went something like this:
So the end goal here is to de-gulp our build. We want to do it in a way where one repository can serve as the foundation for all of our other projects. For that, we’ll also want to incorporate headless testing. But for THAT we need to change the way our dev environment works, probably switching to docker-machine + docker-compose… but then our environment totally breaks because it all points to 127.0.0.1, and docker-machine doesn’t guarantee the same IP each time… Hmm…
Basically, when you start up a docker-machine, it’s assigned a dynamic IP address. Conveniently,
docker-machine ip command exists to resolve a machine name into an IP, but updating our
app.properties file every time the IP changed sounded like a really tedious and error-prone process.
The first step towards solving this problem involved poking around the source code for
pow.cx, after one of my fellow
that they hijack a TLD specifically for doing name resolution. That immediately felt like a good
step in the right direction – hijack the
*.docker TLD and resolve the hostname using
After poking around the source for pow, their solution boiled down to:
- Install a resolver file in
/etc/resolverto handle all calls to the configured TLD
- Run a local DNS server to resolve those TLDs to 127.0.0.1
This sounded perfect– we could hijack the TLD, then run a DNS server in a docker container to actually resolve the domains. A little more research showed that running dnsmasq in a container to resolve domains was a pretty common setup, so I gave that a try.
And it worked.
But it just felt dirty… We had added a decent chunk of overhead to the devenv. Not only would a docker-machine would have to be running before the server could be started, but a hosts file would need to be kept up to date in that container so as machines were started, they could still be resolved. It just felt like the wrong tool for the job.
In the course of reading about DNS servers, I stumbled across this 690 byte DNS
server (including a
license line). Since our use case was so targeted and specific, it’d probably be
better to implement the bare minimum required to accept a DNS query, run it thru
ip, and send back the IP.
Handling a basic DNS request is actually a pretty straightforward (if you ignore all the edge cases):
- Start a UDP listener
- When a new packet comes in, read the hostname (starting at byte 12)
- Translate that hostname into an IP address
- Send back a packet with that same hostname + the resolved IP address
Step 1: Start a UDP listener
This is just crazy easy in node (here, we’ll assume port 53 - the default DNS port):
Step 2: Read the Domain Name
The domain name is part of the
Queries section of a DNS request, which starts at byte 12.
To send domain names over the wire, the DNS protocol encodes the request domain.
dev.debug would be represented as
03 64 65 76 05 64 65 62 75 67 00
To break that down:
03- A string of length 3 follows
64 65 76- “dev”
05- A string of length 5 follows
64 65 62 75 67- “debug”
So really, that first string is the most important part–
dev since that’s the name of the
docker-machine to look up. Parsing this whole thing looks something like this (where
chunk is the
The octets are saved for later because that same value gets sent in the response, and I didn’t feel like re-encoding it.
Step 3: Translate the domain to an IP
This is about as straightforward as it gets:
Step 4: Build the response packet and send it back
This is mostly boilerplate; DNS headers followed by the IP octets that represent the domain.
Step 5: Profit
This micro-server is available on GitHub, complete with installation and usage instructions.
In our environment, while that server is running, our dev environment is accessible through the
dev.docker. This way, our
app.properties file just needs to reference
..101 or whatever IP docker-machine happens to assign today. If
docker-machine to run your containers, this tiny little server helps a lot towards making
your configs deterministic.
Part 2 covers our devenv conversion to docker-compose and some changes to auxiliary tooling like our headless browser configurations!
And remember, if this sort of a project is interesting to you, we’re hiring!