☰
DNS Sinkhole是如何工作的?esp32-c3-adblock去广告协议原理一文讲透
2026/10/8 15:02:10 网站建设 项目流程

DNS Sinkhole是如何工作的?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

这篇文章带你一文讲透DNS Sinkhole(DNS 黑洞)的协议原理,主角是开源项目esp32-c3-adblock:一块售价约 2 美元、不带 PSRAM的 ESP32-C3 开发板,就能跑起 Pi-hole 风格的全屋 DNS 去广告。它的杀手锏是把537,000 个广告域名压缩成 40-bit FNV-1a 哈希存进 flash,用二分查找定位,单次拦截查询约10 毫秒响应,全程只占约50 KB RAM。下面从协议层到工程实现,逐层拆解。

一、3 分钟看懂 DNS Sinkhole 工作原理

浏览器访问doubleclick.net之前,需要先向 DNS 服务器问一句:"它的 IP 是多少?"——这就像查电话簿。

DNS Sinkhole 的核心套路:在路由器旁边放一台"山寨 DNS 服务器",客户端的查询先到它这里:

  1. 域名在广告黑名单里 → 不查真实 IP,直接回一个黑洞地址0.0.0.0(广告请求发到一个不存在的 IP,直接失败);
  2. 域名不在名单里 → 原样转发给上游 DNS(默认 Quad99.9.9.9),拿到答案后再原路交回客户端。

项目 README 里用一张极简流程图概括了整条链路:

查询进来 ──▶ 提取域名 ──▶ FNV-1a 哈希(+ 各级父域名) ──▶ 二分查找 flash 哈希表 ├─ 命中 ──▶ 回复 0.0.0.0(被黑洞拦截) └─ 未中 ──▶ 转发上游解析器,中继应答

DNS 本身是无状态的 UDP 协议,所以"中间人"式的改写成本极低——这正是 ESP32 这种小芯片也能胜任的原因。DNS 报文解析与应答构造的实现可以对照 src/main.cpp 阅读。

二、痛点:为什么大多数 ESP32 去广告方案要 PSRAM?

主流做法是把域名字符串整表搬进 RAM 做比对。一个域名平均十几到二十几个字节,14 万个域名就要吃掉约 2.5 MB RAM——远超 ESP32-C3 自带的 400 KB 内存,只好外挂 PSRAM,硬件成本翻到 $8 左右。

esp32-c3-adblock 的思路完全不同:域名字符串不进 RAM,固定 5 字节(40 bit)的哈希进 flash:

对比项字符串进 RAM 方案本项目(哈希进 flash)
硬件ESP32 + PSRAM(约 $8)ESP32-C3,无 PSRAM(约 $2)
14.1 万域名占用约 2.5 MB RAM0.67 MB flash
运行内存大部分 RAM约 50 KB
单次查找字符串比对约 18 次 flash 读取(含 WiFi 往返约 10 ms)
哈希冲突不存在14.1 万域名下为0(53.7 万个时约 1 个)

三、核心技巧:哈希进 flash + 二分查找

3.1 为什么是 40-bit FNV-1a?

每个域名用 FNV-1a 算法算出 64 位哈希,再**截断成 40 位(5 字节)**存入 flash。为什么偏偏是 40 位?这是"生日悖论"算出来的甜点:

  • 40 bit:14.1 万域名冲突约 0 个;53.7 万个也仅约 1 个(极个别域名被误伤);
  • 32 bit:省 20% flash,但 25 万域名时冲突涨到约 7 个,误伤明显变多;
  • 64 bit:每个域名多花 3 字节,解决的却是一个本不存在的问题。

哈希函数的实现只有几行,见 src/main.cpp#L79-L83。

3.2 二级索引 + 二分查找:把 flash 读取压到最少

flash 里的哈希表是排序好的,天然适合二分查找。但 flash 随机读很慢,项目又加了一层优化:

  • 一级索引:开机时从 flash 表中等距抽取 4096 个"采样哈希"载入 RAM(仅 20 KB),先定位目标大概落在哪个区间;
  • 区间内线性比对:每个区间最多 256 个哈希,一次读出整块 buffer 再在 RAM 里比;
  • 256 槽直接映射缓存:热点域名第二次查询直接命中,不再读 flash。

三级合起来,一次查询约18 次 flash 读取,核心逻辑在 src/main.cpp#L103-L138。

3.3 一个域名,连子域名一起拦

只匹配精确域名是不够的:ads.example.com、trk.ads.example.com全得拦。于是isBlocked会对域名逐级父级依次取哈希比对(a.b.c.d→b.c.d→c.d),实现"父域名命中即全子树拉黑",见 src/main.cpp#L161-L170。这也是黑名单工具能省掉海量通配符规则的原因。

四、一次查询的完整旅程(约 10 毫秒)

把前面各环节串起来,一条 UDP DNS 查询在设备内的旅程是:

  1. 解析parseQuery:从报文剥出域名,顺手去掉www.前缀,省掉一半冗余哈希;
  2. 判定:父域名逐级哈希 → 缓存 / flash 二分查找;
  3. 命中:buildBlocked原地改写报头(0x8180表示"这是应答"),追加一条指向0.0.0.0的 A 记录回包,构造细节见 src/main.cpp#L236-L241。这里藏着一个新手必踩的坑:DNS 客户端普遍带EDNS OPT 记录,被拦截的应答必须只保留 question + 一条 answer(NSCOUNT=ARCOUNT=0),否则报文畸形直接丢弃;
  4. 未命中:forwardUpstream换上一个新的随机事务 ID 转发给 Quad9,且只接受事务 ID、问题区、来源地址三者的应答,先排空残留报文再发送——这是为了根治"慢回复串包,导致每个答案错位一位"的经典 bug(src/main.cpp#L247-L271)。

整个过程不阻塞主循环,设备在空闲时还会主动让出 CPU,既快又凉快。

五、不只是去广告:Web 控制台 + 全 OTA

这个 $2 小盒子还自带一个完整的运维面:

  • Web 仪表盘(c3adblock.local):每台设备的拦截/放行计数、一键封禁某台设备、自定义添加黑名单域名;
  • WiFi 零配置:没连上网时自动变身开放热点C3-AdBlock-XXXX+ 强制门户,手机选个 WiFi、输密码即完成配置,无需重新刷机;
  • 固件与黑名单双 OTA:浏览器上传新固件 / 新黑名单,或填一个 URL 定时自动拉取更新,刷完一次 USB 之后彻底解放。

4 MB flash 有个有趣的取舍(见 partitions.csv):双固件槽(支持 OTA)留给黑名单约 1.3 MB ≈25 万域名;单槽布局则能塞下53.7 万域名的"极限版",但牺牲固件 OTA。

懒得装 PlatformIO?项目内置了浏览器一键刷机页docs/index.html:Chrome/Edge 插上板子点一下,约 30 秒完成固件 + 黑名单写入,零工具链。

六、快速上手:4 步变成全屋去广告网关

  1. 硬件:任意 ESP32-C3 板(作者实测 C3 SuperMini,约 $2,4 MB flash,无需 PSRAM),手机充电头或路由器的 USB 口供电即可;想更精致,仓库附了可打印外壳 hardware/esp32-c3-supermini-enclosure.stl,注意别把天线端埋进实心塑料;
  2. 刷机:按 platformio.ini 配置,依次执行——复制 src/secrets.example.h 为secrets.h并设置控制台/OTA 密码(必填,否则局域网任何人都能重写固件)→ 运行 tools/build_blocklist.py 生成哈希表 →pio run -t upload+pio run -t uploadfs一次性写入固件和 LittleFS 文件系统;
  3. 配网:上电后连上热点C3-AdBlock-XXXX,选择家里 WiFi 并输入密码,设备自动重连;
  4. 生效与验证:把设备 IP 设为路由器(或单台设备)的 DNS,然后测试:
dig @<c3-ip> doubleclick.net # -> 0.0.0.0 (被拦截) dig @<c3-ip> github.com # -> 真实 IP (正常转发)

自定义黑名单同样灵活:build_blocklist.py支持 hosts 文件、纯域名列表、AdGuard/AdBlock 基础规则三种格式混用,还支持@@白名单规则把某个域名从黑名单里豁免出来。

七、常见问题 FAQ

Q1:为什么回复 0.0.0.0,而不是 NXDOMAIN(域名不存在)?因为 0.0.0.0 对几乎所有 HTTP 客户端意味着"连接立即失败",行为最确定;且 A 记录应答(0.0.0.0)能同时兼容 IPv4 解析,报文结构也更简单。

Q2:会不会误伤正常网站?40-bit 哈希在 14.1 万域名下冲突数为 0;53.7 万域名的极限版也仅约 1 个"生日悖论"式冲突——概率上约等于某个倒霉域名被多拦,日常使用的平衡名单基本无感。

Q3:24 小时挂着功耗和发热如何?静态功耗仅几瓦以内,芯片温度约 45–55 °C;用外壳时保留散热孔、别遮挡天线即可。空闲时固件会自动休眠,有负载才满速运行。

Q4:它还能进化成什么?作者在 Roadmap 里留了两个方向:分桶前缀索引把单次查找从约 18 次 flash 读取压到 1–2 次(吞吐翻倍的关键);以及直接充当 DHCP 服务器把自己发给每台设备,实现真正的"插电即用"。

八、项目文件地图(按需深入)

文件说明
README.md完整文档:原理、构建、OTA、安全模型、踩坑记录
src/main.cpp全部核心逻辑:DNS Sinkhole、哈希二分查找、Web 仪表盘、OTA
tools/build_blocklist.py黑名单预处理:域名 → 排序的 40-bit 哈希 blob
partitions.csv4 MB flash 分区布局(OTA 槽 vs 大容量黑名单的取舍)
platformio.iniPlatformIO 构建配置(C3 / 经典 ESP32 双环境)
docs/index.html浏览器一键刷机页(含 docs/manifest.json 刷机清单)
hardware/esp32-c3-supermini-enclosure.stl3D 打印外壳模型

小结:DNS Sinkhole 本质是"在 DNS 电话簿里把广告页撕掉",而 esp32-c3-adblock 的精彩之处在于证明了一件事——不需要 PSRAM、不需要大树莓派,2 美元的芯片 + 排序哈希表 + 二分查找,就能以 10 毫秒的延迟撑起全屋级别的去广告网关。这套"哈希进 flash"的技巧对任何大存储预算、小内存预算的嵌入式场景都通用,值得一读。

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

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

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

立即咨询