M-Abozaid/esp32-c3-adblock

DevOps & Self-hosting

Turns a $2 ESP32-C3 into a DNS sinkhole that blocks ad and tracker domains using hashes stored in flash, for home networks that want Pi-hole-style filtering with very little hardware.

3,474
387
C++
MIT
4 months old

DNS ad blocking on a $2 board

The project runs a Pi-hole-style DNS sinkhole on an ESP32-C3 without PSRAM. Instead of loading blocked domain strings into RAM, it stores each domain as a sorted 40-bit FNV-1a hash in flash and binary-searches that table for each query. A hit is answered with 0.0.0.0; a miss is forwarded to the upstream resolver and the reply relayed. According to the README, roughly 140k domains fit in about 0.7 MB of flash and are matched in around 10 ms, using about 50 KB of RAM. A 537k-domain list is also possible but leaves about one hash collision.

It is aimed at people who want network-wide DNS filtering on a very cheap device, for example plugging a C3 SuperMini into a router's spare USB port. Other features include a web dashboard with per-client counts, client banning, custom blocked domains, mdns discovery at c3adblock.local, OTA firmware and blocklist updates, and a captive-portal WiFi setup.

Quick start

Prepare an ESP32-C3 board with 4 MB flash (no PSRAM needed), a stable USB power source, and a current PlatformIO install. The distro/apt platformio package is too old and fails; use the current VS Code extension core or pip install -U platformio.

  1. Copy the secrets template and edit it: cp src/secrets.example.h src/secrets.h. WEB_USER, WEB_PASS and OTA_PASS are required because they protect the dashboard's state-changing endpoints and network OTA. WIFI_SSID and WIFI_PASS are optional; leaving the placeholders lets you use the on-device setup portal.
  2. Build the blocklist hash table with python3 tools/build_blocklist.py data/blocklist.bin. The default source is StevenBlack base plus Hagezi Light, about 100k entries. You can pass URLs and local files as additional sources; hosts files, plain domain lists and basic AdGuard/Adblock rules are accepted.
  3. Flash the firmware and filesystem: pio run -t upload then pio run -t uploadfs. This is the only USB flash needed.
  4. Open the serial monitor with pio device monitor, note the IP, then reach the dashboard at http://c3adblock.local.

If WiFi credentials are missing or wrong, the device starts an open access point named C3-AdBlock-XXXX with a captive portal. Join it from a phone, pick your network and enter the password. Later you can return to the portal with the dashboard's Forget WiFi button or by holding BOOT while powering on.

To use it, point a client's DNS at the C3's IP, or add it as a secondary resolver behind your main DNS. Verify with:

dig @<c3-ip> doubleclick.net   # -> 0.0.0.0  (blocked)
dig @<c3-ip> github.com        # -> real IP  (forwarded)

Updating

After the initial flash, both the blocklist and firmware update over WiFi from the dashboard. You can upload a freshly built blocklist.bin, set a remote URL so the device pulls a prebuilt list on a schedule, or upload .pio/build/c3/firmware.bin for OTA. A default list is rebuilt every Monday by GitHub Actions and published to a stable release URL. CLI OTA is also available with pio run -t upload --upload-port c3adblock.local --upload-protocol espota, which requires OTA_PASS.

Constraints

  • Flash budget: firmware OTA needs two app slots, leaving about 1.3 MB for the blocklist, roughly 250k domains. The 537k list only fits the single-app partition table, which disables firmware OTA. The partition layout is set in partitions.csv.
  • Board support: the C3 SuperMini is the tested target. Classic ESP32 DevKit/WROOM builds via pio run -e esp32dev -t upload, but that is community-contributed and only compile-tested.
  • Security boundary: the dashboard is plain HTTP on port 80, and Basic Auth credentials are base64, not encryption. State-changing endpoints also require an X-Requested-With: c3-adblock header to block cross-site form/image requests. If secrets.h still has the public placeholder passwords, the device logs a warning and shows a banner but still boots, so set real values before using it on an untrusted network. The WiFi setup access point is intentionally open during setup.
  • Blocklist expressiveness: regex, wildcards, $ modifiers and cosmetic ## rules are skipped and counted. An @@ allow rule only removes one exact entry and cannot exempt a subdomain of a blocked parent.
  • Linux serial: ModemManager on Fedora/Ubuntu can grab /dev/ttyACM0 and reset the C3. Stopping ModemManager and adding a udev rule for vendor ID 303a resolves it.
  • Antenna and power: the README cautions that cheap USB-C to A adapters can brown out the radio, and the printed enclosure should keep the antenna end clear.

HotRepo's take

The project appeared on the monthly C++ list in the snapshot dated 2026-11-01, where it recorded 2,789 stars gained and a best position of 15. An earlier daily snapshot from 2026-10-06 shows 1,614 stars and 196 stars gained that day. It also drew a Hacker News thread on 2026-10-07 with 195 points and 82 comments. The thread's substantive technical criticism is about hash collisions: commenters argued the README frames collisions in terms of two blocked domains sharing a hash, whereas the real risk is a false positive where an allowed domain hashes to the same value as a blocked one (muti, zamadatix). That is worth knowing because the device sits in the path of every DNS query, and an unlucky collision can over-block a legitimate site until it is unblocked via the dashboard or API. The rest of the discussion is about hardware picks, including weak built-in antennas on some C3 boards, which matters more for placement than for the core design.

Written by AI from the project's README, updated . The project's own docs are the source of truth.

Hacker News

ESP32-C3 Adblock
▲ 195 · 82 comments · October 7, 2026