dns-stack 技术详解:两台机器上的抗污染递归解析
摘要
dns-stack 是一套自用的 DNS 递归解析器。家里的手机、电脑通过加密的 DoH / DoT / DoQ 把查询发给它,它从根服务器开始一级一级问下去,把答案交回设备。
它不是托管型的权威 DNS 平台。权威平台替别人的域名回答「这个名字对应什么地址」;dns-stack 替自己的设备去问全世界。所以它要解决的是另外三件事:
- 拿到没被污染的答案:境外域名的查询经隧道从香港出网,墙看不见查询本身,也就无从伪造应答。
- 拿到离自己最近的节点:国内域名直连国内权威,并把设备所在的 /24(ECS)带给 CDN,让它按真实位置调度。
- 坏了能自己恢复,恢复不了也看得见:降级、自愈、限流尽量自动;自动不了的,面板和告警上一眼就能看到。
整套系统只有两台机器:一台国内服务器承担全部功能,一台香港服务器只负责让境外查询从境外出网。
核心架构:四层分工
cn-resolver直连出网
打标 → WireGuard 隧道
wg0 10.100.0.2 ⇄ 10.100.0.3 境外权威的查询 · mosproxy 的降级上游offshore入口层:mosproxy
- 监听 DoH(443/TCP)、DoH3(443/UDP)、DoT(853/TCP)、DoQ(853/UDP)。
- 乐观缓存:过期的答案先回给设备,后台再去刷新,热门域名几乎从不让设备等递归。
- 域名黑名单命中直接回 NXDOMAIN,连递归都不发生;访问控制用 nftables 只放行授权网段。
- 上游是一个按顺序降级的负载均衡:先本机 Unbound,它不应答就转香港 Unbound。
详见 加密入口。
递归层:本机 Unbound + 出口分流
Unbound 自己从根区完整递归,不依赖任何公共 DNS。关键在它发出去的每一个包走哪条路:nftables 匹配 Unbound 进程发出的流量,目标是大陆地址(@direct4)或已知的国内权威(@cn_authority)就直连,否则打上标记、走隧道到香港出网。隧道断了,打标的包被策略路由显式拒绝——宁可解析失败,也不回落直连去拿污染答案。
判据是「这台权威服务器在哪」这个可核验的网络事实,而不是「这个域名算不算国内的」这种要人工维护的分类。详见 出口分流。
数据层:分流数据流水线
一条每 15 分钟运行一次的 Go 流水线,产出分流需要的全部数据:大陆网段、国内权威、ECS 白名单、ECS 分片表、共享 anycast 清单。各步骤有自己的周期,按依赖顺序执行;上游失败时下游保留上一版,不拿空数据硬算。详见 分流数据流水线。
运维层
| 组件 | 职责 |
|---|---|
| 采集器 | 包着 mosproxy 运行,把查询日志写进 SQLite(保留 7 天);采集出错时继续读空管道,绝不拖住 DNS |
| 管理面板 | 概览、查询明细、域名统计、诊断工具、设置;统计只读 |
| 特权助手 | 唯一能做 root 操作的进程,面板经本地套接字调用,固定白名单 + 角色闸门 |
| 分流看门狗 | 每分钟检查一次分流链路,异常时重建,并在异常出现、变化、恢复时推送告警 |
| 例行维护 | 证书续签、备份、日志清理 |
一次解析的旅程
以手机查询一个境外 CDN 上的网站为例:
- 加密到达:手机经 DoH 把查询发到国内节点 443 端口,mosproxy 先过访问控制与黑名单。
- 先看缓存:命中就立即返回——这是九成的情况。已过期的条目也先返回,后台刷新。
- 交给递归:未命中则转给本机 Unbound。它从根开始,依次询问
.com的权威、域名自己的权威、CNAME 指向的 CDN 权威。 - 每一跳独立分流:问根和
.com权威时,目标多在境外,这几个包经隧道从香港出网;如果 CDN 的权威在大陆,这一跳就直连。一次解析里直连与隧道可以交替出现,判断只看那一跳的目标地址。 - 带上位置:CDN 权威在 ECS 白名单里,Unbound 就把手机所在的 /24 一起发过去,CDN 按手机的真实位置挑节点,而不是按隧道出口(香港)。
- 按片缓存:mosproxy 按 ECS 分片表把设备归到「省份 + 运营商」分片,答案按分片缓存,同一片的其它设备直接复用。
- 返回并记录:答案经 mosproxy 返回手机;采集器记下域名、类型、走了哪条路、耗时,供面板统计。
如果第 3 步本机 Unbound 没有应答,mosproxy 会把查询转给香港 Unbound,由它从境外完整递归。这条降级路径拿不到国内 CDN 的就近节点,但能保证不中断。
关键能力
1. 出口分流:把污染变成结构上不可能
两条路径都拿不到污染答案:权威在大陆就直连(境内到境内,墙不介入);权威在境外就走隧道(墙看不见这个查询)。于是「双视角对比、猜谁可信」整套机制失去了存在的理由。
有两点值得单独说:
- 共享 anycast 不能聚合:一些境外 CDN 的权威和国内服务共用 anycast 地址,按网段聚合会把它们误判成国内。所以境外权威只按 /32 精确收录,并有专门的步骤每小时识别这类地址。
- 判据缺席时不冒充健康:归属库、大陆网段任一不可用时,相关步骤保留上一版产物、不写空结果;数据骤降超过阈值时拒绝下发。
2. ECS 就近:分流架构的必需品
境外 CDN 的权威在境外,查询必然走隧道。如果它收不到设备的子网,只能按香港出口判断位置,把国内用户调度到香港节点:链路通、解析成功,但绕了一大圈。所以 ECS 白名单按「这台权威服务谁」收敛,而不是按「权威 IP 在不在大陆」。详见 ECS 就近调度。
3. 健康检查、降级与自愈
| 故障 | 自动处理 | 在哪里看见 |
|---|---|---|
| 本机 Unbound 无应答 | mosproxy 转香港 Unbound | 面板状态点变红、概览横幅 |
| 香港 Unbound 离线 | mosproxy 健康检查剔除;香港侧 supervise-daemon 拉起 | 状态点变黄、上游表格、自检「上游全部在线」、告警推送 |
| 分流规则丢失(nft / 策略路由) | 看门狗每分钟检查并重建 | 自检、看门狗日志、告警推送 |
| 隧道断开 | 打标流量被显式拒绝,境外解析失败但不回落直连 | 自检「香港递归可用」「境外域名解析」 |
| 批处理任务内存失控 | systemd 内存上限只杀掉这个任务,不连累解析 | 模块状态 |
| 线上配置偏离仓库模板 | 不自动改,避免覆盖面板里的调整 | 自检「Unbound 运行配置与模板一致」 |
详见 健康检查与告警。
4. 访问控制与黑名单
域名黑名单命中直接回 NXDOMAIN;访问控制渲染成 nftables 链,只放行列出的网段访问加密端口。面板修改访问控制时有防自锁闸门:改完之后请求方自己不在授权网段里,就拒绝下发。详见 域名黑名单 与 访问控制。
5. 查询观测
面板的数据都来自采集器写的 SQLite:实时查询流、近 24 小时趋势、域名排行、失败域名、各上游的成功率与延迟。客户端 IP 在采集时就已丢弃,面板里只有「查了什么」,没有「谁查的」。详见 查询日志与统计。
负载画像
量过才知道该优化什么。截至 2026-10-01 的 7 天:
| 指标 | 数值 |
|---|---|
| 查询量 | 约 13.5 万次(约 0.2 次/秒) |
| 缓存命中 | 90.1% |
| 国内递归 | 9.6%,平均 311 ms |
| 境外递归 | 0.03% |
| 解析失败 | 0.32% |
这是一个几乎全在境内访问的家庭网络。DNS 解析路径上不跑 Go 代码,Go 只负责数据生成、采集和管理,所以性能工作的重点是批处理任务的峰值内存(机器只有 1.8GB),而不是吞吐。更多取舍见 设计取舍。