想在路由器之外再给全家加一层域名拦截,传统答案是树莓派加 Pi-hole;新答案是把这个拦截器塞进无服务器运行时。serverless-dns 就属于后者:同一份代码,可以原样跑在 Cloudflare Workers、Deno Deploy、Fastly Compute@Edge 与 Fly.io 四个平台上,同时提供 DoH 与 DoT 两种入口。它真正的价值不在「解析更快」,而在于把「一套代码跑四种运行时」变成一次配置动作——代价是各平台的能力与额度差异,连同你选的拦截策略,都被压进了同一份设置里。
它是什么:把 Pi-hole 搬到无服务器上
先给结论:它是一个 Pi-hole 式的内容拦截、无服务器 stub 解析器。stub 是这里的关键词——它不做递归,只把你发出的 DNS 查询先按清单筛一遍,再转给上游的加密解析器。RethinkDNS 团队把它跑在生产环境:端点 sky.rethinkdns.com 在 Cloudflare Workers 上,max.rethinkdns.com 在 Fly.io 上(后者同时开 DoH 与 DoT)。这套部署本身就说明它是被长期运营的完整解析器,而不是演示脚本。
仓库归属 org 账号 serverless-dns,采用 MPL-2.0 许可,3821 stars、2271 forks,创建于 2021 年 3 月,最近一次推送在 2026 年 5 月。这样的体量与时间跨度意味着两件事:接口相对稳定,常见的坑基本被踩过了。因此理解它的正确单位不是「某一次部署」,而是「一套代码 × 四种运行环境」——下面会看到,同一份代码在不同平台上,撞到的限制完全不同。
上手:Cloudflare 十分钟起一个
官方把上手路径压得很短,而且刻意把「部署」和「配置」拆成了两件事。部署这一步交给平台:点开 Deploy to Cloudflare Workers 按钮,授权 GitHub、选好空间,Worker 就起来了。真正决定行为的是配置,而配置不在代码里,在浏览器里。
部署成功后访问你这个域名的 /configure 页,会看到一个清单勾选界面——按官方文档,这个页面把清单按分类陈列,勾选完成后它会生成一段配置 token,形如 https://example.com/1:AIAA7g==。把这段地址填进任何支持 DoH 的客户端即可,末尾那截 1:AIAA7g== 就是你这份策略的 blockstamp。换句话说,改拦截规则不需要重新部署,只要在浏览器里重新生成一次 token。
另一处常被忽略的配置是上游。默认情况下它把查询转给 Cloudflare 的 cloudflare-dns.com,备用上游是 dns.google。想换,只需在 Workers 仪表盘的环境变量里改 CF_DNS_RESOLVER_URL,或在自己的 fork 里改 src/core/env.js。这一步之所以轻松,是因为默认值全部集中在环境管理层的一张表里——这正是它可移植的关键设计。
请求进来之后:六段插件流水线
一次查询究竟走了哪些环节,源码写得很清楚。入口按平台分叉:Workers 走 server-workers.js,Deno 走 server-deno.ts,Node 与 Fly.io 走 server-node.js;但三者拿到请求后都会汇入同一个 doh.js。
doh.js 先做边界处理:OPTIONS 与 HEAD 直接返回 204 空响应,只有 User-Agent 看起来像浏览器时才补上 CORS 头,其余请求交给插件并受一个超时保护。接着进入 plugin.js,这是它的中枢。
plugin.js 按注册顺序串行执行一条固定插件链:先是 userOp,解析出你的身份、拦截策略与专属上游;若运行在递归模式,还会插入一个 prefilter;然后是 cacheOnlyResolver 查缓存、commandControl 处理控制类请求、dnsResolver 真正向上游解析;最后 logpush 记日志——它被标记为「始终执行」,但一旦前面出现异常就跳过。链上每一段的结果都写进一个请求级上下文供后续阶段复用;任一段决定停止,后续非「始终执行」的插件就会被略过。
还有一个容易被忽略的细节:入口在解析查询时会主动丢弃 ECS(EDNS 客户端子网)信息,必要时重新编码报文体;而「既不是 DNS 查询、又不是 GET」的请求会直接吃一个 405。把这些连起来看,它并不是「随手转发 DNS」的薄代理,而是一条每一段都有明确职责与顺序的管线。
拦截清单是怎么进到运行时的
拦截清单的体量是这个项目最容易被低估的部分。README 的口径是 200 多份清单、压缩成一种 succinct radix trie,截至 2025 年 11 月约 1700 万条目;清单仓库自己的记录则是截至 2022 年 12 月收录 194 份清单、约 1350 万个域名。两个数字来自不同时间点,但量级一致:这是千万级规模的黑名单。
关键在于它没有把这千万条塞进运行时逐条比对。清单仓库的构建流程是这样的:先下载各家清单,再用脚本把它们压成紧凑的 radix trie,最后上传到 S3 或 R2 对象存储,运行时只负责按需拉取这个压缩结构。项目里的 pre.sh 会在构建期按「年 / 月-周」的路径从对象存储前端拉取配置与文件标签,并带失败重试。
环境管理层则给出运行期的旋钮:应答缓存 CACHE_TTL 默认 1800 秒,清单下载超时默认 7500 毫秒,清单超过 42 周会自动续期,子域是否一并拦截由 BLOCK_SUBDOMAINS(默认开)控制,也可以整体关掉清单。这套组合的效果是:重活被前置到构建期与对象存储,运行期只做查表和短暂缓存——对于一个只有 10 毫秒 CPU 预算的免费档运行时来说,这不是锦上添花,而是能不能跑起来的前提。
四个平台怎么选:把「免费额度」算清楚
README 有一句话很容易被当成结论直接引用:这些服务的免费档足以覆盖每月 10 到 20 台设备的 DNS 流量。这句话需要按平台分别验算,因为它写下的那个时间点,和现在的定价已经不是一回事了。
先把设备量换算成请求量。取它自己给出的上限——20 台设备、每台每天约 1500 次解析——就是每天 3 万次,按 30.4 天折算约每月 91.2 万次。再逐个平台对照现行免费额度:Cloudflare Workers 是每天 10 万次、约合每月 304 万次(UTC 零点重置,超限返回 Error 1027);Deno Deploy 免费档是每月 100 万次请求;Fastly 的 Compute 请求免费额度是每月 1000 万次。算下来,同样的用量分别占掉约 30%、约 91% 与约 9%。
也就是说,在 README 自己给出的设备区间踩到上限时,最先见底的平台是 Deno Deploy,而不是直觉里最「便宜」的 Cloudflare。而真正让那句断言失效的是 Fly.io:它的官方文档现在写得直白——新组织没有免费档,也没有每月免费用量,只有 2 小时机器运行时或 7 天的免费试用;最小的常开应用约 2.19 美元/30 天。只有 2024 年 10 月停售前的老计划,才保留最多 3 台 256MB 机器加 3GB 卷的免费额度。所以「四个平台都用免费档」这个前提,如今只对上其中三个。
| 运行时 | 上手难度 | 支持协议 | 运行模型 | 免费额度(现行) | 持久性 |
|---|---|---|---|---|---|
| Cloudflare Workers | Easy | DoH | v8 Isolate,瞬时 | 10 万请求/日(约 304 万/月),CPU 10ms | 无状态,靠 R2 与 Cache API |
| Deno Deploy | Moderate | DoH | Deno Isolate,瞬时 | 100 万请求/月,20GiB 出网 | 无状态,靠对象存储 |
| Fastly Compute@Edge | Easy | DoH | wasmtime 沙箱,每请求创建销毁 | Compute 请求 1000 万/月免费 | 无状态 |
| Fly.io | Hard | DoH 与 DoT | Node,常驻微虚拟机 | 无免费档;试用 2 小时/7 天;最小约 2.19 美元/月 | 有持久卷 |
结论
回头看,serverless-dns 做的事可以用一句话概括:它用「一套代码 × 四个平台」,把自建一个带拦截的解析器,从「买设备、装系统、维护黑名单」压缩成了「点一次部署、勾一次清单」。它的工程含量集中在两处——把解析、拦截、鉴权、缓存收进一条可解释的插件流水线,以及把清单的庞大体量前置到构建期与对象存储。
于是选平台这件事,本质上是在选免费额度与协议能力,而不是选代码:想最省心选 Cloudflare,想要更宽的请求额度选 Fastly,需要 DoT 或持久化才考虑 Fly.io,而 Deno Deploy 的免费月度额度在持续查询负载下会最先吃紧。至于要不要自建,判断标准也很朴素——如果你只想要一个干净的加密解析,没必要引入一整套清单与流水线;只有当「按自己的清单拦截、入口由自己掌握」这件事本身有价值时,这套东西才划算。
评论