Writing a MicroPython Pi-hole for the ESP8266

Pi-hole is a self-hosted, domain-blocking DNS resolver, which I've been running as my home network's primary DNS server for a while (using Docker). It's excellent software, and can run on very low-power hardware -- even a Raspberry Pi Zero! However, I wanted to take things a step further. I had a few ESP8266-based NodeMCU V3 boards loaded with MicroPython lying around, so I set out to replicate (some) of the Pi-hole featureset on a platform with 128 kilobytes of RAM.

NodeMCU V3 board

I found this incredibly useful blog post written by James Routley detailing a bit-level breakdown of DNS queries and responses with some sample Python code, which I used to help me write the packet parsing/creation functions.

I quickly ran into my first problem -- MicroPython omits a lot of default Python3 modules, like binascii, for the sake of saving space. So, I ended up only importing the modules socket (to send/recieve data), gc (MicroPython garbage collection, so I could manually clear memory) and time (for timeouts and scheduled jobs). This ultimately didn't have much of an effect on the program as the modules that I would have liked to use could be replicated easily by writing my own code.

After a few weeks of sporadic work, I've reached a point at which I'm happy with the project. The server can handle both A and AAAA (IPV4 and IPV6) queries by itself, using a binary search to compare the domain found in the query's QNAME with the blacklisted domains found in the imported blocklist to see if the request should be refused. It stores domains and IPs in dictionaries as a form of caching. If the program doesn't know how to handle a query (either due to hardcoded parsing of packets or because I haven't added support for the query type), it is simply sent upstream, and the result returned back to the client. This means all queries are able to be fulfilled. After 1 hour, the caches are cleared and the built-in subroutine gc.collect() called to try and reduce memory usage.

Clearly, it would be a poor idea to run this program in a "production environment" (ie your home WiFi) due to the massive vulnerabilites that I'm sure are present in the code, and because it's performance can be at the least multiple times worse than simply using 1.1.1.1 (and that's just for one client!). I think it's pretty cool that such an important piece of the internet process even works on a $3 IOT development board, though.

Here's the GitHub link.

Comments

Popular posts from this blog

The Casio F-91W

Haskell gravity simulation