esp32-c3-adblock: DNS ad-blocker voor de ESP32-C3

De technische aanpak

De kern van dit project is de manier waarop de blokkeerlijst wordt beheerd. In plaats van de lijst in het RAM-geheugen te bewaren, worden de domeinen opgeslagen als gesorteerde 40-bit hashes in het flashgeheugen. Hierdoor passen meer dan 140.000 domeinen in ongeveer 0,7 MB flash, waarbij matches in circa 10 ms worden gevonden en er slechts ~50 KB RAM wordt gebruikt.

Het procesverloop: Query → domein extraheren → FNV-1a hash (+ parent suffixes) → binair zoeken in de flash-hashtabel

  • Hit: Antwoord 0.0.0.0 (sinkholed).
  • Miss: Doorsturen naar de upstream resolver en het antwoord relayeren.

Waarom deze methode?

De meeste DNS-sinkholes voor de ESP32 laden de blokkeerlijst (domeinstructs) in het RAM, waardoor ze PSRAM vereisen. Dit project gebruikt vaste 5-byte (40-bit) hashes in flash.

KenmerkString-in-RAM benaderingHash-in-flash benadering (dit project)
HardwareESP32 + PSRAM (~$8)ESP32-C3, geen PSRAM (~$2)
141k domeinen~2.5 MB RAM0.67 MB flash
RAM-gebruikMeestal volledig bezet~50 KB
ZoektijdString-vergelijking~18 flash-reads (~10 ms incl. WiFi RTT)
CollisiesN.v.t.0 bij 141k (1 bij 537k)

De keuze voor 40 bits

40 bits is het ideale punt voor dit flash-budget. Collisies volgen de birthday bound: bij 141.000 domeinen zijn er vrijwel geen collisies, bij 537.000 is er ongeveer één (één onfortuinlijk domein wordt dus onterecht geblokkeerd). Een verlaging naar 32 bits zou 20% flash besparen, maar zou leiden tot circa 7 collisies bij 250.000 domeinen. Een verhoging naar 64 bits zou 3 bytes per domein verspillen om een probleem op te lossen dat er nauwelijks is.

Deze techniek is niet alleen een workaround voor de C3; het werkt op grotere chips. Op een ESP32-S3 met 16 MB flash kunnen deze hashes circa 2,7 miljoen domeinen bevatten, tegenover 466.000 voor strings in 8 MB PSRAM.

Hardware

  • Board: Elke ESP32-C3 board (getest op C3 SuperMini), 4 MB flash, geen PSRAM nodig.
  • Alternatieven: De klassieke ESP32 (DevKit / WROOM, 4 MB) is ook ondersteund via pio run -e esp32dev -t upload (community-bijdrage, compile-getest; de C3 is het primaire doelwit).
  • Voeding: Gebruik een stabiele USB-bron (telefoonlader of USB-poort van de router). Goedkope of loszittende USB-C → A adapters kunnen zorgen voor brown-outs van de radio tijdens WiFi-transmissie.

Behuizing

Er is een printbaar STL-bestand beschikbaar voor de C3 SuperMini: hardware/esp32-c3-supermini-enclosure.stl.

Printinstructies:

  • Geen supports nodig; 0.2 mm layers, ~15% infill.
  • Houd het uiteinde van de antenne vrij. De PCB-antenne van de C3 is de zig-zag trace op de korte zijde tegenover de USB-C poort; bedek deze niet met massief plastic of metaal om het signaal (RSSI) niet te verslechteren.
  • Laat de ventilatieopeningen open: het board wordt in rust ongeveer 45–55 °C.

Installatie en Flashen (PlatformIO)

Na de eerste USB-flash kunnen zowel de firmware als de blokkeerlijst via WiFi worden bijgewerkt.

Let op: Gebruik een actuele versie van PlatformIO. De gebundelde core in de VSCode extensie of pip install -U platformio in een venv wordt aangeraden. Oude distro/apt packages (bijv. 4.3.4) falen met een AttributeError: ... 'resultcallback'.

Stappenplan:

  1. Secrets configureren: Kopieer het template en pas het aan:

cp src/secrets.example.h src/secrets.h

  • WIFISSID / WIFIPASS zijn optioneel (gebruik anders de on-device setup portal).
  • WEBUSER / WEBPASS / OTA_PASS zijn verplicht. Deze beveiligen de endpoints van het dashboard (/ban, /addblock, /upload, /update, /setupdate, /forgetwifi) en netwerk-OTA.
  1. Blokkeerlijst bouwen: (Standaard: StevenBlack base + Hagezi Light, ~100k entries)

python3 tools/build_blocklist.py data/blocklist.bin

  1. Flashen:

pio run -t upload pio run -t uploadfs

  1. Starten: Open de seriële monitor om het IP-adres te vinden of ga naar http://c3adblock.local.

Eigen blokkeerlijsten

Het script build_blocklist.py OUT.bin [SOURCE ...] accepteert URL's en lokale bestanden in de volgende formaten:

  • Hosts bestanden: 0.0.0.0 ads.example.com tracker.example.com
  • Plain domain lists: Eén domein per regel.
  • AdGuard / Adblock basisregels: ||ads.example.com^ blokkeert, @@||ok.example.com^ verwijdert een domein.

Een geblokkeerd domein blokkeert automatisch ook alle subdomeinen. Regels die een DNS-hashlijst niet kan uitdrukken (regex, wildcards, $ modifiers, cosmetische ## regels) worden overgeslagen.

Configuratie en Updates

WiFi-instellingen

Indien het apparaat niet kan verbinden (of secrets.h niet is ingesteld), start het een open access point C3-AdBlock-XXXX met een captive portal. Via een telefoon kunnen de netwerkgegevens worden ingevoerd. Om later naar een nieuw netwerk over te stappen: klik op "Forget WiFi" in het dashboard of houd de BOOT-knop ingedrukt tijdens het opstarten.

Over-the-air (OTA) updates

Het dashboard op http://c3adblock.local beheert het volgende:

  • Blokkeerlijst: Upload een nieuw blocklist.bin bestand of stel een URL in voor automatische updates. Er is een standaardlijst beschikbaar die elke maandag wordt bijgewerkt via GitHub Actions.
  • Firmware: Upload firmware.bin via het dashboard of push via de CLI:

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

Trade-off bij 4 MB flash: Firmware OTA vereist twee app-slots, wat ongeveer 1,3 MB overlaat voor de blokkeerlijst (~250k domeinen max). De "ultimate" lijst van 537k domeinen past alleen in een single-app partition table (geen firmware OTA). De keuze hiervoor staat in partitions.csv.

Beveiliging

De read-only weergave (/, /stats.json) is open, maar alle state-changing endpoints vereisen HTTP Basic Auth (WEBUSER/WEBPASS):

  • /ban, /addblock, /unblock, /forgetwifi
  • /upload, /update (blokkeerlijst en firmware OTA)
  • /setupdate, /fetchnow

Netwerk OTA (ArduinoOTA) vereist de OTA_PASS.

Belangrijke beveiligingsdetails:

  • HTTP Basic Auth: Dit is een controle voor de LAN-trust-boundary, geen versleuteling. Alles verloopt via plain HTTP op poort 80, omdat de chip geen budget heeft voor een TLS-server. Gevoelige gegevens kunnen door iemand die het LAN-verkeer kan sniffen worden gelezen.
  • CSRF-beveiliging: Om te voorkomen dat een externe website via de browser acties uitvoert op het dashboard (CSRF), vereist elk muterend endpoint nu een custom X-Requested-With: c3-adblock header. Dit is de reden waarom /forgetwifi niet langer een directe URL is die je simpelweg kunt bezoeken.
  • Standaard inloggegevens: Indien de placeholders uit secrets.example.h worden behouden, start het apparaat met publiek bekende wachtwoorden. Dit wordt gemeld in de seriële logs en via een banner op het dashboard.
  • WiFi Setup Portal: Het access point voor de initiële setup is onversleuteld, zodat deze zonder wachtwoord toegankelijk is.

Gebruik en Testen

Stel het DNS-adres van een apparaat in op het IP van de C3, of voeg het toe als secundaire resolver.

Testen met dig:

  • dig @<c3-ip> doubleclick.net → 0.0.0.0 (geblokkeerd)
  • dig @<c3-ip> github.com → echt IP (doorgestuurd)

Bekende problemen (Gotchas)

  • ModemManager (Fedora/Ubuntu): Deze service grijpt vaak /dev/ttyACM0 aan, wat de C3 reset en seriële communicatie blokkeert.
  • Oplossing: Stop ModemManager of voeg een udev-regel toe:

echo 'ATTRS{idVendor}=="303a", ENV{IDMMDEVICE_IGNORE}="1"' | sudo tee /etc/udev/rules.d/99-esp-no-modemmanager.rules

  • USB-Serial-JTAG: De console kan vroege boot-output opslokken totdat de host verbinding maakt.
  • DNS-compatibiliteit: DNS-clients voegen vaak een EDNS OPT record toe; een geblokkeerd antwoord moet uitsluitend de vraag + antwoord bevatten (ANCOUNT=1, NSCOUNT=ARCOUNT=0), anders is het malformed.

Status en Toekomst

Voltooid:

  • Webdashboard (per-client counts, ban client, custom domains).
  • mDNS (c3adblock.local) voor discovery.
  • OTA voor firmware en blokkeerlijsten (incl. scheduled pulls).
  • Captive-portal WiFi setup en browser-web-installer.

Toekomstige plannen:

  • Bucketed prefix index: Verminderen van flash-reads per lookup van ~18 naar ~1–2 voor betere throughput.
  • DHCP-server functionaliteit: Zichzelf aanbieden als DNS voor echte plug-and-play.

Credits & Licentie

Geïnspireerd door s60sc/ESP32_AdBlocker voor het idee om 0.0.0.0 terug te sturen voor geblokkeerde domeinen. Dit is een onafhankelijke implementatie vanaf nul, gericht op hash-in-flash optimalisatie voor chips zonder PSRAM.

Licentie: MIT.