James Dabbs

Introducing: Droxy

02 Sep 2015

Nods to Foxy Shazam for the title and development soundtrack

This morning, I realized I was tired of typing docker-machine ip $FOO. So I wrote a thing to route requests to http://$NAME.dock:$PORT on to the docker-machine running at docker-machine ip $NAME.

Sitting here atop my pile of yak hair, I’d like to a take a minute to reflect on what I built and - more importantly - what I learned along the way. I’ve long been intrigued by Pow and know that I learn with my hands, so digging in and building something similar sounded exciting. Here’s what I found:

Automation

How Pow Works

If you’re not familiar with Pow it’s a “zero-config Rack server for OSX”. The idea is that you register your-app with Pow (by simply creating a symlink ~/.pow/your-app => /path/to/your-app), and then Pow will listen for requests to your-app.dev, rackup if needed, and forward the request on. No muss, no fuss, no forgetting to start servers or mucking around with ports. It all (modulo pry - sad face emoji) Just Works™.

That’s left me with the same question as anything else comparably magical: how does it just works? Fortunately, Pow has well-annotated source code available - though, interestingly enough for a Rack server server, it’s written in coffeescript. Here are the key (for me) parts -

/etc/resolver/dev

Pow writes something like the following to /etc/resolver/dev:

# Lovingly generated by Pow
nameserver 127.0.0.1
port 20560

This hooks into OSX’s resolver system and makes sure that DNS requests for the .dev TLD are routed to port 20560 (by default; most of this is configurable), where …

DNS server

Pow also starts a DNS server. This server is quite simple - it resolves all requests for a .dev domain to 127.0.0.1.

Firewall rule

Now we have a problem - we’ll be getting a lot of .dev traffic to localhost port 80 (the default HTTP port) but don’t really want to run pow with enough privileges to bind port 80. So Pow also adds a firewall rule to route that traffic to port 20559 (again, by default), where …

HTTP server

Pow is also running a web server. This is where the bulk of the business logic is, but it’s also slightly more familiar territory - a middleware stack responsible for find[ing]RackApplications, handl[ing]ProxyRequests and so on.

Others

There’s lots more good stuff in the config layout and installer script and update strategy, that I’d feel remiss not to mention. That said, it’s a bit out of scope for this post.

How DNS Works

Potentially embarrassing confession: before this morning, I could sum up what I knew about DNS as

so this seemed like a nice chance to get a little more familiar with the process. I pretty quickly stumbled upon dig which proved invaluable for tracing DNS lookups. For example, $ dig google.com produces

; <<>> DiG 9.8.3-P1 <<>> google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28324
;; flags: qr rd ra; QUERY: 1, ANSWER: 8, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;google.com.                    IN      A

;; ANSWER SECTION:
google.com.             242     IN      A       65.199.32.25
google.com.             242     IN      A       65.199.32.24
google.com.             242     IN      A       65.199.32.27
google.com.             242     IN      A       65.199.32.26
google.com.             242     IN      A       65.199.32.21
google.com.             242     IN      A       65.199.32.20
google.com.             242     IN      A       65.199.32.23
google.com.             242     IN      A       65.199.32.22

;; Query time: 17 msec
;; SERVER: 192.168.1.1#53(192.168.1.1)
;; WHEN: Wed Sep  2 21:36:21 2015
;; MSG SIZE  rcvd: 156

showing that a query for google.com could resolve to any of the listed ips.

So - new sledgehammer in hand - I spun up a simple RubyDNS server, plopped a couple prys in, and started taking swings at it, first with dig @localhost -p $PORT, and then using an /etc/resolver/dock and curl &/or Chrome. A few searches - like https://65.199.32.25/search?q=a+record+vs+aaaa+record (spoiler alert: IPv4 vs IPv6) - later, here’s where I ended up.

One big lesson learned: DNS lookups get cached all over the place. When something seems to be going sideways, make sure you’ve flushed the cache. The specifics vary, but if you find yourself doing it often make it convenient for yourself. Also, there are wealth of chrome tools like chrome://net-internals/#dns that can make your life easier.

Future Work

I’ve got a workable version that I’m proud of for today, but also have a jumping-off point for a few other items I’m interested in.

Web Server

One of the nice things about Pow is that all your *.dev requests pass through the webserver, so you have a good opportunity to present well-formed error messages (“rackup failed to start”, “something’s wrong with rvm”, &c.). I’d love to be able to present similar messages - i.e. to distinguish between “this is a valid machine that is running”, “this is running, but nothing is responding on the given port” and “that isn’t even a Docker machine, what are you talking about?”.

That will require rethinking the architecture a bit, however. Droxy may get requests for any arbitrary ports on the Docker machine, and we can’t listen on all of them. We could change the syntax - requesting 4567.dev.dock, for instance - or try to get smart about running containers and do something like sinatra.dev.dock. If you have ideas here, please let me know!

Celluloid

RubyDNS is in the process of being re-written as a thin layer over Celluloid::DNS. I’ve been intrigued by Celluloid ever since doing a deep-dive into Sidekiq’s architecture, and would love to port this over to sit directly on top of Celluloid and make more extensive use of it. The ip lookup cache, for instance, would be a natural fit for an actor that could poll periodically in the background for improved (perceived) performance.

Takeaways

To me, programming is a weird mix between being delighted when things work magically, and wanting to eliminate all magic. This morning, Pow and DNS were magic; tonight, not so much.

I’m also a big believer in learning by doing. For me, this sort of rolling-up-your-sleeves and reverse engineering is infinitely more illuminating than reading a textbook or Wikipedia article or blog post (sorry). Now I’ve got a skeleton to start learning more about “what happens when I type ‘google.com’ and press enter” interview questions.

So next time you run across a yak, shave away. You never know what you’ll find under it all.

And I’ll get back to Dockerizing my Haskell environment tomorrow, I promise.