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:
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
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 -
Pow writes something like the following to
# 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 …
Pow also starts a DNS server. This server is quite simple - it resolves all requests for a
.dev domain to 127.0.0.1.
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 …
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
handl[ing]ProxyRequests and so on.
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
- It’s like a phonebook for the internet (name => ip)
- I have to set up A records to get my domains pointed to the right place
- There are namesevers in the mix somewhere
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 22.214.171.124 google.com. 242 IN A 126.96.36.199 google.com. 242 IN A 188.8.131.52 google.com. 242 IN A 184.108.40.206 google.com. 242 IN A 220.127.116.11 google.com. 242 IN A 18.104.22.168 google.com. 242 IN A 22.214.171.124 google.com. 242 IN A 126.96.36.199 ;; 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
curl &/or Chrome. A few searches - like https://188.8.131.52/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.
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.
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!
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.
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.