# esp32-dnsfilter **Repository Path**: qiao08/esp32-dnsfilter ## Basic Information - **Project Name**: esp32-dnsfilter - **Description**: esp32 广告过滤dns服务固件 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-11-15 - **Last Updated**: 2026-11-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ESP32 DNS 过滤器(MicroPython) 把 ESP32 变成家庭局域网的 DNS 过滤器:命中规则的域名返回 `0.0.0.0`(或 `::`),其余转发上游,并带 TTL 缓存。域名匹配参考 [M-Abozaid/esp32-c3-adblock](https://github.com/M-Abozaid/esp32-c3-adblock):大名单以 40 位 FNV-1a 哈希存在 flash 里,查询时二分查找,不占板子内存。 **默认配置按 ESP32-S3 N16R8(16MB Flash + 8MB Octal PSRAM)调校**,小内存板子的回落值见第三节。 ## 目录 ``` esp32-dnsfilter/ ├── boot.py Wi-Fi 连接 ├── main.py 启动入口 ├── config.py Wi-Fi、上游、端口、容量(N16R8 默认值) ├── dns_proto.py 报文解析与构造(纯字节操作) ├── rules.py 文本规则(自定义/放行,放内存) ├── hashlist.py flash 哈希表二分查找 ├── cache.py TTL 缓存 ├── dnsserver.py UDP 53 + 上游转发 ├── webconfig.py HTTP 80 配置页 ├── rules.txt 文本规则 ├── tools/build_blocklist.py 大名单编译器(电脑上跑) ├── tools/push.sh mpremote 部署脚本 ├── tests/ 主机侧单测(CPython) └── docs/ 设计说明 ``` ## 一、准备固件(N16R8) R8 是**八角(Octal)PSRAM**,必须刷 `ESP32_GENERIC_S3` 的 **SPIRAM_OCT** 变体,普通版/Quad 版认不到这 8MB。 到 https://micropython.org/download/ESP32_GENERIC_S3/ 下载 `SPIRAM_OCT` 固件后: ```bash esptool.py --chip esp32s3 -p /dev/ttyUSB0 erase_flash esptool.py --chip esp32s3 -p /dev/ttyUSB0 write_flash -z 0 ESP32_GENERIC_S3-SPIRAM_OCT-*.bin ``` **不需要改分区表。** 我核对过 MicroPython 源码:默认分区表只划到固件结束(约 2MB),启动时 `main.c` 会把"固件之后到 flash 末尾"整段注册成 `vfs` 分区,`_boot.py` 首次启动自动格式化挂载。所以你的 16MB 板子文件系统实际约 **14MB**,哈希表直接放里面。 上板后验证: ```python import os s = os.statvfs("/") print(s[0] * s[3] / 1048576) # 期望约 14 ``` 启动日志也会直接打印"文件系统可用 xx MB"。 ## 二、部署 ```bash pip install mpremote cd esp32-dnsfilter sh tools/push.sh /dev/ttyUSB0 ``` 脚本会拷代码和 `rules.txt`;当前目录有 `blocklist.bin` 时一起拷。 ## 三、配置 改 `config.py` 里的 Wi-Fi 和上游: ```python WIFI_SSID = "your-wifi" WIFI_PASSWORD = "your-password" UPSTREAMS = [("223.5.5.5", 53), ("119.29.29.29", 53)] ``` 需要固定 IP 时填 `STATIC_IP = ("192.168.1.53", "255.255.255.0", "192.168.1.1", "223.5.5.5")`。 **N16R8 默认值(已调好,一般不用动)** | 参数 | 值 | 依据 | | --- | --- | --- | | `MAX_RULES` | 20000 | 文本规则放 8MB PSRAM 堆,约 1~2MB | | `CACHE_SIZE` | 256 | 256 × 2KB ≈ 512KB | | `HASH_CACHE_SIZE` | 256 | 热点域名免 flash 读 | | `MAX_HASH_ENTRIES` | 2000000 | 200 万 × 5B ≈ 10MB,14MB 文件系统留余量 | | `MAX_BLOCKLIST_UPLOAD` | 10MB | 与上限匹配,流式写盘不占 RAM | | `MAX_UPLOAD` | 1MB | 2 万条文本规则约 800KB | **4MB Flash / 无 PSRAM 板子的回落值** | 参数 | 值 | | --- | --- | | `MAX_RULES` | 2000 | | `CACHE_SIZE` | 64 | | `HASH_CACHE_SIZE` | 32 | | `MAX_HASH_ENTRIES` | 250000(文件系统只有约 2MB) | | `MAX_BLOCKLIST_UPLOAD` | 1310720 | | `MAX_CACHE_PACKET` | 1024 | ## 四、域名匹配 匹配顺序: ``` 查询域名 ├─ 文本规则放行(@@) → 直接转发(跳过哈希表) ├─ 文本规则拦截(||、hosts) → 0.0.0.0 / :: ├─ flash 哈希表:完整域名 → 逐级父域 │ 命中 → 0.0.0.0 / :: └─ 都未命中 → 缓存 → 上游转发 ``` 域名和每一级父域都查,所以拦 `doubleclick.net` 自动覆盖 `ad.doubleclick.net`,名单不用重复写子域。 ### 文本规则(放内存) | 写法 | 含义 | | --- | --- | | `\|\|example.com^` | 拦截自身及所有子域 | | `@@\|\|ok.example.com^` | 放行,优先于拦截和哈希表,对子域同样生效 | | `example.com` | 等价于 `\|\|example.com^` | | `0.0.0.0 example.com tracker.test` | hosts 格式,一行多个域名 | | `=example.com` | 只匹配自身(扩展) | | `*.example.com` | 只匹配子域(扩展) | `#`、`!` 开头是注释。不支持正则、`$` 修饰符、`##` 外观规则。 ### 大名单:flash 哈希表 电脑上编译: ```bash # https://github.com/StevenBlack/hosts/blob/master/hosts python3 tools/build_blocklist.py blocklist.bin 源文件或URL [更多源...] # 例:StevenBlack + Hagezi Light(约 10 万条) #python3 tools/build_blocklist.py blocklist.bin \ # https://github.com/StevenBlack/hosts/blob/master/hosts \ # https://github.com/hagezi/dns-blocklists/blob/main/wildcard/light-onlydomains.txt # python tools/build_blocklist.py blocklist.bin .\hosts\hosts.txt .\hosts\light-onlydomains.txt ``` 支持 hosts、纯域名、`||domain^`、`@@||domain^`(从名单移除精确条目)。正则、通配符、`$` 修饰符、`##` 外观规则跳过并计数。要放行被拦父域下的某个子域,写进设备 `rules.txt` 的 `@@`。 输出 `HFB1` + 条目数 + 升序 5 字节哈希。上传(不重启、立即生效): ```bash curl --data-binary @blocklist.bin http://192.168.1.53/blocklist ``` 设备校验魔数、条目数、文件长度,全过才替换旧表;失败保留旧表。 **容量与代价(N16R8)** | 名单规模 | flash 占用 | 单次查询 flash 读 | | --- | --- | --- | | 1 万 | 50 KB | 约 14 次 | | 10 万 | 500 KB | 约 17 次 | | 100 万 | 5 MB | 约 20 次 | | 200 万 | 10 MB | 约 21 次 | 40 位哈希的冲突按生日界估算:100 万条期望约 0.5 个、200 万条约 2 个,表现为极少数无关域名被多拦;编译器会打印实际冲突数。 ## 五、使用与自检 ```bash nslookup doubleclick.net 192.168.1.53 # 期望 0.0.0.0 nslookup www.example.com 192.168.1.53 # 期望真实地址 curl http://192.168.1.53/status # 拦截数、缓存、内存、文件系统余量 ``` 上板自检顺序: 1. 串口看到本机 IP → Wi-Fi 通 2. 串口"文件系统可用"约 14MB → 16MB flash 被完整利用 3. `flash 哈希 N 条` 与编译输出一致 → 哈希表加载成功 4. 名单内域名 → 0.0.0.0;普通域名二次查询缓存命中增加 5. 上传一次 `blocklist.bin` → 页面提示条数,旧缓存清空 ## 六、已知限制 - 只监听 UDP 53,不处理 TCP 查询 - 哈希表只表达"域名 + 子域"拦截,表达不了正则/通配/`$` 修饰符 - 40 位哈希在百万级名单会有个位数冲突 - 文本 `@@` 放行子域是提前短路,不会删哈希表条目 - 上游查询阻塞,单次超时默认 1 秒 - 网页无鉴权,仅限内网 ## 七、排错 | 现象 | 排查 | | --- | --- | | 串口停在连接 Wi-Fi | SSID/密码错,或路由器只有 5GHz(ESP32 只支持 2.4GHz) | | 文件系统只有约 2MB | 刷的不是 16MB 识别正常的固件;确认 `esptool flash_id` 显示 16MB | | `/` 不可写 | 首启格式化没跑:`import vfs, flashbdev; vfs.mount(flashbdev.bdev, "/", mkfs=True)` | | 哈希条目为 0 | `/blocklist.bin` 没拷进去或文件头不是 `HFB1` | | 上传 400 | 文件截断、超 `MAX_HASH_ENTRIES`、或非本工具生成 | | 文本规则 400 | 超 `MAX_RULES`,改用哈希表 | | 内存持续下降 | 关 `VERBOSE`、减 `CACHE_SIZE`、定期 `gc.collect()` | 内容由 AI 生成