# esp32-c3-adblock: a Pi-hole-style DNS sinkhole on a $2 ESP32-C3

> M-Abozaid's esp32-c3-adblock stores 140k blocklist domains as sorted 40-bit FNV-1a hashes in flash and binary-searches them, so a board without PSRAM can answer blocked lookups in about 10 ms. Here is how it installs, what it costs, and where it breaks.

**M-Abozaid/esp32-c3-adblock** — Pi-hole-class DNS ad-blocker on a $2 ESP32-C3 (no PSRAM): 537k domains as 40-bit FNV-1a hashes in flash, binary-searched. UDP DNS sinkhole + web dashboard. https://youtube.com/shorts/RaxszOUMi8E?feature=share

- Repository: https://github.com/M-Abozaid/esp32-c3-adblock
- Stars: 708 · Forks: 72
- Language: C++
- License: MIT
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/m-abozaid-esp32-c3-adblock

## The problem: a blocklist that does not fit in RAM

A DNS sinkhole is simple in principle. Intercept the lookup, and if the domain is on a list, answer 0.0.0.0 instead of the real address. The hard part on a microcontroller is the list. Most ESP32 DNS sinkholes keep the domain strings in RAM, which is why they ask for PSRAM and push the board price toward $8. The README states the string-in-RAM approach needs roughly 2.5 MB of RAM for 141k domains. An ESP32-C3 has nowhere near that.

The project's answer is to stop storing strings. Each domain becomes a fixed 5-byte, 40-bit FNV-1a hash, sorted and written to flash. A lookup is a binary search over those bytes, not a string comparison. According to the README, 141k domains occupy about 0.67 MB of flash and the firmware uses around 50 KB of RAM, which is what makes a $2 board viable. The audience is the homelab tinkerer with a spare USB port on the router, not a network administrator managing hundreds of clients.

## How the hash-in-flash lookup works

The README gives the data flow directly. A query arrives, the firmware extracts the domain, hashes it with FNV-1a, and also hashes the parent suffixes so that a rule for example.com can cover ads.example.com. It then binary-searches the sorted hash table in flash. A hit is answered with 0.0.0.0. A miss is forwarded to the upstream resolver and the reply is relayed back.

The design hinges on the hash width. At 40 bits, the birthday bound puts collisions at roughly zero for 141k domains and about one at 537k, meaning one unlucky domain gets over-blocked. The README argues 32 bits would save about 20 percent of flash but cost around seven collisions at 250k domains, while 64 bits wastes three bytes per domain. That is a defensible trade, and the README is unusually honest that the same trick is not C3-specific: on a 16 MB ESP32-S3 the hashes hold about 2.7M domains versus roughly 466k for strings in 8 MB of PSRAM.

The cost is flash reads. A binary search over 141k entries is about 18 reads, and the README quotes roughly 10 ms per lookup including WiFi round-trip time. The repository lists a bucketed prefix index as an open item (issue #3) that would cut that to one or two reads. Until that lands, throughput is bounded by those reads.

## Build and flash with PlatformIO

The README describes one USB flash and then everything else over WiFi. The toolchain is PlatformIO, and the README warns that the distro or apt package (for example 4.3.4) is too old and fails with an AttributeError mentioning resultcallback. Use the VSCode extension's bundled core or a pip-installed PlatformIO in a virtual environment.

WiFi credentials are optional at compile time. Copy the example header and edit it, or skip this entirely and use the on-device setup portal later.

```bash
cp src/secrets.example.h src/secrets.h
# edit src/secrets.h -> WIFI_SSID / WIFI_PASS
```

Next, build the blocklist binary. The default is described as StevenBlack base plus Hagezi Light, around 140k domains, with WhatsApp and social domains left alone.

```bash
python3 tools/build_blocklist.py data/blocklist.bin
```

Then flash both the firmware and the filesystem image. The filesystem upload is what carries the blocklist, so skipping it leaves the device without rules.

```bash
pio run -t upload
pio run -t uploadfs
pio device monitor
```

The monitor should show a boot log and the mDNS name, http://c3adblock.local. If credentials were never set, the README says the board starts an open access point named C3-AdBlock-XXXX with a captive portal: join it from a phone, pick the network, enter the password. To move it later, open http://c3adblock.local/forgetwifi or hold the BOOT button while powering on.

## Pointing clients at it and confirming a block

Once the board is on the network, point a client's DNS at its IP, or add it as a secondary resolver behind your main DNS. The README's own test uses dig against the device address.

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

A blocked name should come back as 0.0.0.0 and an unlisted name should return its real address. The dashboard at http://c3adblock.local reports per-client block and allow counts, lets you ban a client, and accepts custom domains. Firmware and blocklist updates also happen there: upload a freshly built blocklist.bin under Blocklist, or set a URL under Remote auto-update so the device pulls a prebuilt file on a schedule, for example a GitHub release asset that every device fetches. Firmware goes up under Firmware, or over the air from the CLI.

```bash
pio run -t upload --upload-port c3adblock.local --upload-protocol espota
```

## The 4 MB flash tradeoff and other failure modes

Firmware OTA needs two application slots, which the README says leaves about 1.3 MB for the blocklist, roughly 250k domains. The aggressive 537k list only fits the single-app partition table, and that table removes firmware OTA. This is a real fork in the road, not a footnote: you either keep the ability to update firmware over WiFi or you keep the biggest list, and the choice lives in partitions.csv. The README does not document rollback if an OTA image misbehaves.

Hardware is the second failure mode. The README warns that cheap or loose USB-C to USB-A adapters can brown out the radio during WiFi transmit, so the power source matters. On the host side, ModemManager on Fedora and Ubuntu grabs /dev/ttyACM0, toggles DTR and RTS, and resets the board, blocking serial. The README gives a udev rule to exempt the device by vendor ID. There is also a protocol constraint: DNS clients add an EDNS OPT record, and a blocked reply must contain only the question and answer (ANCOUNT=1, NSCOUNT=ARCOUNT=0) or it is malformed. That is a correctness trap for anyone modifying the response path.

Where it is the wrong tool: it is not a DHCP server. The repository lists that as an open item, so it cannot hand itself out as DNS for true plug-and-play. It also has no per-client policy engine beyond ban and allow counts, so a network that needs different rules per device or per group is out of scope.

## Alternatives and how they differ

The README credits s60sc/ESP32_AdBlocker as the inspiration, and the difference is the storage strategy. That project follows the string-in-RAM approach, which is why it expects PSRAM and a more expensive board. esp32-c3-adblock trades exact domain storage for fixed-width hashes and accepts the small collision risk in exchange for running on a board with no PSRAM at all.

Pi-hole itself is the other reference point, and the README frames the project as Pi-hole-class. The difference is scope rather than capability: Pi-hole runs on a general-purpose host with a full database, per-client groups, and a long history of blocklist tooling. This firmware fits in a dongle and draws power from a router's USB port. If you already run a Pi-hole, this is a redundant second resolver, not a replacement. If you want a filtering resolver with no always-on computer, this is the cheaper path.

## Licence and maintenance cost

The repository is MIT licensed, which permits commercial use and modification provided the copyright notice and permission notice are preserved. That is the whole of the licence implication here; nothing in the repository suggests any additional restriction, and this is not legal advice.

The last push to the default branch was on 2026-08-01, and the repository is not archived. The README's done list covers the dashboard, mDNS, OTA, and the captive portal. Two items remain open: the bucketed prefix index and acting as a DHCP server. Upgrading is mostly a matter of pulling a new firmware.bin through the dashboard or espota, plus rebuilding blocklist.bin with tools/build_blocklist.py if the list format changed. The real upgrade cost is the partition decision: if you chose the single-app table for the 537k list, every firmware update means a USB flash.

## Conclusion

Adopt esp32-c3-adblock if you want a cheap, low-power DNS sinkhole for one network and you are comfortable flashing with PlatformIO and editing partitions.csv. Do not adopt it if you need per-device filtering at scale, a DHCP server that hands itself out as DNS, or a blocklist larger than roughly 250k domains while keeping firmware OTA. Before you buy a board, verify that your PlatformIO is current (the distro package fails with an AttributeError on resultcallback), and decide up front whether you want the two-app partition table with ~1.3 MB of blocklist space or the single-app table that fits the 537k list but removes firmware OTA.

## FAQ

### What is the purpose of esp32-c3-adblock?

It is a Pi-hole-style DNS ad-blocker that runs on an ESP32-C3 without PSRAM. It answers blocklisted lookups with 0.0.0.0 and forwards everything else to an upstream resolver.

### Can an ESP32 DNS server block ads?

Yes. This project does it by extracting the domain from each DNS query, hashing it with FNV-1a, and binary-searching a sorted hash table in flash. A hit is answered with 0.0.0.0 and a miss is forwarded upstream.

### Which is better for esp32-c3-adblock, ESP32 or ESP32-C3?

The README argues the hash-in-flash trick is not a C3 workaround and beats strings in PSRAM on bigger chips too. On a 16 MB ESP32-S3 the hashes hold about 2.7M domains versus roughly 466k for strings in 8 MB of PSRAM.

### Why isn't esp32-c3-adblock illegal?

The repository does not discuss the legality of ad blocking. It is MIT licensed, which covers use and modification of the code.

## Sources

- [Issues](https://github.com/M-Abozaid/esp32-c3-adblock/issues)
- [License: MIT](https://github.com/M-Abozaid/esp32-c3-adblock/blob/main/LICENSE)
- [M-Abozaid/esp32-c3-adblock on GitHub](https://github.com/M-Abozaid/esp32-c3-adblock)
- [README](https://github.com/M-Abozaid/esp32-c3-adblock/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/m-abozaid-esp32-c3-adblock
