很多人想给家里搭建全网广告拦截,常规操作不是翻出一台落灰的树莓派,就是在软路由、NAS 甚至小主机里开个 Docker 跑 Pi-hole 或 AdGuard Home。这套方案成熟好用,但总让人感觉有点“大炮打蚊子”——一台功耗几瓦到十几瓦、算力充裕的小电脑,整天开机就是为在局域网里接发几十字节的 UDP 报文,顺手把恶意域名解析成 0.0.0.0。
前阵子社区里出现了一个让人眼前一亮的开源项目:esp32-c3-adblock。作者用一块淘宝上十块钱出头(约 2 美元)、指甲盖大小的 ESP32-C3 核心板,硬生生跑起了媲美 Pi-hole 的 DNS 广告拦截服务,而且能稳稳装下 14 万甚至多达 53 万条过滤规则。
最绝的一点在于,这块板子完全没有外扩 PSRAM,全靠一套极致的数据结构设计把硬件资源压榨到了极限。
为什么之前没人这么做?
玩过嵌入式或者单片机网络开发的朋友都知道,在微控制器上做 DNS 域名过滤,最棘手的问题从来不是网络吞吐,而是内存(RAM)。
像 AdGuard Home 或常规的 DNS 拦截器,通常需要把几万到几十万条域名字符串加载进内存里做快速匹配。一个普通域名按 20 个字节计算,十万条规则光是纯文本就要占掉 2MB 以上内存;如果算上哈希表节点指针、前缀树(Trie)开销,轻松突破数兆字节。
但普通的 ESP32-C3 只有区区 400KB 左右的片上 SRAM,扣除 Wi-Fi 协议栈、FreeRTOS 内核和 TCP/IP 缓冲之后,留给应用层能自由支配的堆内存通常只有一两百 KB。如果走传统的“把规则全读进 RAM”路线,十块钱的单片机当场就会 OOM 崩溃。以往想在单片机上跑类似项目,大家只能多花几倍价钱买带 PSRAM(伪静态内存)的高配 ESP32 模组。
这个项目的作者打破了这种惯性思维:既然单片机的 RAM 贵如黄金,但板载的 SPI Flash 却有整整 4MB,为什么非得把域名字符串塞进内存?
核心设计:Hash-in-Flash + 二分查找
项目的核心思路简单粗暴,却极为精妙,作者将其称为 Hash-in-Flash。
整个工作流程可以拆成两步。
1. 离线预处理:用 40-bit 哈希压缩空间
在将规则写入设备之前,Python 脚本会先清洗规则库(支持 hosts 格式、AdGuard 基础规则以及普通域名列表),提取出所有要拦截的域名及其父级域名。
脚本并不存储原始域名字符,而是对每个域名计算一次 FNV-1a 哈希,并且只保留 5 字节(40-bit)。处理完后,脚本将所有 40-bit 的哈希值按升序严格排序,打包成一个二进制文件 blocklist.bin 烧录进 ESP32 的 Flash 分区。
原本十几兆的文本规则,被压缩成了纯粹的 5 字节数字流:
- 14 万条规则仅占 Flash 约 0.7 MB
- 扩展到极限的 53.7 万条规则也只占约 2.6 MB
2. 在线查询:Flash 上的极速二分查找
当路由器或终端设备向 ESP32 发送 DNS 查询 时,单片机解析出域名,计算其 40-bit 哈希,然后直接在 Flash 存储空间内执行二分查找。
因为数据已经预先排序,在 14 万条记录中进行二分查找,最坏情况也只需要读取约 18 次 Flash。ESP32 的 SPI Flash 映射读取非常迅速,整个拦截查询从收到请求到返回,包含 Wi-Fi 空口延迟在内仅需约 10 毫秒。
更可怕的是内存消耗:由于所有匹配逻辑都在 Flash 上原地完成,系统运行时占用的堆内存只有 50 KB 左右,C3 自带的片上 SRAM 甚至还有大把结余。
为什么是 40-bit?
这里还藏着一个有趣的工程取舍。单片机存储精打细算,为什么不用标准的 32 位(4 字节)或者 64 位(8 字节)?
作者根据哈希碰撞的“生日悖论”算了一笔账:
- 如果用 32 位哈希,省了空间,但在 25 万条规则下大概率会出现 7 次以上的哈希碰撞。这意味着会有正常网站被误杀,在 DNS 服务里不可接受;
- 如果用 64 位哈希,完全不会碰撞,但每条规则凭空多出 3 字节,Flash 空间直接爆掉;
- 而 40-bit 刚好是甜点位:14 万条规则下碰撞概率基本为 0;即便堆到 53.7 万条规则,理论上也只会出现约 1 次碰撞(即全网可能只有一个无辜域名被误判拦截)。用一次微乎其微的误杀代价,换取了 4MB Flash 容纳半百万级规则的能力。
不只是玩具,细节全部拉满
很多嵌入式 PoC 项目往往只能通过串口看日志,改个配置就得重新插线烧录代码,但 esp32-c3-adblock 在实用性上做得非常成熟:
-
直接插在路由器屁股上
硬件首选是 ESP32-C3 SuperMini,尺寸几乎和拇指盖一样大。加一个几块钱的 USB-A 转 Type-C 转接头,就可以直接插在路由器背面的 USB 口上取电。不用外接电源适配器,不占插排,也没有杂乱的网线和电源线。仓库里甚至提供了配套的外壳 3D 打印 STL 文件。
-
配网与 Web 面板
设备首次上电若连不上 Wi-Fi,会自动开启名为 C3-AdBlock-XXXX 的热点并弹出强制门户(Captive Portal),手机连上去直接选 Wi-Fi 输密码即可。板子自带一个轻量 Web 控制台,不仅能看每个客户端的拦截/放行统计,还支持手动临时封禁设备或添加自定义域名。
-
真正的全无线 OTA 维护
初次通过串口烧录完固件后,后续完全不需要再拔下来插电脑:
- 固件支持通过网页上传或 ArduinoOTA 局域网推送
- 规则库(Blocklist)同样支持 OTA 上传。更省心的是,设备支持配置定时拉取远程规则。项目作者在 GitHub Actions 上配置了自动化工作流,每周一自动重新构建最新规则包发布在 Releases 中,填入 URL 后它每周自己静默更新
-
安全边界考虑
对于一个串在家庭网络关键路径上的 DNS 设备,安全绝不是小事。项目对面板里的所有敏感操作(上传固件、修改黑名单、重置 Wi-Fi)强制要求 HTTP Basic Auth 验证。为了防范内网 CSRF 攻击,前端在所有修改状态的请求上都附加了自定义请求头(X-Requested-With),杜绝了通过外部恶意网页伪造请求把设备重置或篡改的隐患。
工程师视角的浪漫
在硬件算力廉价膨胀的今天,我们早已习惯了用更快的 CPU、更宽的 RAM 去堆砌功能,动辄几十兆内存起步的 Electron 应用充斥着桌面。
但这个项目展现了一种久违的工程美感:没有昂贵的硬件,没有高大上的复杂框架,依靠对数据结构的深刻理解、对哈希碰撞概率的严谨推导,以及对存储特性的精准把控,用十几块钱的边缘硬件干掉了需要整套软路由环境才能解决的需求。
如果家里正好有闲置的 ESP32-C3 板子,不妨花上十几分钟把它刷成一个静默工作的小拦截器——插在路由器背面,功耗不到 1W,安安静静守护全屋的网络体验。作为 云栈社区 的一员,这种把边缘硬件玩出花样的项目,正是极客精神最生动的体现。