家庭网络搭建03-踩坑两年:DNS 慢、流媒体卡、规则难维护
Viewed: loading...
14 minutes to read
Hello 朋友们,上一篇家庭网络系列写到了软路由的基本原理,也搭了一套「看起来能跑」的方案。这两年一直在用,大部分时候确实挺顺,但有几个问题一直在后台静静地折腾我——有时候 GitHub 要等将近一分钟才能开、小红书在家 WiFi 下图片不加载、换了个 VPN 服务商自定义规则全失效……每次出问题都感觉「就差一点」,但就是搞不清楚根在哪里。
趁着这次把三件事全搞清楚了,顺手整理成这篇踩坑记录。
如果还没看过前两篇的朋友,可以先补补背景:
先说说现在的网络长什么样
当前的架构是这样:
光猫 → 主路由(优倍快 UniFi)→ 旁路由(OpenWRT + OpenClash + AdGuard Home)
↓
家庭设备(手机/电脑/平板)主路由从小米 AX3600 换成了优倍快(Ubiquiti UniFi),旁路由这套没动,OpenClash 跑 Fake-IP Mix 模式,AdGuard Home 负责 DNS 广告过滤。
换 UniFi 有一个很大的便利:它支持划分多个子网,每个子网可以单独配置 DHCP 下发的 DNS 和网关。我建了一个「科学上网」的 SSID,在这个网络的 DHCP 设置里把 DNS 和网关直接指向软路由 IP,设备连上去就自动走全链路,不需要手动改任何网络设置。在第二篇文章里提过那个麻烦——旁路由模式下每台设备都要手动改一遍——在这里被彻底解掉了:想走代理就连这个 SSID,想走普通直连就切另一个,软路由挂了也只需要切 SSID,5 秒恢复。
UniFi 主路由
├─ 普通家用 WiFi → DHCP DNS = 主路由自身 → 正常上网,不过软路由
└─ 科学上网 WiFi → DHCP DNS = 软路由 IP → 自动走 OpenClash 全链路理解这个架构,还有一条隐藏的链路很重要,那就是 DNS 的流向。设备发出一个 DNS 请求,它要经过几手才能拿到结果:
设备
└─ dnsmasq(53) ← OpenWRT 内置,DHCP 告诉设备 DNS 在路由器上
└─ OpenClash(7874) ← 实现 Fake-IP,必须在此拦截
└─ ADG(5335) ← 广告过滤
└─ 223.5.5.5 ← 真实上游看着有点绕,但这个结构是 Fake-IP 模式的必然设计。设备并不知道 dnsmasq 的存在,它只知道「DNS 在路由器上」,而路由器 IP 的 53 端口就是 dnsmasq。OpenClash 必须夹在 dnsmasq 和 ADG 之间,因为 Fake-IP 的核心逻辑——拦截 DNS 请求、返回假 IP、等 TUN 用假 IP 建连时再查表还原域名——要求它必须是 DNS 链路里最靠近设备的那一层。如果 OpenClash 排在 ADG 后面,DNS 结果早已出去,假 IP 机制就完全没法工作了。
所以这条 4 层链路不是冗余,是 Fake-IP 的必要代价。4 跳全在本地回环或局域网内,延迟是微秒级,在实际使用里感知不到。
「玄学的一分钟」:等很久,突然通了
交代完架构,说第一个让我折腾了挺久的问题:GitHub 偶尔要转圈接近一分钟才能打开,然后突然就通了。当时一直以为是节点质量的问题,换过几个节点,没什么用。
后来顺着 DNS 链路查下去,才发现根本不是节点的问题,问题出在 AdGuard Home 的两个配置项叠在了一起。
第一个是「Override minimum TTL」,我当时设的是 3600 秒——意思是不管域名的原始 TTL 是多少,ADG 都强制把 DNS 缓存延长到至少一小时。GitHub 的 DNS TTL 是 60 秒,但被 ADG 硬延到 3600,哪怕对方的 IP 已经切换了,我这边最多要等一个小时才能感知到变化。
第二个是「Optimistic Caching(乐观缓存)」:缓存过期时不等真正刷新,先把旧 IP 返回给你,同时在后台默默更新。表面上看是为了让 DNS 响应更快,但和 TTL 强制延长叠在一起就出问题了——旧 IP 被返回,你拿着这个 IP 发 TCP 连接,对方服务器根本不在那个地址上,TCP SYN 超时要等 63 秒……
于是就出现了那个「等一分钟,突然通了」的经典现象:不是运气好,是乐观缓存后台刷好了,你的下一次请求才拿到新 IP,计时器到了。
修法很直接:ADG DNS 设置里把 Override minimum TTL 改为 0(尊重域名原始 TTL),Optimistic Caching 关掉,顺手把 Fallback DNS 填上 8.8.8.8 和 1.1.1.1 做兜底,然后手动清空一次 DNS 缓存。改完之后,那个玄学一分钟彻底消失了。
小红书在家里加载慢,切 4G 秒好
DNS 问题解决之后,另一件事开始变得更明显:家里 WiFi 下刷小红书,图片经常加载不出来,抖音视频也反复缓冲;但一切手机流量就立刻恢复正常,说明锅在我的网络链路,不是 App 本身。
这个问题的根因其实很简单,理解了规则匹配的逻辑就清楚了:OpenClash 的分流规则是从上往下匹配的,命中了就按那条规则走,全都没命中就走最底部的 MATCH, Proxies——所有未知流量一律走代理节点。
小红书、抖音的图片和视频资源走的是国内 CDN,这些域名在我的订阅配置里根本没有,于是它们统统命中了 MATCH,绕着代理节点出境再回来,不慢才怪。
对应的 CDN 域名大概是这些:
| 域名 | 归属 |
|---|---|
xhscdn.com | 小红书图片 CDN |
douyinvod.com | 抖音视频 |
douyinpic.com | 抖音图片 |
byteimg.com | 字节系图片 CDN |
pstatp.com | 字节系静态资源 |
kuaishou.com | 快手 |
ksyun.com | 快手云 CDN |
把这些加到直连规则里,问题就消失了。但把这些域名加进去的过程,让我意识到了第三个也是最根本的问题……
真正的麻烦:规则和订阅绑在一起,迟早会崩
我原来维护自定义规则的方式是通过 subconverter:在 GitHub 上维护 proxy.list 和 direct.list,写好 clash-template.ini 模板,让 subconverter 把 VPN 订阅和自定义规则合并成一个 Clash 配置文件。这套方案在一个订阅上跑得挺好。
但问题是,这种方式把「规则」和「订阅」强绑在一起了。有一次我换了 VPN 服务商,切了新的订阅 URL,subconverter 没对接上,自定义规则全失效——不是规则写错了,是整个合并管道断了。
更麻烦的是,我现在用的 VPN 订阅(equaldcdn.com)有 Cloudflare 验证,subconverter 是服务端程序去拉节点的,Cloudflare 直接把它挡掉,返回一张人机验证页,拿不到任何节点,报「No nodes were found!」。这条路基本是死的。
折腾了一圈,最后找到了一个更干净的思路:让 OpenClash 自己去拉订阅,规则完全独立出来,单独从 GitHub 加载。
OpenClash 在拉订阅时有正确的 User-Agent,能过 Cloudflare 验证,不需要 subconverter 介入。规则独立之后,用的是 clash.meta 原生的 rule-providers 机制——在 GitHub 上维护两个 YAML 规则文件,clash.meta 启动时去拉取,之后每 24 小时自动刷新一次。
把规则注入进配置的方式,是 OpenClash 的「Custom Config Overwrite Scripts」——每次配置加载后运行的一段脚本,可以对 YAML 文件做任意修改:
#!/bin/sh
. /usr/share/openclash/ruby.sh
. /usr/share/openclash/log.sh
CONFIG_FILE="$1"
ruby - "$CONFIG_FILE" << 'ENDRB'
require 'yaml'
config = YAML.load_file(ARGV[0])
config['rule-providers'] ||= {}
config['rule-providers']['peter-proxy'] = {
'type' => 'http',
'behavior' => 'classical',
'url' => 'https://raw.githubusercontent.com/PeterChen1997/peter-config/master/config/clash/rule-providers/proxy-rules.yaml',
'path' => './peter-proxy.yaml',
'interval' => 86400
}
config['rule-providers']['peter-direct'] = {
'type' => 'http',
'behavior' => 'classical',
'url' => 'https://raw.githubusercontent.com/PeterChen1997/peter-config/master/config/clash/rule-providers/direct-rules.yaml',
'path' => './peter-direct.yaml',
'interval' => 86400
}
config['rules'] ||= []
config['rules'].unshift('RULE-SET,peter-direct,DIRECT')
config['rules'].unshift('RULE-SET,peter-proxy,Proxies')
File.write(ARGV[0], config.to_yaml)
ENDRB脚本做两件事:往配置里注入 rule-providers 定义,然后把 RULE-SET 引用插到所有规则的最前面,优先级最高。
GitHub 上的规则文件格式很简单,两个 YAML:
# proxy-rules.yaml
payload:
- DOMAIN-KEYWORD,supabase
- DOMAIN-KEYWORD,n8n
- DOMAIN-SUFFIX,docker.io
- DOMAIN-KEYWORD,instagram
# ...# direct-rules.yaml
payload:
- DOMAIN-SUFFIX,xhscdn.com
- DOMAIN-KEYWORD,xiaohongshu
- DOMAIN-SUFFIX,douyinvod.com
- DOMAIN-KEYWORD,byteimg
# ...Apply Settings 重启 OpenClash 之后,Core Log 里能看到 Start initial provider peter-proxy / peter-direct,流量日志里能确认 xiaohongshu.com → RuleSet(peter-direct) → DIRECT,说明规则已经生效了。
以后想加新域名,在 GitHub 上改一行、push,最迟 24 小时 clash.meta 自动刷新,重启 OpenClash 可以立即生效。换 VPN 订阅只需要在 OpenClash 的订阅管理里改 URL,自定义规则完全不受影响——这才是这件事最该有的样子。
写在最后
这套方案有一个轻微的套娃风险:规则文件托管在 GitHub,而 GitHub 有时需要代理才能访问 hhh。不过规则文件本地缓存 24 小时,就算 GitHub 临时抽风,已有的规则还是照常跑的,影响不大。
感兴趣的朋友可以看看我的 peter-config 仓库,里面有完整的规则文件。
附录:OpenClash 的 Fake-IP 是什么,和其他模式有什么区别
既然全文提了好几次 Fake-IP,顺便展开讲一下,这是理解整套方案的一个关键概念。
传统代理为什么有局限
你在浏览器里输入 github.com,第一件事是 DNS 解析——浏览器问 DNS「github.com 是哪个 IP?」,拿到 IP 之后再去建 TCP 连接。
Clash 想在这里做流量分流,但如果它只拦截 TCP 连接层,拿到的已经是 IP 了,域名信息丢失了。要用 DOMAIN-KEYWORD,github 这类规则来匹配,就得反向查「这个 IP 是从哪个域名解析来的」,又慢又容易出错,尤其是 CDN 域名一个 IP 对应大量域名,根本查不准。
Fake-IP 怎么解决这个问题
Fake-IP 的思路是在 DNS 阶段就把信息拿住:
设备问 DNS:「github.com 是哪个 IP?」
↓
OpenClash 拦截,不做真实解析
直接返回假 IP:198.18.0.123(RFC 预留测试地址段)
记下映射:198.18.0.123 ↔ github.com
↓
设备拿着 198.18.0.123 发起 TCP 连接
TUN 虚拟网卡拦截这个连接
查表:198.18.0.123 → github.com
匹配规则 → 走代理
代理节点去真正解析并连接 github.com设备全程以为自己在连 198.18.0.123,背后是代理帮你解析了真正的 IP。好处是 DNS 响应极快(不用问上游,直接返回假 IP),域名规则匹配精准,GEOIP 规则也正常(假 IP 不会被误判为中国 IP)。
Fake-IP 的一个坑
198.18.0.0/16 是 IANA 测试地址段,正常网络里不会出现。但一些国内 App、游戏 SDK 或 IoT 设备会校验 IP 是否合法,看到 198.18.x.x 就拒绝连接——直播 App 打不开房间、游戏 SDK 初始化失败,切 4G 就好了,就是这个原因。
解决办法是把有问题的域名加到 fake-ip-filter 列表里,加进去的域名走真实 DNS 解析,不返回假 IP。OpenClash 默认已内置了 NTP、STUN、Nintendo、PlayStation 等常见场景,大部分情况够用。
模式对比:该怎么选
OpenClash 的模式分两个维度:
DNS 模式:
| 模式 | DNS 返回什么 | 优点 | 缺点 |
|---|---|---|---|
| Fake-IP | 假 IP(198.18.x.x) | 快、域名匹配准 | 可能触发「非法 IP」校验 |
| Redir-Host | 真实 IP | 完全兼容,无非法 IP 问题 | 多一次 DNS 解析延迟,GEOIP 规则慢 |
流量拦截方式(Enhance Mode):
| 模式 | 拦截方式 | 覆盖范围 |
|---|---|---|
| TUN | 虚拟网卡,接管所有 IP 流量 | 最全,UDP 也能拦 |
| Mix(混合) | TUN + HTTP/S 系统代理 | 覆盖面略小,兼容性更好 |
| Normal(普通) | 仅 HTTP/S 系统代理 | 只有应用层代理,系统级流量会漏 |
我目前用的是 Fake-IP + Mix:Fake-IP 保证规则匹配速度和准确性,Mix 在 TUN 基础上补了系统代理,兼容性比纯 TUN 更好,偶尔遇到「非法 IP」就在 fake-ip-filter 里加一条。国内 App 多或有 IoT 设备的场景,可以考虑换成 Redir-Host + TUN,牺牲一点 DNS 速度换更好的兼容性。
好啦,这篇写得比前两篇长了不少 hhh,每一个坑都是真实踩过的,希望对有同款配置的朋友有点参考价值。咱们下次见
