Files
vps/docs/lan-dns-architecture.md
T
windyboy d54ec71aea docs(dns): record gfw foreign branch DoH change (W1N-62)
Document encrypted DoH upstream for mosdns foreign queries and note that
DoH traffic goes direct to hk2, not via OpenClash proxy.
2026-08-13 10:50:16 +08:00

241 lines
17 KiB
Markdown

# 内网 DNS 架构调研与优化建议
> 状态:2026-08-12 调研定稿,Linear **W1N-56**。**Phase 0 验证(2026-08-12)全部完成,最终裁决(用户定稿)已对齐**;本期未改动任何生产 DNS 路径。
> 2026-08-12 现场核查修正:gfw 上 mosdns **不是闲置**——它是 OpenClash clash 的
> `nameserver`/`default-nameserver`(DIRECT 规则真实 IP 解析),处于活动链路,裁决第 5 条
> 的"删除闲置 mosdns"前提不成立,处置改为"正式纳管并文档化"。
> 相关:`docs/lan-overview.md`、`hosts/dns.windy.lan.md`、`hosts/gfw.windy.lan.md`、W1N-40。
> 2026-08-13 (W1N-62):gfw mosdns 国外分支已从"明文国内公网 DNS"改为**加密 DoH**
> (自建 `https://adg.chans.xyz/dns-query`,hk2),新增 `foreign_upstream`/`foreign_fallback`
> (primary=DoH, secondary=明文国内 DNS, threshold 1000ms),`bootstrap` 用现有国内公网 IP
> (防自举循环)。实测:mosdns 平面国外域名 A/AAAA 恢复(google AAAA `2607:f8b0…`)、
> `dup.baidustatic.com`→`0.0.0.0`(AGH 拦截保留)、clash 7874 fake-ip 平面不变。
> **重要修正(定稿)**:DoH 流量实测为 **gfw→hk2 直连,未经 clash 代理**——nft output 链
> (mangle mark/tcp redirect)计数为 0、`/proc/net/tcp` 存在到 hk2:443 的 established
> 连接,路由自身 TCP 输出当前并未被 OpenClash 重定向,故"经代理访问加密 DNS"的假设不成立。
> **最终决策:接受直连,不强行走代理**——`foreign_upstream` 唯一指向自建解析器
> `adg.chans.xyz`(hk2),自有 VPS 直连即可达、无被墙/污染问题、DoH/TLS 已加密、应答干净,
> 走代理毫无增益反而把 DNS 平面耦合进 clash;实测 kill clash 期间国外查询 0.01s 正常应答,
> watchdog 自动拉起,直连使 DNS 平面独立于代理(优于过代理)。
> 备份:`config.yaml.bak-foreign-doh-20260813-103746` / `config.yaml.bak-foreign-doh-20260813-103813`。
## 1. 现状(实测 2026-08-12)
| 角色 | 部署 | 职责 | 是否在活动路径 |
|------|------|------|----------------|
| **AdGuard Home** | `192.168.66.36`(PVE VM 120,Docker host 网络) | EdgeRouter DHCP 通告给 LAN55/66 客户端的唯一 DNS;广告/过滤、查询统计、Web 面板 | ✅ **是(LAN 客户端唯一入口)** |
| **mosdns** | `192.168.66.1`(gfw OpenWrt)监听 `127.0.0.1:6052`(仅本机) | clash 的 `nameserver`/`default-nameserver`:DIRECT 规则真实 IP 分流(国内 → AGH `.36:53`,国外 → `223.5.5.5`/`119.29.29.29`) | ✅ 网关侧(clash 消费,不面向客户端) |
| **OpenClash / clash(meta)** | `192.168.66.1`(gfw) | gateway 自身/被劫持流量的 fake-ip + 代理;DNS 走 dnsmasq→clash `#7874` | 仅网关侧与 VLAN10 |
**关键事实(全部实测):**
- LAN 客户端 DNS 直连 `66.36`,**不经过** gfw(EdgeRouter `service dns forwarding` cache 512,通告 `.36`)。
- AGH 上游:DoH `dns.alidns.com`(→`223.5.5.5`/`223.6.6.6`)+ `doh.pub`(→`120.53.53.53`/`1.12.12.12`),
`upstream_mode: load_balance``fastest_timeout: 1s``upstream_timeout: 10s`;bootstrap
`223.5.5.5`/`223.6.6.6`(公网 IP,无自举循环);兜底 DoH `adg.chans.xyz`(→`hk2.chans.xyz``154.36.174.161`)。
DNS 监听 UDP/TCP **v4 only(`0.0.0.0:53`)**,无 v6 监听;`enable_dnssec: false`;cache 4 MB;ratelimit 20。
- AGH 宿主出网:默认路由 `via 192.168.66.254`(EdgeRouter),**直连,不经 gfw**;DoH 实测可达
(223.5.5.5:443 → HTTP 400/0.05s,120.53.53.53 → 502/0.06s,adg.chans.xyz 冷连接 ~3.5s)。
- gfw 自身出网:OpenClash `openclash_mangle_output` 对非本地区域/国内 IP 流量统一
`mark 0x162 → tproxy 127.0.0.1:7895`(**gfw 自身流量默认走代理**,含 clash 的
nameserver-policy DoH);mosdns 的上游(AGH `.36` 本地区域、`223.5.5.5` 国内 IP)均被 bypass,保持直连。
- gfw DNS 链:`server=127.0.0.1#7874`(dnsmasq)→ clash:`nameserver: [127.0.0.1:6052]`(mosdns)、
`default-nameserver: [127.0.0.1:6052]``nameserver-policy` 国外域名 → DoH `https://1.1.1.1/dns-query`
`enhanced-mode: fake-ip`(198.18.0.1/16)、`ipv6: false`;nft 有 UDP/53 hijack → dnsmasq。
- **mosdns 配置缺陷(2026-08-12 发现并修复)**:`main` sequence 的国内分支
(`matches: qname $domestic_domains → exec: $domestic_upstream`)之后**缺少
`matches: has_resp → accept` 守卫**。mosdns v5 的 `sequence``forward` 成功后不会停止,
只有 `accept`/`reject`/`return` 或错误会终止——因此命中 `geosite_cn` 的查询会被转发**两次**
(AGH 与 223.5.5.5/119.29.29.29),最终应答来自最后一个 forward(国内公网 DNS),**AGH 的
拦截/rewrite 对 DIRECT 国内域名静默失效**。实测证据:`dup.baidustatic.com`(在 `geosite_cn`
且在 AGH 拦截表)经 mosdns 返回真实 IP `183.60.227.49` 而非 `0.0.0.0`。已修复(备份
`/etc/mosdns/config.yaml.bak-20260812`),修复后同一域名返回 `0.0.0.0`,taobao/google 解析
与 clash 链均无回归。**§6 的二期示例同款缺陷已一并修正。**
同日追加加固:`domestic_fallback`(fallback 插件:`primary: domestic_upstream`(AGH)、
`secondary: default_upstream`(223.5.5.5/119.29.29.29)、`threshold: 500ms`)使 AGH 宕机时
DIRECT 国内真实 IP 查询回退国内公网 DNS,不再直接报错;实测:AGH 停止时缓存未命中查询由
fallback 应答(NXDOMAIN/真实 IP),AGH 恢复后主路径即时应答且拦截(`0.0.0.0`)恢复。
备份:`/etc/mosdns/config.yaml.bak-fallback-20260812`
- AGH rewrites(实测):`hass.windy.lan`/`hass.local``192.168.55.11`;`dns.windy.lan``.36`;
`ubnt.windy.lan``.46`;`gfw.windy.lan``.1`;`nas.windy.local``.32`
- 拦截:仅启用 **AdGuard DNS filter**(filter_1);实测 `doubleclick.net`/`googleadservices.com``0.0.0.0`
**现状缺口(实测确认):**
1. AGH 上游是固定 DoH,**无"国内/国外分流"能力**;国外域名解析质量依赖唯一兜底路径。
2. **兜底失效**(kill-test 证实):主上游黑洞时,兜底 `adg.chans.xyz` 在客户端 15s 窗口内不生效
(`upstream_timeout: 10s` + TCP 重试行为),缓存未命中查询无有界降级——见 §8。
3. 国外域名 AAAA 经国内路径全部置空(见 §8),v6 解析缺位。
## 2. 最终裁决对齐(用户定稿 2026-08-12)
| # | 裁决 | 本issue处理 |
|---|------|------------|
| 1 | **保留 AGH `.36` 为唯一 LAN DNS 入口**(现有方案增强版),不改 EdgeRouter DHCP 通告 | ✅ 现状保持;本期零改动 |
| 2 | **否决"AGH 全局转发到 Clash fake-IP"**——DNS 平面必须与流量转发平面一致 | ✅ 分层方案(§4)明确 AGH 上游为**真实 IP** 解析路径,不与 fake-ip 混用 |
| 3 | **AGH → mosdns 仅为二期可选项**(经实测确有需求后启用) | ✅ §4 为二期方案;§8 kill-test 已给出"实测需求"证据(降级缺口) |
| 4 | 代理 VLAN10 将来用独立 OpenClash DNS 平面(fake-ip + TPROXY),不污染普通 LAN | ✅ 现状即此(dnsmasq→clash,非面向 LAN 客户端);文档记录 |
| 5 | **删除或明确禁用** `.1` 上未使用的 mosdns | ⚠️ 前提修正:mosdns 是 clash 的 nameserver,处于活动链路(§1)。处置改为**正式纳管并文档化**(本文件 + `hosts/gfw.windy.lan.md`),不删除 |
| 6 | 先完成验证再改动生产路径 | ✅ 本期完成全部 Phase 0 验证(§8),**未改任何生产 DNS 路径** |
| 7 | 建立 `home.arpa` 内部域(替代 `.local`) | ⏳ 后续任务:当前命名空间为 `.lan`(AGH rewrites + EdgeRouter DHCP domain),`hass.local` 兼容保留至迁移完成;home.arpa 需联动 AGH rewrites、DHCP domain、客户端,另行排期 |
| 8 | 高可用时增加第二个等价 AGH(独立物理故障域) | ⏳ 备用方案,记录不实施 |
## 3. 两个候选方案评估
### 方案 A:AGH 单独作为统一入口(现状演进)
- 优点:单解析点、面板/拦截/日志集中、维护简单。
- 缺点:AGH 对 geo 分流 + 防污染支持弱(官方定位是"过滤/家长控制")。固定 DoH 上游无法按域名
region 选路;且 §8 kill-test 显示主上游全挂时缓存未命中查询无有界降级。→ **不足以解决防污染/分流/降级问题。**
### 方案 B:mosdns 作为智能上游分流器
mosdns(v5)用 `sequence` 编排:`geosite/geoip` 匹配器 → 国内域名转发国内 DoH、国外域名转发加密 DoH(防污染),可加 `cache``reject`
- 优点:真正解决"国内快 / 国外不被污染"的分流;性能高。
- 缺点:纯转发器,无 Web 面板、无每客户端统计、拦截靠域名表。→ 单独当入口会退回原始体验。
**结论:两个方案互补,不是二选一。** 单用 A 无法分流防污染且降级无界;单用 B 失去 AGH 管理体验。
## 4. 二期可选项:分层架构(AGH 前端 + mosdns 后端,经实测需求后启用)
```
局域网客户端(DHCP DNS = 192.168.66.36)
AdGuard Home (66.36) ── 前端:广告/过滤、拦截表、每客户端统计、Web 面板
│ 上游 = mosdns
mosdns(66.36 伴生容器) ── 后端:智能分流 + 防污染
│ - geosite:cn → 国内 DoH/UDP(aliDNS / 腾讯 DNSPod)
│ - 其他 → 加密 DoH(自建 adg.chans.xyz 等)
上游 DoH
```
职责分离:
- **AGH = 策略/拦截/可观测**(拦截表、每客户端日志、面板)。
- **mosdns = 智能转发**(geo 分流 + 加密防污染 + 内置 cache,可显著缩短降级窗口)。
- **OpenClash(gfw)= 代理选路**(fake-ip + 规则)。与 DNS 解析选上游是两个独立决策,分开放最干净。
**部署位置:mosdns 与 AGH 同机(66.36 伴生容器),而非 gfw(.1)**:单点即 AGH 所在;不受网关重启/
OpenClash churn 影响;可纳入现有 compose/ansible 管理;不占用 OpenWrt 资源。放 gfw 会与 clash 的
DNS 处理互相干扰、耦合,且网关重启即断全 LAN DNS。**不推荐放 .1。**
> 启用条件(kill-test 实测需求,§8):主上游全挂时,当前 AGH 单入口对缓存未命中查询无有界降级。
> 分层方案(或下调 `upstream_timeout` + 改 failover 模式)可修;启用与否由用户在二期决定。
## 5. 更优替代方案(一并考虑)
1. **分层(推荐二期,见 §4)**:AGH(66.36)→ mosdns(66.36 伴生)→ 上游。体验最好、职责最清。
2. **纯 mosdns + 前端面板**:损失拦截/统计管理体验。**不推荐**用于替换。
3. **AGH 只挂一个带分流的上游(第三方 DoH 聚合)**:失去可控性且不可信。不推荐做主路径。
4. **全部交给 OpenClash fake-ip,关闭 AGH**:让"代理网关"成为全 LAN DNS 单点;且 AGH 拦截/日志也没了。**不推荐。**
## 6. mosdns 配置要点(mosdns v5,二期实施预留)
核心是 `sequence` + 上游拆分 + 缓存 + 屏蔽:
```yaml
plugins:
- tag: main
type: sequence
args:
- exec: cache 1024 # 缓存加速
- matches: has_resp
exec: accept
# 国内分流:命中 geosite:cn → 国内 DoH
- matches: [ qname &geosite:cn ]
exec: forward https://dns.alidns.com/dns-query
# 广告域名屏蔽交 AGH 前置,不重复维护
# 其余(国外)→ 加密 DoH 防污染
- exec: forward https://adg.chans.xyz/dns-query
- type: udp_server
args: { entry: main, listen: "127.0.0.1:5353" }
- type: tcp_server
args: { entry: main, listen: "127.0.0.1:5353" }
```
> **注意**:`sequence` 中每个 `forward` 分支之后必须跟 `matches: has_resp → accept`
> (或改用 `goto`/`jump` + `return` 结构),否则查询会继续执行后续规则被二次转发,
> 最终应答来自最后一个 forward——gfw 上 mosdns 的同类缺陷(2026-08-12)已实测并修复(见 §1)。
要点:
- 上游可加 `upstream``concurrent > 1` 与多地址故障切换;mosdns 自带 cache,能保证上游故障时
缓存命中仍即时应答(对应 §8 认定的降级缺口)。
- `geosite:cn` / `geoip:cn` 数据自动更新;国内 aliDNS/腾讯,国外可用自建 `adg.chans.xyz`(实测
唯一能返回国外 AAAA 的路径,§8)。
- 屏蔽交 AGH 前置,AGH 与 mosdns 不各自维护拦截表。
## 7. 迁移 / 实施顺序(二期,待用户确认启用)
1. 在 66.36 起 mosdns 伴生容器(`/opt/mosdns` + compose,**按 digest 固定镜像**,纳入 ansible)。
2. AGH「上游 DNS 服务器」改为指向 mosdns(`127.0.0.1:5353`,bootstrap 仍用公网 IP,避免
AGH → mosdns → AGH 死循环);只保留**一条语义一致的上游路径**,保持可回滚(备份 yaml + `--check-config`)。
3. 验证:国内域名、国外域名、被拦截域名、每客户端日志、AAA A 解析(§8 基线)。
4. 回归:EdgeRouter 通告不变(仍 `.36`),LAN 客户端无感;重启 AGH/mosdns 单点验证(§8 kill-test 模板)。
5. 上线后重跑 §8 kill-test,确认降级窗口有界。
## 8. Phase 0 验证证据(2026-08-12 全部实测)
### 8.1 基线与功能
| 项 | 结果 |
|----|------|
| rewrites:`hass.windy.lan` / `hass.local` | → `192.168.55.11` ✅(兼容保留) |
| rewrites:`dns.windy.lan` / `gfw.windy.lan` / `ubnt.windy.lan` / `nas.windy.local` | → `.36` / `.1` / `.46` / `.32` ✅ |
| 国内解析 `taobao.com`(经 AGH) | 真实 CN IP(59.82.x 等)✅ |
| 国外解析 `google.com` / `github.com`(经 AGH) | 真实 IP(142.250.x / 20.205.x),**无 fake-IP 泄漏** ✅ |
| 广告拦截 `doubleclick.net` / `googleadservices.com` | → `0.0.0.0` ✅ |
| clash 7874 `google.com` | `198.18.1.101`(fake-ip,仅网关/VLAN10 平面)✅ |
| clash 7874 `taobao.com` | 真实 IP(经 mosdns→AGH)✅ |
| dnsmasq :53(.1)`google.com` / `taobao.com` | fake-ip / 真实 IP ✅ |
| mosdns 6052 直连 `taobao.com` / `google.com` | 真实 IP(59.82.x / 142.250.73.78)✅ |
| DNSSEC:`dnssec-failed.org`(经 AGH) | 返回正常应答 `96.99.227.255`(非 SERVFAIL)→ 当前路径不校验,W1N-40 结论复现,**维持关闭** |
### 8.2 出口路径与 fake-IP 泄漏
- AGH 宿主默认路由 `via 192.168.66.254`(EdgeRouter),**直连出网,不经 gfw**;DoH 端点实测可达
(见 §1)。LAN 客户端经 AGH 的解析结果全部为真实 IP,无 `198.18/16` 泄漏。
- gfw 自身流量默认进代理(`openclash_mangle_output` mark 0x162 → tproxy :7895),
clash nameserver-policy 的 `1.1.1.1` DoH 实测 35ms 可达(走代理链路,不依赖直连)。
- mosdns 上游(AGH `.36``223.5.5.5`)命中本地/国内 bypass 规则,保持直连——设计意图达成。
### 8.3 IPv6 / RDNSS / AAAA
- LAN 有 IPv6 SLAAC(EdgeRouter dhcpv6-pd /60 → eth0 host-address + switch0,**仅 `service slaac`**,
**无 RDNSS/dns-server 通告**);`.36` 有全局 v6 地址 + RA 默认路由。
- **RDNSS 未通告** → v6 客户端无 v6 DNS,回退 v4 DNS(`.36`);AGH 仅监听 `0.0.0.0:53`(v4 only),无 v6 DNS 服务。
- **AAAA 解析实测**:`baidu.com` 公网本就无 AAAA(dns.google NOERROR/0,权威 NS 而已);
`taobao.com` AAAA 经国内路径正常(`2408:4001:f10::6f` 等);**国外域名(`google.com`)经
`223.5.5.5` UDP、alidns DoH、`8.8.8.8` UDP 全部返回空**,而 dns.google 与 `adg.chans.xyz`
DoH 均能返回 `2404:6800:4005:81a::200e` → **国内路径对国外域 AAAA 置空;`adg.chans.xyz`
兜底是当前唯一能返回国外 AAAA 的路径**。clash `ipv6: false` 亦不返回 AAAA。
### 8.4 自举(bootstrap)循环
- AGH `bootstrap_dns: [223.5.5.5, 223.6.6.6]`(公网 IP,非 AGH 自身)→ 无自举循环;AGH 解析
DoH 主机名不经过自身。二期方案要求 AGH→mosdns 时 bootstrap 仍用公网 IP(§7)。
### 8.5 Kill-test 矩阵(2026-08-12,全部实测)
| # | 场景 | 结果 |
|---|------|------|
| 1 | 重启 `.1` mosdns(init.d) | ✅ 直连 6052 与 clash 链恢复 |
| 2 | 重启 `.1` dnsmasq | ✅ 真实 IP 与 fake-ip 双路径恢复 |
| 3 | kill `.1` clash 核心 | ✅ LAN DNS(AGH)不受影响;gfw dnsmasq→clash **有界 3s 失败**(无卡死);OpenClash watchdog ~15s 自动拉起 |
| 4 | stop/start `.1` OpenClash | ✅ DNS 平面独立于代理;clash 与 tproxy 规则恢复 |
| 5 | 重启 `.36` AGH 容器 | ✅ 全量恢复:rewrites/拦截/国内外解析/DNSSEC 行为不变 |
| 6 | 黑洞 alidns DoH(223.5.5.5/223.6.6.6:443) | ✅ ~0.5s 内经 `doh.pub` 应答(load_balance 生效) |
| 7 | 黑洞全部主上游,兜底存活(adg.chans.xyz) | ⚠️ **客户端 15s 内无应答**——兜底未在窗口内生效 |
| 8 | 黑洞全部上游(含兜底),缓存未命中 | ⚠️ **25s 内无应答、无 SERVFAIL**——解析器对缓存未命中查询"卡死" |
| 9 | 黑洞全部上游,缓存命中 | ✅ 瞬时 NOERROR(cache 兜底) |
**结论(Phase 0 门禁):** 国内解析在代理停止/上游单点故障/组件重启下均维持可用;
但**"主上游全挂"时缓存未命中查询无有界降级**——`upstream_timeout: 10s` 与 TCP 重试行为使
兜底 `adg.chans.xyz` 在实践中无法在客户端期望窗口内生效。这是 §4 二期分层方案(或下调
`upstream_timeout` + failover 模式)的**实测需求依据**;按最终裁决,本期不改生产路径。
## 9. 风险与备注
- mosdns 仅监听 `127.0.0.1`(不对外),由 clash 消费;二期若启用,保持同样的边界,避免 LAN 出现两套入口。
- AGH 上游指向本机 mosdns 时务必配公网 bootstrap,否则自举死循环。
- 本方案不改 EdgeRouter DHCP/通告、不改 gfw OpenClash 代理规则,只动 66.36 上的 DNS 链路,风险可控。
- 已知降级缺口(§8.5 #7/#8):主上游全挂时缓存未命中查询无有界降级;启用二期前,LAN 客户端会感知
超时(约 10s+)。缓解:AGH cache 已覆盖高频域;根治需二期。
- 与 W1N-40「审查并修正 AdGuard Home」联动:该 issue 侧重 AGH 本身,本 issue 侧重整体 DNS 分层。