プログラミング
ESP32-C3 Adblock
要約
ESP32-C3マイクロコントローラー上で動作する、Pi-holeスタイルのDNS広告ブロッカー「ESP32-C3 Adblock」が開発されました。このプロジェクトは、ブロックリストをRAMに保持する必要がなく、フラッシュメモリにソートされた40ビットハッシュとして保存することで、PSRAM不要で約0.7MBのフラッシュと約50KBのRAMで140,000以上のドメインを約10ミリ秒で処理できる革新的な手法を採用しています。
全文翻訳
esp32-c3-adblock 日本語 README
PSRAM不要の2ドルESP32-C3で動作するPi-holeスタイルのDNS広告ブロッカー。
📰 Tom's Hardware、XDA Developers、Korbenで紹介されました。
誰もが見落としているトリック:ブロックリストをRAMに保持する必要はありません。ドメインをフラッシュにソートされた40ビットハッシュとして保存し、バイナリサーチします。140,000以上のドメインが約0.7MBのフラッシュに収まり、約10ミリ秒でマッチングされ、約50KBのRAMを使用します。
クエリ → ドメイン抽出 → FNV-1aハッシュ(親サフィックスを含む) → フラッシュハッシュテーブルのバイナリサーチ
├─ ヒット → 0.0.0.0として応答(シンクホール)
└─ ミス → アップストリームリゾルバに転送、応答を中継
なぜこれが興味深いのか
ほとんどのESP32 DNSシンクホールは、ブロックリスト(ドメイン文字列)をRAMにロードするため、PSRAMを要求します。このプロジェクトは、代わりにフラッシュに固定5バイト(40ビット)ハッシュを保存します。
文字列インRAMアプローチ
ハッシュインフラッシュ
ハードウェア
ESP32 + PSRAM(約8ドル)
ESP32-C3、PSRAMなし(約2ドル)
141kドメイン
約2.5MBのRAM
0.67MBのフラッシュ
RAM
ほとんどすべてを使用
約50KB
ルックアップ
文字列比較
約18回のフラッシュ読み取り(WiFi RTT込みで約10ミリ秒)
衝突
該当なし
141kで0(537kで1(つまり、1つの不運なドメインが過剰にブロックされる))
なぜ40ビットなのか?
このフラッシュ予算のスイートスポットです。衝突はバースデーバウンドに従います—141kドメインで約0、537kで約1(つまり、1つの不運なドメインが過剰にブロックされます)。32ビットに下げるとフラッシュの20%が節約されますが、250kで約7回の衝突が発生します。64ビットにすると、問題がない問題(つまり、問題がない)を解決するためにドメインあたり3バイトが無駄になります。
同じトリックはより大きなチップでも機能します—C3の回避策ではありません。16MBのESP32-S3では、これらのハッシュは8MBのPSRAMの文字列の約466kに対して約2.7Mドメインを保持します。フラッシュ内のハッシュは、PSRAM内の文字列よりも基本的にどこでも優れています。C3はそれを否定できないものにするだけです。
ハードウェア
ESP32-C3ボード(C3 SuperMiniでテスト済み)、4MBフラッシュ、PSRAM不要
クラシックESP32(DevKit / WROOM、4MB)もビルド可能:pio run -e esp32dev -t upload(コミュニティ提供、コンパイルテスト済み;C3がテスト済みターゲットです)
安定したUSBソース(携帯電話充電器またはルーターのUSBポート)から電源を供給してください。安価/緩いUSB-C→Aアダプターは、WiFi送信中にラジオをブラウンアウトさせる可能性があります。USB-A → USB-Cドングルを使用すると、ほとんどのルーターの背面にあるスペアUSBポートに直接接続できます。電源不要、追加の箱不要。
エンクロージャー
C3 SuperMini用の印刷可能なケース:hardware/esp32-c3-supermini-enclosure.stl
印刷上の注意:サポートは不要です。0.2mmレイヤー、約15%インフィルで十分です。アンテナ端をクリアに保ってください。C3のPCBアンテナは、USB-Cポートの反対側の短いエッジにあるジグザグトレースです。それを固体プラスチックに埋め込んだり、金属を近づけたりしないでください。そうしないと、RSSIが悪化します。通気口を開けておいてください:ボードはアイドル時に約45〜55°Cになります。
ビルドとフラッシュ(PlatformIO)
開始するための1回のUSBフラッシュ。その後、ファームウェアとブロックリストの両方がWiFi経由で更新されます(下記参照)。
⚠️ 現在のPlatformIOを使用してください—VSCode PlatformIO拡張機能にバンドルされているコア、またはvenvにpip install -U platformio。ディストリビューション/aptのplatformioパッケージ(例:4.3.4)は古すぎて、AttributeError: ... 'resultcallback'(issue #4)で失敗します。ワンクリックブラウザインストーラーが進行中です(ホスティングは未定)。
# 1. secretsテンプレートをコピー(gitignoreされており、ローカルに保持)して編集します:
# - WIFI_SSID / WIFI_PASSはオプションです—プレースホルダーを残して、代わりに
# オンデバイスセットアップポータルを使用します(下記参照)。
# - WEB_USER / WEB_PASS / OTA_PASSは必須です:これらはダッシュボードの
# 状態変更エンドポイント(/ban、/addblock、/upload、/update、/setupdate、
# /forgetwifi)およびネットワークOTAをゲートします。実際の値を設定してください—これらは以前はLAN上の誰でもアクセス可能でした。
cp src/secrets.example.h src/secrets.h
# 次にsrc/secrets.hを編集します
# 2. ブロックリストハッシュテーブルをビルドします(デフォルト=StevenBlackベース+Hagezi Light、
# 約10万エントリ、WhatsApp/ソーシャルセーフ)
python3 tools/build_blocklist.py data/blocklist.bin
# 3. ファームウェア+ブロックリストファイルシステムをフラッシュします(唯一のUSBフラッシュ)
pio run -t upload
pio run -t uploadfs
# 4. ブートを監視し、IPをメモするか、ダッシュボードを開きます
pio device monitor
# → http://c3adblock.local
独自のブロックリスト
build_blocklist.py OUT.bin [SOURCE ...] は、URLとローカルファイルの任意の組み合わせを受け入れます。これらの形式のいずれかです:
ホストファイル — 0.0.0.0 ads.example.com tracker.example.com (行のすべてのドメインが含まれます)
プレーンなドメインリスト — 1行に1ドメイン
AdGuard / Adblockの基本ルール — ||ads.example.com^ ブロック、@@||ok.example.com^ ドメインを削除します(例:AdGuard Homeの許可リストをミラーリングするため)
ブロックされたドメインはサブドメインもブロックします。DNSハッシュリストでは表現できないルール(正規表現、ワイルドカード、$修飾子、化粧品##ルール)はスキップされ、カウントされます。@@ルールは、その正確なエントリのみをブロック解除します—ブロックされた親からサブドメインを切り出すことはできません。ソースがダウンロードできない場合、ビルドは小さいリストをサイレントに生成する代わりに停止します(--allow-missingで上書き可能)。
WiFiセットアップ(再フラッシュ不要)
接続できない場合(またはsecrets.hを設定しなかった場合)、オープンアクセスポイントC3-AdBlock-XXXXをキャプティブポータルで起動します—電話から接続し、ネットワークを選択し、パスワードを入力すれば完了です。後で別のネットワークに移動するには:ダッシュボードで[Forget WiFi]をクリックするか、電源投入中にBOOTボタンを押し続けると、セットアップポータルが再度表示されます。(/forgetwifiは現在認証が必要なため、単なるURLではなくなりました—下記セキュリティ参照。)
オーバー・ザ・エア・アップデート(USB不要)
ダッシュボード(http://c3adblock.local)ですべてを行います:
ブロックリスト — 新しくビルドされたblocklist.binを[Blocklist] → [Upload]にドロップするか、[Remote auto-update]の下にURLを設定すると、デバイスはスケジュールに従って事前ビルドされたblocklist.binを取得します。GitHub Actionsによって毎週月曜日に新しいデフォルトリストが再ビルドされ、安定したURLで公開されるため、これを一度貼り付けるだけでデバイスは自動的に最新の状態に保たれます:https://github.com/M-Abozaid/esp32-c3-adblock/releases/download/blocklist/blocklist.bin
ファームウェア — [Firmware] → [OTA update]の下で.pio/build/c3/firmware.binをアップロードします。デバイスはそれを検証し、新しいイメージにリブートします。またはCLIからWiFi経由でプッシュします:pio run -t upload --upload-port c3adblock.local --upload-protocol espota
4MBフラッシュのトレードオフ:ファームウェアOTAには2つのアプリスロットが必要で、ブロックリストには約1.3MB(最大約250kドメイン)が残ります。積極的な537kの「アルティメット」リストは、シングルアプリパーティションテーブル(ファームウェアOTAなし)にしか収まりません。partitions.csvでトレードオフを選択してください。
セキュリティ
ダッシュボードの読み取り専用ビュー(/、/stats.json)は開いたままですが、状態変更エンドポイントはすべてHTTP Basic Auth(secrets.hのWEB_USER/WEB_PASS)が必要です:
/ban、/addblock、/unblock、/forgetwifi
/upload、/update(ブロックリストおよびファームウェアOTA)
/setupdate、/fetchnow
ネットワークOTA(ArduinoOTA、例:pio run -t upload --upload-port c3adblock.local --upload-protocol espota)は、同じファイルからのOTA_PASSが必要です。
これがないと、LAN上のデバイスに到達できる誰でも、任意のファームウェアでデバイスを再フラッシュしたり、ブロックリストを書き換えたりできます。ネットワーク上のすべてのDNSクエリのパスにデバイスが存在することを考えると、これは知っておく価値があります。
カスタムブロックドメイン名も、ダッシュボードにレンダリングされる前にHTMLエスケープされるため、ドメイン文字列にマークアップが含まれている場合(/addblock経由で追加された場合)に、表示ブラウザで実行される可能性のある格納型XSSパスが閉じられます。
ここでのBasic Authは暗号化ではなく、LAN信頼境界の制御です。すべてが:80でプレーンHTTPです—このチップにはTLSサーバーを実行するための現実的な予算がありません。Basic Authの認証情報はBase64(暗号化されていない)で、認証されたすべてのリクエストで送信されます。LANトラフィックをスニッフィングできる人(オープン/ゲストWiFi、ARPスプーフィング)は、ワイヤーからそれらを読み取ることができます。これは、ネットワーク上の攻撃者に対してではなく、一般的なケース(認証情報なしでAPIにアクセスするネットワーク上の別のデバイス、またはそれをCSRFするブラウザタブ)に対して強化します。
キャッシュされたBasic AuthによるCSRF:ブラウザは、既に認証されたオリジンへの後続のリクエスト(例えば、<img src="http://c3adblock.local/forgetwifi">、JS不要)に、キャッシュされたBasic Auth認証情報を自動的に添付します。これにより、ダッシュボードに一度ログインすると、LAN上に誰がいるかに関係なく、任意のWebページがこのAPIをサイレントに駆動できるようになります。上記のすべての変更エンドポイントは、現在も