I Can’t Believe It’s Not DNS!
By Yuri Schaeffer
“I Can’t Believe It’s Not DNS!” is an authoritative DNS server on ESP8266 written in MicroPython. It has the following anti-features:
- No storage of zone files, AXFR each boot.
- DNSSEC filtering.
- TSIG-less AXFR support!
- Notify ‘handling’.
- Highly optimized: no sanity checks.
Jumping on the Bandwagon
The Espressif ESP8266 is one of the favorite microcontrollers of IoT-Hipsters for some time. There are many models varying in specs. Typically these devices have 0.5 to 4 MiB of flash, 64KiB instruction memory, run at 80MHz and have a couple of GPIO pins and an analogue input. Their unique selling point however is that they have Wifi built in and cost only a few bucks. Literally. For 4 Euro you can be known as ‘Mr fancy pants with his own reset button and usb port’.
Its wifi is quite capable and can do 802.11 b/g/n. So what do these hipsters use these chips for? Well sadly, mainly for logging their sensor data to the cloud via JSON over a websocket. Because websockets are better regular sockets right? Right.
(Close) Encounter of the First Kind
What’s interesting is that not only can you program these thing directly (there is a gcc based toolchain available), but there is also a ready firmware for it: NodeMCU. NodeMCU lets you write code in Lua. Having experience with the ESP nor Lua I needed a ‘hello world’ type of project. Something doable, but a bit more out of my comfort zone than blinking an LED. Oh I know, I’ll write an DNS server.
Long story short: Lua on the ESP is a pain and I don’t like it. NodeMCU could echo DNS queries over TCP with almost 2qps. Over UDP, who knows? The API never allowed me to discover the source address or port of an incoming UDP message. Don’t stray of the path of the IoT people when using NodeMCU.
And Now For Something Completely Different
Recently, after a successful crowdfunding campaign, MicroPython was released for the ESP and I decided I would give it another go. And thus “I Can’t Believe It’s Not DNS!” was born. “Not DNS” because while it might reply with DNS-like messages it is far from complete and correct. But it will serve you with about 200qps!
The premise is that this is going to be a plug and forget kind of device. We can’t just ssh in to it and change the zonefile, thus on startup we need to do an AXFR. My zone is around 20 records or so, it surely must fit? Well no. After receiving the AXFR and trying to use the data required more allocations: ENOMEM. Crud.
Python to the rescue! We can make an iterator that we would feed a socket and would spit out parsed Resource records! Little memory overhead, easy does it. Except… DNS uses compression pointers. Compression pointers greatly reduce the size of DNS packets by eliminating repeating of owner names. BUT we don’t have enough memory to buffer the entire AXFR to resolve those pointers. We need a plan.
Plan A. So a compression pointer is just an octet pointing back relative to its current position right? (HINT: No it isn’t, I’m being an idiot). So I just need to keep a sliding window of the last 256 bytes! In reality a compression pointer is 14 bits wide and absolute from the start of the message. Most names will point to one of the very first resource records in the message. So let us also keep a copy of the first 256 bytes. That surely must catch 99 percent of all cases, and we just drop any record we can’t resolve the pointer for. Who cares! Well, that is mostly true. But I wasn’t satisfied with the amount of records dropped in my small zone. So nothing else to do but store the AXFR on flash you say? Oh you don’t know me! It’s personal now, I have a plan B.
Plan B. What if we don’t resolve the compression pointers during the AXFR? That’s right, just let them sit unresolved for a bit. In the mean time drop all those pesky DNSSEC records we are offered. Those are to big anyway and I really don’t want to deal with NSEC lookups on this tiny device. Also, while we are at it drop anything other than class IN, that does not exist in my world. We end up with just a small set of records. But how do we resolve the owner names, we don’t have this data any more?
I know somebody who has this data… the master! You know what? With that set of records in hand do _another_ AXFR a couple of bytes at the time and resolve those pointers on the fly without the need to buffer anything longer than a label (max 63 bytes). Of course compression pointers can be nested so we need to repeat this process in a loop until every pointer is resolved!
Are You Being Served?
Serving queries is the easy part. Lets do as little as possible. When a query comes in we chop of anything beyond the question section. BAM! We have most of our reply done. Fiddle a bit with the flags and section counts, assume query name is uncompressed and append our resource record. Our database only contains TYPE and RDATA. Query name? Always a pointer to byte 12 in the query packet. Class? always IN. TLL? always 15 minutes, deal with it.
Finally we need a mechanism to update our little DNS server if the zone has changed. Serious software would keep track of the version of the zone via the SOA serial number. Poll for a new version on set times, listen to notifies from the master and make an intelligible decision when and how to update the zone. We don’t have the memory available to be intelligible. But we can listen for notify queries. If we receive a notify, any notify — we optimized out any ACL or checking of the zone name, we simply reboot(). The ESP8266 will powercycle and the new version of the zone will be transferred and served. SOA serial management made easy!
It should be clear to everybody this software is crap. It sort of mimics DNS but really it isn’t. You should not use this, I should not use this (but you know I will because hosting my zone on a ESP8266 is freaking awesome!)