A pocket-sized, network-wide ad & tracker blocker for the M5StickS3. by AlasdairDev
whxrl is a DNS sinkhole — a tiny Pi-hole-style ad blocker that runs entirely on an M5StickS3 (ESP32-S3). Point your router's DNS at it and every device on your Wi-Fi — phones, laptops, TVs — gets ads and trackers blocked, with nothing to install on those devices.
It sets itself up right on the Stick: scan for your Wi-Fi, join it, and type the password on the on-screen keypad. No PC or phone required. Once it's online there's also a clean web dashboard at http://whxrl.local.
When a device asks "what's the IP for ads.example.com?", whxrl checks the name
against its blocklist. If it's an ad/tracker domain it answers 0.0.0.0 so the
request never loads. Everything else is quietly forwarded to a real upstream
resolver (Quad9 9.9.9.9 by default) and the answer passed back.
The blocklist lives in PSRAM as a hash set of 32-bit fingerprints (4 bytes per domain), so lookups are effectively instant even at the ~600,000 domains it holds. The same packed hashes are what's stored on flash too, so the on-device list is ~3.5× smaller than holding the domain text and boots in a single read. On first connect the device downloads and merges several well-maintained lists, de-duplicated — together they total roughly a million domains, trimmed to the ~600k that fit the hardware:
- Hagezi DoH — encrypted-DNS bootstrap servers (see Anti-bypass below)
- Hagezi Pro++ — ads + trackers + telemetry + native app/OS tracking (the bulk of the list)
- Hagezi TIF — threat intelligence (malware / phishing / scam)
- StevenBlack hosts — ads + malware
The lists refresh themselves automatically every few days, so new domains get picked up without a reflash. The download verifies each list server's TLS certificate against the built-in Mozilla CA bundle.
CNAME uncloaking: replies coming back from the upstream resolver are inspected
too. If an innocent-looking first-party name (e.g. metrics.example.com) turns
out to resolve via a CNAME to a known tracker, that answer is sinkholed as well
— catching trackers that try to dodge a plain domain match.
Anti-bypass: many browsers, phones and TVs ship their own DNS-over-HTTPS and would quietly route around this box. whxrl blocks the bootstrap domains those DoH resolvers use, so those devices fall back to system DNS — i.e. back through the filter. (It still can't read DoH/DoT traffic itself; block those ports at your firewall if a device hard-codes its own resolver IP.)
| Board | M5StickS3 (ESP32-S3, M5StickC Plus2-class) |
| Flash | 8 MB |
| PSRAM | 8 MB |
| Display | 240 × 135 LCD |
PSRAM is what makes the big blocklist possible. On a board without PSRAM the firmware falls back to a small in-RAM list (
MAX_DOMAINS_NO_PSRAM, 8k domains).
Want it running without installing anything? The release/ folder
has prebuilt binaries you can flash straight from a browser with the Espressif
web flasher — use Chrome or Edge (Web Serial isn't supported in Firefox/Safari):
- Open https://espressif.github.io/esptool-js/.
- Set Baudrate to
921600, click Connect, and pick the Stick's serial port. (If Connect fails, hold the BOOT button while connecting, or drop to115200.) - Click Add File → choose
release/whxrl-m5sticks3-merged.bin→ set the offset to0x0→ click Program. - Press reset. The LCD drops into Wi-Fi setup; then open http://whxrl.local
(default login
admin/whxrl— change it right away).
See release/README.md for the component-file option and the
exact offsets.
Everything builds with arduino-cli; the
included tools/build.ps1 fetches the CLI, the ESP32 core, and the libraries for
you, then compiles firmware/whxrl.
# from the repo root, on Windows:
powershell -ExecutionPolicy Bypass -File .\tools\build.ps1 # build only
powershell -ExecutionPolicy Bypass -File .\tools\build.ps1 -Upload COM9 # build + flashTo build by hand with your own arduino-cli:
arduino-cli compile \
--fqbn esp32:esp32:esp32s3:PSRAM=opi,FlashSize=8M,PartitionScheme=custom,FlashMode=qio,CDCOnBoot=cdc \
firmware/whxrl
Important build notes
- This is an 8 MB board. The bundled
firmware/whxrl/partitions.csvgives a 3 MB app slice + ~4.9 MB FAT partition for the blocklist. The stock 16 MB schemes (app3M_fat9M_16MB) will not fit and FFat won't mount. PartitionScheme=customtells the core to use thatpartitions.csv.- Libraries: M5Unified, plus the ESP32Async forks of AsyncTCP and ESPAsyncWebServer (the older me-no-dev repos can fail to build on S3).
If your device doesn't show up as a COM port when plugged in: use a data (not charge-only) USB-C cable, and try holding the BOOT button while connecting.
| Button | Action |
|---|---|
| Front "M5" button | Select / OK / tap a key |
| Side button — single click | Next / Down |
| Side button — double click | Previous / Up |
| Side button — hold ~0.6 s | Back / Cancel |
First boot drops you straight into Wi-Fi setup: pick your network, type the password on the keypad, done. The home screen then shows RUNNING, the device's IP, and live Queries / Blocked / %-blocked counters. The About screen always shows the current dashboard login.
To filter your whole network, set your router's DNS server (in its DHCP / LAN settings) to the device's IP, then toggle each device's Wi-Fi off and on so it picks up the change. To try it on just one device first, set that device's DNS to the whxrl IP.
Tip: reserve the device's IP in your router (tie it to the Stick's MAC) so it never changes. The dashboard's setup guide walks through all of this.
Once connected, open http://whxrl.local (or the device's IP) for a live dashboard: requests, ads blocked, block rate, most-blocked sites, a just-blocked feed, an On/Off toggle, and a "refresh blocklist" button.
The dashboard is password-protected (see Security below). The default login is:
username: admin
password: whxrl
The dashboard shows a reminder to change this immediately, and there's a Change login link to set your own username and password. The current login is always visible on the Stick's About screen if you forget it.
whxrl is a network device, so it's built to be safe to leave running on your LAN:
- DNS is LAN-only. The sinkhole answers queries from private addresses only
(
10.x,192.168.x,172.16–31.x,127.x,169.254.x), so it can't be abused as an open resolver from the internet. - Upstream replies are validated. Forwarded answers are only accepted from the resolver they were sent to, and the answer parser is bounds-checked, so a forged or malformed reply can't poison a lookup or corrupt memory.
- The dashboard requires a login. All control endpoints (stats, on/off toggle,
refresh, blocklist upload, change-login) sit behind HTTP Basic auth. Credentials
are stored on the device's flash and changeable from the dashboard. Change the
default
admin/whxrlright away — it's a published default. - The setup hotspot is WPA2-protected. When you start on-device "Web Setup
(AP)", the temporary
whxrlhotspot uses a random per-device key shown on the screen, so a passer-by can't join during setup and snoop the Wi-Fi password you type. - Blocklist downloads use validated TLS. Each list is fetched over HTTPS with
the server certificate checked against the built-in Mozilla CA bundle (with a
graceful fallback so a captive portal or odd middlebox can't break updates — the
dashboard shows whether the last refresh was
TLS verified).
Everything runs over plain HTTP on your LAN, which is the norm for this class of device. If you want it reachable only from trusted machines, restrict it at your router/firewall.
firmware/whxrl/ the firmware — whxrl.ino + partitions.csv (this is the Arduino sketch)
release/ prebuilt .bin files + web-flasher instructions
tools/build.ps1 one-shot build + flash script (Windows)
examples/ a small sample blocklist.txt
docs/ extra notes (e.g. generating flashable binaries)
arduino-cli/ and arduino-user/ are local build tooling installed by the build
script; they're gitignored and not part of the repo.
- Domains are matched by 32-bit hash, so there's a vanishingly small chance of a collision blocking a legitimate domain. Widen the table to 64-bit if that ever matters for your list.
- It handles standard UDP DNS (port 53). It does not read DNS-over-HTTPS/TLS traffic — but it does block the bootstrap domains DoH clients use, which pushes most devices back onto filtered system DNS. For a device that hard-codes its own resolver IP, block that at your firewall.
- It blocks ads served from separate ad domains, but not ones delivered from the same place as the content itself (e.g. YouTube's in-stream ads).
- Counters reset when the device restarts (they're not saved to flash).
MIT.