Subscribe via RSS

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, the 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 engineers mentioned 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 docker-machine ip.

After poking around the source for pow, their solution boiled down to:

  • Install a resolver file in /etc/resolver to 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 docker-machine ip, and send back the IP.

0 to nameserver in under 100 lines of JavaScript

Why JavaScript? I’m a web developer. And my attempts to do this in a shell script fell apart when I realized I had no idea where to send the response packet.

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):

var dgram = require('dgram');
var socket = dgram.createSocket('udp4');

socket.bind(53, function() {
    s.on('message', function(datagram) {
        // datagram contains the DNS packet
    });
});

Boom.

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.

For example, 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”
  • 00 - Null-Terminator

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 DNS packet):

var length, offset, targetIndex;
var txnid = chunk.slice(0, 2);
var hostname = {
    machineName: '',
    fqdn: [],
    octets: null
};

// read the domain name
for (offset = 12; chunk[offset]; offset += length) {
    length = chunk[offset];
    targetIndex = offset + length + 1;
    hostname.fqdn.push(chunk.slice(++offset, targetIndex).toString());
}

hostname.machineName = hostname.fqdn[0];
hostname.fqdn = hostname.fqdn.join('.');
hostname.octets = chunk.slice(12, offset);

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:

var exec = require('child_process').exec;

exec('docker-machine ip ' + machineName, function(err, stdout) {
    // stdout is the IP address of `machineName`
});

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.

// turn the IP addr into 4 8-byte octets
var ipOctets = resolvedIPAddress.split('.').map(Number);

var bufarr = [
    txnid[0], txnid[1], // txnid - this comes from the original request
    0x81, 0x00,         // flags (std response)
    0x00, 0x00,         // question count
    0x00, 0x01,         // answer count
    0x00, 0x00,         // authority count
    0x00, 0x00          // additional
];

// push the original hostname octets back into the response
for (var x = 0; x < hostname.octets.length; x++) {
    bufarr.push(hostname.octets[x]);
}
bufarr.push(0x00);

// more headers
bufarr = bufarr.concat([
    0x00, 0x01,             // Type A (host address)
    0x00, 0x01,             // Class: IN
    0x00, 0x00, 0x00, 0x00, // TTL
    0x00, 0x04,             // ip length
]);

// add the IP address to the end
bufarr = bufarr.concat(ipOctets);

// return it back to the requester
s.send(new Buffer(bufarr), 0, bufarr.length, rinfo.port, rinfo.address);

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 domain name dev.docker. This way, our app.properties file just needs to reference dev.docker, instead of 192.168.99.100 or ..101 or whatever IP docker-machine happens to assign today. If you use docker-machine to run your containers, this tiny little server helps a lot towards making your configs deterministic.

Stay Tuned

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!