☰
53万条黑名单之外的隐藏坑:构建失败、容量撑爆与重启失联
2026/10/13 3:24:24 网站建设 项目流程

53万条黑名单之外的隐藏坑:构建失败、容量撑爆与重启失联

【免费下载链接】esp32-c3-adblockPi-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项目地址: https://gitcode.com/GitHub_Trending/es/esp32-c3-adblock

一台约 2 美元的 ESP32-C3(无 PSRAM),把最多 53.7 万个广告域名压成 40-bit FNV-1a 哈希写进 Flash,用二分查找在约 10 毫秒内裁决一次 DNS 查询,全程只占约 50 KB 内存——这是 esp32-c3-adblock 最出圈的叙事。社区文章几乎都聚焦"哈希存 Flash 如何吊打字符串存 RAM",但真正把设备长期跑在家庭网络里的用户,遇到的麻烦几乎都集中在三个没人写的地方:黑名单构建与同步的失败现场、4 MB Flash 容量逼近时的结构性取舍、以及升级或换网后的"失联"与救砖路径。本文直接对照仓库源码逐层复盘这三个坑,给出可操作的规避方式。

一、黑名单构建与同步失败的现场复盘

1.1 第一道坎:构建工具链的版本陷阱

项目使用 PlatformIO 构建,platformio.ini 默认目标为c3(esp32-c3-devkitm-1,板载 4 MB Flash)。构建失败的案例里,最高频的根因不是代码,而是发行版自带的旧版 PlatformIO。README 的警告写得很直白:apt 源里的platformio4.3.4 太旧,会直接以AttributeError: ... 'resultcallback'崩溃(对应 issue #4)。官方 CI 在 .github/workflows/build.yml 中固定走pip install -U platformio,本地复现这个环境即可绕坑——或者直接用 VSCode PlatformIO 扩展内置的 core。

另一个常被忽略的点是secrets.h:它由 src/secrets.example.h 复制而来且被 gitignore,CI 里也是cp占位符版本编译(固件会在连不上 WiFi 时自动落入配置门户,见第三节)。如果你本地的secrets.h是从旧版本仓库拷来的,字段缺失或宏冲突同样会导致编译失败。

1.2 第二道坎:数据源失效导致的"静默缩水"

黑名单构建工具 tools/build_blocklist.py 默认拉取 StevenBlack hosts + Hagezi Light 两路数据源,产出约 10 万条目的哈希表。它的失败处理是刻意设计过的"响亮失败":

except Exception as e: # Fail loudly: silently dropping a source shrinks the list without anyone noticing # (happened when Hagezi retired domains/light.txt). Pass --allow-missing to continue. print(f' !! FAILED to read {src}: {e}', file=sys.stderr) if not ALLOW_MISSING: sys.exit(1) continue

注释记录了真实事故:上游 Hagezi 曾下线过domains/light.txt这个文件,导致列表"少了一路数据源却没人察觉"。于是工具默认在任意源下载失败时直接exit(1),而不是产出一个小一截的 bin。配套的每周自动化构建 .github/workflows/blocklist.yml 也加了双重护栏:

test $((size % 5)) -eq 0 # whole 40-bit entries test "$size" -gt 250000 # >50k entries, catches a silently shrunk list test "$size" -lt 1300000 # fits the dual-OTA filesystem partition

大于 250000 字节防缩水、小于 1300000 字节防超容——这两条正是本文标题里两个坑的机器化表达。

1.3 第三道坎:上传一个"不是 5 的倍数"的文件

构建只是第一步,把 bin 送进设备是第二步。固件端的提交校验在 src/main.cpp 的commitNewBlocklist():

static bool commitNewBlocklist() { // /blocklist.new -> live (validated) File f = LittleFS.open("/blocklist.new", "r"); size_t sz = f ? f.size() : 0; if (f) f.close(); bool ok = sz > 0 && (sz % HASH_BYTES) == 0; // sorted hash blob -> 5-byte multiple if (ok) LittleFS.rename("/blocklist.new", BLOCKLIST_PATH); else LittleFS.remove("/blocklist.new"); reopenBlocklist(); return ok; }

设备只认"空文件或者长度是 5 的整数倍"两种形态。但真正的坑在它前面的beginBlocklistSwap():

static void beginBlocklistSwap() { if (blocklist) blocklist.close(); numHashes = 0; LittleFS.remove(BLOCKLIST_PATH); LittleFS.remove("/blocklist.new"); }

旧列表先删、新列表后校验。如果你上传的 bin 长度不是 5 的倍数、或者写入途中 LittleFS 空间不足、又或者刷机中途断电,设备会带着numHashes = 0重启——拦截直接停摆。此时固件进入 Fail-Open:所有查询原样转发上游,宁可放行广告也不误杀正常流量(isBlocked在numHashes == 0时直接返回 false)。这个设计保住了网络可用性,但也意味着"上传失败"和"拦截失效"是同一件事,需要及时察觉 dashboard 上 domains 计数归零。

1.4 远程自动更新的连锁反应

走 "Remote auto-update" 定时拉取的路径同样隐藏着连锁坑。fetchBlocklist()的实现要点:

  • WiFiClientSecure使用setInsecure()(黑名单本身不是机密,跳过证书固定);
  • HTTPC_STRICT_FOLLOW_REDIRECTS处理 GitHub release 到 CDN 的跳转;
  • 非 200 状态直接记入updateStatus(如"HTTP 404"),不进入交换流程;
  • 流式下载中 15 秒无数据则 break,随后commitNewBlocklist()失败,同样回到 Fail-Open。

定时更新默认 24 小时一次(updateIntervalH),也就是说只要远端 URL 失效一次,你的网络就可能在不知不觉中"去广告失效"一整天——dashboard 的upstat字段(src/main.cpp 的handleStats)是排查这类问题的第一现场。

二、4 MB Flash 容量逼近时的表现

2.1 分区表里的数学题

社区热炒的"53.7 万条"并非默认配置。默认的双 OTA 分区表 partitions.csv 长这样:

app0, app, ota_0, 0x10000, 0x150000 app1, app, ota_1, 0x160000, 0x150000 spiffs, data, spiffs, 0x2b0000, 0x150000

两个应用槽各 1.3125 MB(0x150000 = 1,376,256 字节),留给黑名单的 LittleFS 分区也只有 1.3125 MB。换算一下:

  • 仓库自带的 blocklist.bin 实测 521,905 字节 = 104,381 条 × 5 字节 ≈ 0.5 MB,这是日常默认档;
  • 25 万条 × 5 字节 ≈ 1.19 MB,勉强塞进 1.3125 MB 的文件系统——这就是 README 里"250k domains max"的来源;
  • 53.7 万条 × 5 字节 ≈ 2.56 MB,在双 OTA 布局下物理上放不下。

2.2 "容量撑爆"的真相:OTA 与容量二选一

53.7 万条的 "ultimate" 列表只在"单应用分区表(无固件 OTA)"下成立。原因在分区注释里写得很清楚:固件为了支持 TLS + HTTP + OTA 已经膨胀到 1.24 MB(docs/firmware.bin 实测 1,299,024 字节),而每个应用槽只有 1.3125 MB,槽内余量仅约 77 KB。所以:

  • 选双 OTA 分区表 → 固件可无线升级,但黑名单上限约 25 万条;
  • 选单应用分区表 → 塞得下 53.7 万条哈希,但固件只能靠 USB 刷。

这不是"Flash 不够大"的故障,而是分区表级别的结构性取舍。想在 4 MB 上既要 OTA 又要极限容量,唯一的出路是砍固件体积(比如去掉 TLS 客户端)或换 8/16 MB 芯片——README 里也点明了同样的哈希方案在 16 MB ESP32-S3 上可装约 270 万条。

2.3 容量逼近时的两个隐性表现

第一,文件系统写满的失败模式。上传超大的 bin 时,beginBlocklistSwap()已把旧列表删除,写入/blocklist.new若因空间不足中途失败,设备回到 Fail-Open。CI 的-lt 1300000检查就是提前在源头拦住这类文件,手动构建时同样要盯这个 1.3 MB 红线。

第二,哈希碰撞的统计学代价。README 给出了生日界限的数据:141k 条时碰撞约 0 个,537k 条时约 1 个——即极限容量下平均有一个无辜域名会被连带拦截(40-bit 下 32-bit 会到约 7 个碰撞/25 万条)。容量越逼近上限,这个"误杀"概率越高,属于哈希表方案固有的隐形成本,选择 "ultimate" 列表前应有心理预期。

三、升级失联后的救砖路径

3.1 串口"失联":先查 ModemManager 和 USB-Serial-JTAG

很多"没输出、连不上"其实不是固件问题。README 的 Gotchas 记载了两个高频案例:

  • ModemManager 抢口:Fedora/Ubuntu 默认的 ModemManager 会抓取/dev/ttyACM0并切换 DTR/RTS,这动作会直接复位 C3并阻塞串口。标准解法是停服务加 udev 规则(ATTRS{idVendor}=="303a", ENV{ID_MM_DEVICE_IGNORE}="1")。
  • USB-Serial-JTAG 吞日志:C3 的串口控制台在主机尚未连接时会吞掉早期启动输出,while(!Serial)是常见补救。看到黑屏先排查这两点,别急着重刷。

3.2 固件 OTA 失败与"起不来"的救砖

固件 OTA 走 ArduinoOTA 与浏览器上传双通道(src/main.cpp 的handleFwUpload/handleFwUpdateDone),流程是Update.begin(UPDATE_SIZE_UNKNOWN)→ 边收边写 →Update.end(true)校验 →ESP.restart()。任何一步失败都会Update.printError(Serial)打印原因且不重启。真正的险情是烧入了一个能通过校验但起不来的固件(分区表不匹配、镜像损坏等),重启后 mDNS 与 dashboard 双双消失——这才是"升级失联"的最坏形态。救砖路径有三层:

  1. USB 强制重刷:插 USB 线用pio run -t upload覆盖(PlatformIO 会在下载模式自动协商);
  2. 浏览器救砖:仓库 docs/index.html + docs/manifest.json 提供 esp-web-tools 一键刷写(Chrome/Edge 桌面),manifest 里固化了 bootloader、partitions、boot_app0、firmware、littlefs 各偏移——即使设备完全变砖,浏览器插 USB 约 30 秒即可恢复,且littlefs.bin自带满配约 14.1 万条域名的文件系统镜像;
  3. BOOT 键进下载模式:上电瞬间按住 BOOT(C3 为 GPIO9,固件在 src/main.cpp 中按CONFIG_IDF_TARGET_ESP32C3区分)再插 USB,可稳定进入串口下载模式。

3.3 WiFi"失联"不等于变砖:三层兜底

这是最容易误判的一类。固件的启动逻辑(setup()与startConfigPortal())提供了完整的三层兜底:

  • 未配置或连不上:自动开启开放 APC3-AdBlock-XXXX,并用 DNSServer 在 53 端口 catch-all,手机连上即弹强制门户,选网输密码后保存进 NVS 并重启(handleWifiSave);
  • 已配置但暂时连不上(路由器重启、信号弱):门户循环里 3 分钟无设备接入会自动ESP.restart()重试 WiFi,不会卡死在配置页;
  • 想换网络:dashboard 的 Forget WiFi(需 Basic Auth + CSRF 头)或上电按住 BOOT清除 NVS 凭据,设备回到门户状态。

也就是说,只要没刷坏固件,设备"失联"几乎总有一条无需 USB 的自愈路径。真正需要动手的,是确认它是否进入了 Fail-Open(拦截停摆)——那是比失联更隐蔽的坑。

结语:把"能装 53 万条"翻译成运维语言

esp32-c3-adblock 的价值在哈希入 Flash 的架构创新,但长期运行的可靠性取决于三个容易被营销话术掩盖的事实:构建要盯住工具链版本与上游数据源(失败必须响亮)、容量要接受"OTA 与 53.7 万条二选一"的分区表约束、失联要区分"串口被抢、WiFi 未连、固件真坏"三种场景并善用 BOOT 键与浏览器救砖。读懂 partitions.csv、tools/build_blocklist.py 和 src/main.cpp 里的这些细节,比记住"50 KB 内存挡 53 万个广告域名"更能让你的盒子稳定服役。

【免费下载链接】esp32-c3-adblockPi-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项目地址: https://gitcode.com/GitHub_Trending/es/esp32-c3-adblock

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询