☰
事务ID错乱惊魂:esp32-c3-adblock如何根治上游转发应答串包问题
2026/10/8 21:34:54 网站建设 项目流程

事务ID错乱惊魂:esp32-c3-adblock如何根治上游转发应答串包问题

【免费下载链接】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

esp32-c3-adblock 是一个运行在2 美元 ESP32-C3(无需 PSRAM)上的Pi-hole 风格 DNS 广告拦截器:它把 53.7 万个广告域名以 40 位 FNV-1a 哈希的形式存进 flash,约 10 毫秒完成一次拦截判定,只占约 50 KB 内存;命中黑名单的域名直接"黑洞"掉,未命中的则转发给上游 DNS 并回传应答。而在这个"转发-回传"环节,早期版本曾出现过一个极难排查的DNS 应答串包顽疾——本文就来复盘这场"事务ID错乱惊魂"的成因,以及它被彻底根治的完整思路。

一、惊魂现场:一次转发超时引发的"全员串包"

先看清一次域名解析的路径:

手机/电脑 → esp32-c3-adblock → 上游 DNS(默认 Quad9)

DNS 走 UDP,每条查询都携带一个事务ID(txid),应答必须带着相同的 txid 返回——这是客户端确认"这封应答是回给我的"的唯一凭证。惊魂的根源在于两个事实:

  • C3 上所有客户端共享同一个上游 UDP 套接字;
  • UDP 应答超时后,迟到的数据包不会自动消失,而是滞留在套接字缓冲区里。

于是链条就断了:A 的查询转发给上游后超时,C3 认为失败;下一位用户 B 的查询进来,C3 顺手把缓冲区里 A 那条迟到应答转发给了 B,B 的应答又被给了 C……正如源码注释里那句"every answer shifted by one"(每条应答都错位了一位)。

用户视角的表现是"某个网站随机解析到错误 IP、时好时坏、无法复现"——典型的间歇性故障,靠猜 IP、猜时间都查不出来。

二、根治方案:forwardUpstream 的四道关卡

完整修复集中在 src/main.cpp 的 forwardUpstream 函数(即 Issue #10 的修复),可以拆成四步:

第 1 步:转发前先清空套接字"残留行李"

每次转发前先释放半读缓冲区,再循环排空、丢弃所有上一轮遗留的旧应答(main.cpp L248-L249),保证发新查询之前套接字是"干净"的。

第 2 步:每次转发都换一张"全新随机身份证"

转发出去的查询不再沿用旧事务ID,而是每次用esp_random()生成一个全新随机 txid(main.cpp L251)。旧应答的 txid 永远对不上新 ID,即使没被清空,也天然失效。

第 3 步:三重校验,只收"确实属于本次查询"的应答

等待应答期间(main.cpp L258-L266)不是"来者不拒",而是同时满足三个条件才接受:

校验项防住的风险
源地址、源端口 == 上游 DNS防止局域网里任意设备伪造应答
应答 txid == 本次新生成的随机 ID证明应答确实针对"我的"查询
问题区(question section)与原始查询逐字节一致最后一道保险,域名问的是同一个

等待策略用的是1 秒截止时间而不是重试次数(main.cpp L258):截止时间一到就放弃,避免在慢上游上空耗,也避免过早放弃。

第 4 步:回传前"换回客户的身份证"

通过校验后,C3 把报文头恢复成客户端自己的原始事务ID(main.cpp L267)再发回去。对客户端而言,这就像自己的查询得到了直接应答——中间的"换 ID 转发"完全透明。

🔍 一句话总结这套机制:清残留 → 换新 ID → 严格对账 → 换回原 ID。四步缺一不可:清残留防脏读,新 ID 断旧账,对账防冒牌,换回保证客户端无感知。

三、为什么这个修复对广告拦截器是"生死线"

  • C3 挡在全网络每台设备的解析路径上,答错一次就是错误的网站或连接失败,用户会直接怪罪这个"小白盒";
  • 设备只有约 50 KB 可用内存,养不起连接池、复杂状态机——"清残留 + 随机 ID + 三重对账"是超低资源环境下最稳的工程解;
  • 这也是所有 UDP 服务的通用教训:永远不要假设套接字里只有自己的包,收到任何数据都要先验证"它是发给谁的"。

四、三分钟上手:验证你的 C3 不再串包

  1. 烧录固件与 blocklist 文件系统后上电,打开仪表盘http://c3adblock.local;
  2. 把任意设备的 DNS 指向 C3 的 IP(或作为二级解析器挂在主 DNS 之后);
  3. 用两条命令验证(README.md 中 "Use it" 一节):
dig @<C3的IP> doubleclick.net # -> 0.0.0.0(命中黑名单,已拦截) dig @<C3的IP> github.com # -> 真实 IP(转发上游,应答一一对应)

只要被拦的域名稳定返回 0.0.0.0、放行的域名拿到正确 IP,"ID 错乱"就已经被根治。

五、相关文件速查

文件说明
src/main.cpp固件主程序:DNS 黑洞、上游转发、Web 仪表盘、OTA 与 WiFi 配网
src/page.h仪表盘页面(PROGMEM 内嵌 HTML)
tools/build_blocklist.py拦截名单构建器:hosts/域名列表/Adblock 规则 → 排序哈希表
tools/test_build_blocklist.py构建器单元测试
partitions.csv分区表:黑名单容量与固件 OTA 的取舍
platformio.iniPlatformIO 构建配置(C3 SuperMini / 经典 ESP32)
README.md完整文档:硬件、烧录、安全模型与踩坑记录

✅ 打印一个 3D 外壳(hardware/esp32-c3-supermini-enclosure.stl),插进路由器背后空着的 USB 口,就是一台免电源、免维护的家用 DNS 广告拦截器。

【免费下载链接】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),仅供参考

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

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

立即咨询