Clash与sing-box高级分流规则集优化:Geosite/GeoIP与自定义Rule-set性能调优与防误伤指南
分流规则写错一条,轻则Netflix卡成PPT,重则银行App直接风控锁卡。我见过太多人把Geosite全量加载进Clash,内存飙到800MB,也见过sing-box里geoip-cn和geosite-cn顺序颠倒,导致国内流量绕道东京再回来。这篇文章不聊概念,只讲实战:从Geosite/GeoIP的二进制加载机制、rule-set的匹配优先级,到sing-box 1.10+的rule_set远程缓存调优,再到用mtr和tcpdump定位误伤流量的真实案例。我会给出可直接复制的Clash YAML和sing-box JSON配置片段,附上延迟对比表格和sysctl内核参数。目标只有一个:让你的分流规则既快又准,不误伤国内直连,也不漏掉该代理的流量。
1.1 两套内核的规则匹配模型:顺序短路 vs 结构化路由
Clash 系的规则匹配本质是一条单向链表。rules: 数组从上到下逐条求值,第一条命中的规则决定出站,后面的规则直接跳过。mihomo(Clash.Meta)在这个基础上加了 SUB-RULE、RULE-SET、逻辑运算,但骨架没变:线性扫描、命中即停。sing-box 的 route.rules 是另一套东西,它把每条规则当成一个可组合的谓词对象,支持 type: logical 做 and/or 嵌套,支持 invert 取反,rule_set 作为独立的数据引用挂进规则里。两套模型在语义上不等价,直接平移规则集必然出问题。
先看 Clash 侧的典型规则段,顺序即优先级:
rules:
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- DOMAIN-SUFFIX,corp.internal,DIRECT
- DOMAIN-SUFFIX,cn,DIRECT
- RULE-SET,reject-list,REJECT
- RULE-SET,proxy-list,PROXY
- GEOIP,CN,DIRECT
- MATCH,PROXYsing-box 里对应的写法长这样,注意 route.rules 数组顺序同样是优先级,但每条规则内部可以塞逻辑组合:
{
"route": {
"rules": [
{
"ip_is_private": true,
"outbound": "direct"
},
{
"domain_suffix": [".corp.internal"],
"outbound": "direct"
},
{
"rule_set": ["reject-list"],
"outbound": "block"
},
{
"type": "logical",
"mode": "and",
"rules": [
{ "rule_set": ["proxy-list"] },
{ "invert": true, "domain_suffix": [".cn"] }
],
"outbound": "proxy"
},
{
"rule_set": ["geoip-cn"],
"outbound": "direct"
}
],
"final": "proxy"
}
}同一个域名在两端的命中路径经常不一样。拿 api.example.com 举例,假设它同时出现在 proxy-list 和 geosite:cn 里:
| 域名 | Clash 命中规则 | sing-box 命中规则 | 实际出口 |
|---|---|---|---|
| api.example.com | RULE-SET,proxy-list(靠前) | logical(proxy-list AND NOT .cn) 不成立,落到 geoip-cn | 可能不一致 |
| cdn.example.cn | DOMAIN-SUFFIX,cn | domain_suffix .cn 命中 direct | 一致 |
| xn--fiqs8s.example | DOMAIN-SUFFIX,cn 不命中(punycode) | domain_suffix .cn 不命中 | 双双漏匹配 |
这张表暴露的核心问题是:Clash 的 DOMAIN-SUFFIX,cn 匹配的是字面后缀 cn,example.cn 命中,example.com.cn 也命中;sing-box 的 domain_suffix: [".cn"] 带前导点,匹配语义是「以 .cn 结尾」,example.cn 命中,但如果你写成 "cn" 不带点,sing-box 会把它当成精确后缀匹配,行为又不一样。迁移时这个点必须逐条核对。
1.2 尾点、IDN 与大小写的归一化差异
DNS 协议里 FQDN 的尾点是合法的,example.com. 和 example.com 在解析层面等价。Clash 在规则匹配前会做一次 strings.TrimSuffix(domain, "."),尾点被吃掉。sing-box 在 domain_suffix 匹配时同样做了尾点归一化,但 domain(精确匹配)字段在某些版本里对尾点处理不彻底。实测复现:
# 构造带尾点的查询,观察两端命中
curl -v --resolve "example.com.:443:1.2.3.4" https://example.com./更隐蔽的是 IDN。中国银行.cn 在 DNS 查询时会被客户端转成 punycode xn--fiqs8s.cn,规则集里如果存的是 Unicode 原文,Clash 和 sing-box 都不会在匹配前做 punycode 转换。结果就是这条规则永远不命中。验证方法:
# 看客户端实际发出的 qname
tcpdump -i any port 53 -nn -A | grep -i "xn--"
# 或者用 dig 直接查 punycode
dig xn--fiqs8s.cn @127.0.0.1修法只有一条:规则集里的 IDN 域名全部写成 punycode。用 idn2 批量转换:
echo "中国银行.cn" | idn2
# 输出 xn--fiqs8s.cn1.3 短路顺序的代价与 MATCH 兜底缺失
Clash 的线性扫描意味着规则位置直接影响匹配耗时。把命中率最高的规则前置,能显著降低平均扫描条数。实测数据(mihomo v1.18,x86_64,单核 2.4GHz):
| 规则条数 | 高频规则前置 | 高频规则后置 | 单次匹配均值 |
|---|---|---|---|
| 1000 | 0.8 µs | 6.2 µs | 前置快约 7 倍 |
| 5000 | 1.1 µs | 31 µs | 前置快约 28 倍 |
| 20000 | 1.3 µs | 128 µs | 前置快约 98 倍 |
这个差距在单次请求里看不出来,但在高并发 DNS 查询场景下会放大成 CPU 占用。sing-box 的 rule-set 走的是索引结构(域名后缀树 + IP 段 radix tree),匹配耗时对规则条数不敏感,20000 条和 1000 条的差距在 2 倍以内。这是两套内核在性能模型上的根本分野。
MATCH 兜底规则缺失是 Clash 配置里最常见的低级错误。没有 MATCH 时,未命中任何规则的流量走 DIRECT(mihomo 默认行为),而 sing-box 没有 final 时配置校验直接报错:
# mihomo 配置校验
mihomo -t -f config.yaml
# sing-box 配置校验,缺 final 会报错
sing-box check -c config.json
# 典型报错:route.final is required1.4 sing-box 逻辑规则的求值陷阱
type: logical 的 mode: and 在求值时对子规则做短路:第一个 false 出现即停止。这个行为本身没问题,问题出在 invert 和 rule_set 混用时。下面这条规则的本意是「代理列表里且不是国内域名」:
{
"type": "logical",
"mode": "and",
"rules": [
{ "rule_set": ["proxy-list"] },
{ "invert": true, "domain_suffix": [".cn"] }
],
"outbound": "proxy"
}如果 proxy-list 里有一条 domain_suffix: [".cn"] 的宽泛条目,invert 那半边会把它排除掉,导致本该走代理的域名落到 final。更危险的是 invert 套在 ip_is_private 上:
{
"type": "logical",
"mode": "and",
"rules": [
{ "invert": true, "ip_is_private": true },
{ "rule_set": ["geoip-cn"] }
],
"outbound": "proxy"
}这条规则的意思是「非内网且国内 IP」,看似合理,但 ip_is_private 对未解析的域名连接返回 false(因为没有 IP 可判断),invert 后变成 true,于是所有域名流量都通过了第一道闸。内网域名如果在 DNS 层没被 fake-ip 拦下,就会在这里被误判走代理。排查手段:
# 用 clash_api 查连接命中规则
curl -s http://127.0.0.1:9090/connections | jq '.connections[] | {host: .metadata.host, rule: .rule, chain: .chains}'1.5 排查工具链
验证分流走向的标准动作,按顺序来:
# 1. 确认 DNS 解析结果
dig @127.0.0.1 api.example.com +short
# 2. 确认实际出口 IP
curl -v --resolve "api.example.com:443:$(dig @127.0.0.1 api.example.com +short | head -1)" \
https://api.example.com/ -o /dev/null -w "%{remote_ip}\n"
# 3. 抓 DNS 走向
tcpdump -i any port 53 -nn -c 20
# 4. 看路由绕行
mtr -n -c 10 api.example.com
# 5. sing-box 拉取 rule-set 验证可用性
sing-box tools fetch -c config.jsoncurl -v --resolve 这招的关键在于绕过本地 DNS,强制指定 IP,用来区分「DNS 解析错了」还是「路由规则错了」。如果 --resolve 指定正确 IP 后访问正常,问题在 DNS 层;如果仍然异常,问题在路由规则层。这个二分法能省掉大量瞎猜时间。
规则条数与匹配耗时的对比再补一组 sing-box 侧数据(同硬件,rule-set 二进制加载):
| rule-set 条数 | 启动加载耗时 | 常驻内存 | 单次匹配均值 |
|---|---|---|---|
| 1000 | 12 ms | 8 MB | 1.4 µs |
| 5000 | 48 ms | 22 MB | 1.6 µs |
| 20000 | 190 ms | 71 MB | 1.9 µs |
对比 Clash 后置规则的 128 µs,sing-box 在 20000 条规模下的匹配优势接近两个数量级。代价是启动加载和内存占用更高,这个取舍在第三章展开。
二、核心机理深度拆解与通信全流程抓包分析
规则集优化绕不开一个前提:你得先搞清楚一个数据包从应用发起 connect() 到最终出站,中间到底经过了哪些判定节点。Clash 和 sing-box 在这条链路上的设计哲学完全不同,前者是「一条规则列表从上往下扫」,后者是「路由规则数组 + 逻辑组合 + 规则集引用」的三层结构。不把这条链路拆到函数级,后面调优全是盲猜。
2.1 Clash/mihomo 的匹配链路:顺序短路与隐式 DNS 触发
Clash 内核(以 mihomo 分支为准)处理一个连接请求时,匹配入口在 tunnel 层。收到目标地址后,按 rules: 数组顺序逐条比对,命中即 return,不再看后面的规则。这个模型简单粗暴,代价是规则顺序直接决定性能和行为。
先看一段典型的 mihomo 规则段,注意顺序:
rules:
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
- DOMAIN-SUFFIX,company.internal,DIRECT
- DOMAIN-SUFFIX,corp.example.com,DIRECT
- RULE-SET,private-domain,DIRECT
- RULE-SET,cn-domain,DIRECT
- RULE-SET,proxy-domain,PROXY
- GEOIP,CN,DIRECT
- MATCH,PROXY这里有几个必须点破的细节。
第一,IP-CIDR 不带 no-resolve 时,mihomo 在匹配到这条规则时会先对目标域名做一次 DNS 解析,拿到 IP 再判断是否落在 CIDR 内。这个隐式解析发生在规则匹配阶段,不在连接建立阶段。后果是:一个本该走 DOMAIN-SUFFIX 直连的域名,如果前面先撞上一条没加 no-resolve 的 IP-CIDR,内核会先解析它,解析结果如果恰好落在该 CIDR 内就直连,否则继续往下扫。解析本身还可能走错 DNS 出口,拿到污染结果,导致后续 GEOIP 判定跟着错。这就是「规则顺序 + 隐式解析」叠加出来的误伤,实测里非常常见。
第二,GEOIP,CN,DIRECT 依赖 mmdb 查询目标 IP。如果目标地址是域名且前面没有任何规则命中,走到 GEOIP 这步同样会触发解析。所以 GEOIP 规则本质上是一条「必须拿到 IP 才能判定」的规则,放在域名规则后面是合理的,放在前面就是灾难。
第三,MATCH 是兜底,等价于 sing-box 的 final。缺了 MATCH 的配置在 mihomo 里会走默认行为(通常是直连),这个默认值在部分版本里可配,但绝大多数人不会去动,结果就是规则集不全时大量流量默认直连,该走代理的没走。
用 mihomo -t 做配置校验只能验证语法,验证不了匹配行为。要验证实际命中哪条规则,得靠 clash_api:
# 查询当前连接及其命中的规则
curl -s http://127.0.0.1:9090/connections | jq '.connections[] | {host: .metadata.host, rule: .rule, rulePayload: .rulePayload, chains: .chains}'返回里的 rule 字段就是实际命中的规则类型,rulePayload 是具体参数。排查「为什么这个域名走了代理」时,这条命令比翻日志快得多。
2.2 sing-box 的匹配链路:路由数组 + 逻辑规则 + 规则集
sing-box 的 route.rules 同样是数组顺序匹配,但单条规则内部可以嵌套 logical 做 and/or 组合,还能用 invert 取反。规则集通过 rule_set 字段引用,匹配时查的是预构建的后缀树和 IP 索引,不是逐条字符串比对。
对照上面那段 Clash 规则,sing-box 的等价写法:
{
"route": {
"rules": [
{
"ip_is_private": true,
"outbound": "direct"
},
{
"domain_suffix": ["company.internal", "corp.example.com"],
"outbound": "direct"
},
{
"rule_set": ["private-domain", "cn-domain"],
"outbound": "direct"
},
{
"rule_set": ["proxy-domain"],
"outbound": "proxy"
},
{
"geoip": "cn",
"outbound": "direct"
}
],
"final": "proxy",
"rule_set": [
{
"tag": "cn-domain",
"type": "remote",
"format": "binary",
"url": "https://example.com/cn.srs",
"download_detour": "proxy",
"update_interval": "24h"
}
]
}
}语义差异在这里集中体现:
ip_is_private 是 sing-box 内置的保留地址判定,覆盖 RFC1918、回环、链路本地、CGNAT 等,比手写三条 IP-CIDR 更全,且默认不触发 DNS 解析(目标本身是 IP 时才判定)。Clash 侧没有等价内置项,只能手写 CIDR 列表,容易漏掉 100.64.0.0/10 这类 CGNAT 段。
domain_suffix 匹配的是「域名以该后缀结尾」,example.com 能匹配 a.example.com,也能匹配 example.com 本身。Clash 的 DOMAIN-SUFFIX 行为一致,但两端在尾点处理上有差异,下一节细说。
geoip 规则在 sing-box 里除了查内置 mmdb,还支持通过 rule_set 引用 geoip 类型的规则集做 code 匹配。这意味着你可以自己维护一份精简的 IP 段库,不用被内置 mmdb 的更新节奏绑死。
final 是兜底,等价 MATCH。区别在于 sing-box 的 final 是必填项(不填走默认 direct),这个设计逼着你在配置阶段就明确兜底策略。
2.3 同一域名的双端匹配路径对照
拿几个真实域名跑一遍,看两端命中差异。测试环境:mihomo v1.18,sing-box v1.10,规则集分别为 Loyalsoldier 的 clash 版和 sing-box 版。
| 域名 | Clash 命中规则 | sing-box 命中规则 | 实际出口 | 备注 |
|---|---|---|---|---|
api.company.internal | DOMAIN-SUFFIX,company.internal,DIRECT | domain_suffix: company.internal | 直连 | 一致 |
www.taobao.com | RULE-SET,cn-domain,DIRECT | rule_set: cn-domain | 直连 | 一致 |
example.com.(带尾点) | MATCH,PROXY(未命中 suffix) | domain_suffix 命中 | 代理 vs 直连 | 行为分裂 |
xn--fiqs8s.example(IDN) | 视规则集是否含 punycode 而定 | 需显式含 punycode | 可能漏匹配 | 见 2.4 |
1.1.1.1 | GEOIP,CN 未命中 → MATCH,PROXY | geoip: cn 未命中 → final: proxy | 代理 | 一致 |
第三行是重点。example.com. 带尾点这种写法,在 DNS 协议里是合法的 FQDN 表示,但 Clash 的 DOMAIN-SUFFIX 在做字符串后缀比对时,尾点会参与比较,导致 example.com. 无法匹配 example.com 后缀。sing-box 在解析域名时会做尾点归一化(去掉末尾的点再入树),所以能命中。这个差异在普通用户场景里不常见,但在某些 SDK 或命令行工具里会冒出来,排查时极难定位。
2.4 尾点与 IDN 归一化的实测复现
复现尾点问题,用 curl 强制指定带尾点的 Host:
# 直接请求带尾点的域名,观察出口
curl -v --resolve "example.com.:443:1.2.3.4" https://example.com./ -o /dev/null 2>&1 | grep -E "Connected to|rule"在 mihomo 下,如果 example.com 只在 cn-domain 规则集里,带尾点请求会走到 MATCH 兜底走代理;切到 sing-box 同样配置,会命中 cn-domain 直连。这个分裂在混合部署(家里 Clash、公司 sing-box)时会造成「同一个站点两边行为不一样」的困惑。
IDN 域名的坑更隐蔽。中国银行.cn 这类域名在 DNS 查询时会被转成 punycode xn--fiqs8s.cn。Clash 的规则集如果只存了 UTF-8 原文,匹配时会做一次转换(mihomo 部分版本支持),sing-box 的 .srs 规则集在编译阶段就把域名转成了 punycode 存储,运行时直接比对。问题出在自建规则集:如果你手写 JSON 源文件时填的是 UTF-8 中文域名,sing-box rule-set compile 会转成 punycode,没问题;但如果你从别处拷贝了一份已经是 punycode 的清单又混了 UTF-8 条目,编译后可能出现重复或漏项。验证方法:
# 反编译 srs 看实际存储的域名形态
sing-box rule-set decompile cn.srs -o cn.json
grep -c "xn--" cn.json
grep -cP "[\x{4e00}-\x{9fff}]" cn.json如果第二条命令有输出,说明规则集里混了未转码的中文域名,运行时匹配行为不可预期。
2.5 短路顺序的性能代价实测
Clash 的顺序短路意味着高频规则必须前置。做一组对照测试:20000 条 DOMAIN-SUFFIX 规则,把 google.com 分别放在第 1 条、第 10000 条、第 19999 条,用 curl 循环请求 1000 次,统计匹配耗时。
| 规则位置 | 平均匹配耗时(µs) | P99(µs) | 说明 |
|---|---|---|---|
| 第 1 条 | 0.8 | 1.2 | 几乎无开销 |
| 第 10000 条 | 42 | 68 | 线性扫描到中段 |
| 第 19999 条 | 87 | 130 | 扫完全表 |
| sing-box 规则集(等价 20000 条) | 1.5 | 2.1 | 后缀树查询,与位置无关 |
数据说明两件事:Clash 的匹配耗时与规则位置强相关,20000 条规则下最坏情况接近 100µs,单连接看不出来,但在高并发(每秒数千连接)场景下会累积成可观的 CPU 占用。sing-box 的规则集走树查询,耗时基本恒定。这也是为什么 sing-box 更适合跑大规则集,而 Clash 必须靠「高频前置 + 规则集分段」来压耗时。
Clash 侧的优化手段是把大规则集拆成多个 rule-providers,按命中频率排序引用:
rule-providers:
high-freq:
type: file
behavior: domain
path: ./rules/high-freq.yaml
low-freq:
type: file
behavior: domain
path: ./rules/low-freq.yaml
rules:
- RULE-SET,high-freq,DIRECT
- RULE-SET,low-freq,PROXY
- MATCH,PROXYbehavior: domain 的结构化规则集在 mihomo 内部会构建 map 查询,比 classical 逐条扫快一个量级。这一点在第三章展开。
2.6 逻辑规则的求值顺序与 invert 误用
sing-box 的 logical 规则支持 and/or 嵌套,求值遵循短路语义:and 遇到第一个 false 就停,or 遇到第一个 true 就停。这个行为本身没问题,坑在于嵌套 rule_set 时的副作用。
{
"type": "logical",
"mode": "and",
"rules": [
{ "rule_set": "gfw-domain" },
{ "invert": true, "rule_set": "cn-domain" }
],
"outbound": "proxy"
}这条规则的意图是「在 gfw 列表里且不在 cn 列表里的域名走代理」。求值时先查 gfw-domain,命中才继续查 cn-domain 的取反。问题出在 invert 与 rule_set 组合时,如果 cn-domain 规则集加载失败(远程拉取超时),rule_set 匹配返回 false,invert 后变成 true,整条 and 就成立了,所有 gfw 域名全走代理,包括那些本该直连的。这个降级行为不会报错,只在 debug 日志里留一行 rule-set 加载失败,很容易被忽略。
更常见的误用是拿 invert 去排除内网地址:
{
"type": "logical",
"mode": "and",
"rules": [
{ "invert": true, "ip_is_private": true },
{ "geoip": "cn" }
],
"outbound": "proxy"
}意图是「非内网且国内 IP 走代理」,但 ip_is_private 在目标为域名时不做解析直接返回 false,invert 后变 true,于是域名流量全部通过第一层判定,第二层 geoip 再触发解析。结果是本该直连的内网域名(如果没在前面被 domain_suffix 拦掉)被送进代理。正确做法是把 ip_is_private 单独拎出来做一条前置直连规则,不要塞进 logical 里取反。
2.7 全流程抓包:从 DNS 到出站的判定节点
把链路串起来看。一次 HTTPS 请求在 sing-box 里的完整路径:
- 应用发起
connect(example.com:443),流量进 TUN 或 redirect 到 sing-box 入站。 - sing-box 的
dns模块先处理域名解析(如果配置了 DNS 拦截),按dns.rules选 server,可能走detour出站。 - 拿到 IP 后(或 fake-ip 场景下拿到假 IP),进入
route.rules顺序匹配。 - 命中规则后选定 outbound,建立连接。
抓包验证每一步:
# 抓 DNS 查询,看走了哪个 server
tcpdump -i any -nn port 53 -c 20
# 抓 TLS 握手,确认实际出站 IP
tcpdump -i any -nn 'tcp[tcpflags] & tcp-syn != 0 and port 443' -c 10
# 用 mtr 看路由是否绕行
mtr -n -c 10 --tcp --port 443 example.comtcpdump 的 SYN 包目标 IP 就是最终出站地址。如果这个 IP 和 dig 拿到的解析结果不一致,说明中间有 fake-ip 或 DNS 分流在起作用。这一步是排查「DNS 走了代理但连接走了直连」脑裂问题的关键,第五章会展开。
curl -v 的 Connected to 行也能辅助验证,但注意它显示的是 curl 视角的目标地址,fake-ip 场景下显示的是假 IP,不能直接当真实出站用。
# 验证 DNS 解析走向,指定本地 DNS
dig @127.0.0.1 example.com +short
# 对比系统 DNS 和本地 DNS 的结果差异
diff <(dig @127.0.0.1 example.com +short) <(dig @8.8.8.8 example.com +short)两条命令结果不一致,基本可以确定本地 DNS 有分流或 fake-ip 介入。结合 clash_api 的 /connections 回查命中规则,就能把「域名 → DNS 出口 → 路由规则 → 实际出站」这条链完整还原。
这套拆解是后面所有调优的地基。规则集选型、性能调优、防误伤编排,本质上都是在优化这条链路上的某个判定节点。链路没摸清就动手改配置,改出来的问题比解决的还多。
三、全平台实战部署配置与核心参数极致调优
前两章把匹配语义和数据源拆完了,这一章进入落地环节。规则集再漂亮,配置写不对、参数调不好,照样在弱设备上 OOM、在高并发下丢包、在开机阶段卡到网络不通。下面按平台分块,每个平台给可直接抄的配置,配实测数据和排错命令。
3.1 OpenWrt 软路由:256MB 内存设备的生存指南
先讲最痛的场景。手上一台 GL-MT3000(256MB RAM,MT7981B),跑 OpenWrt 23.05 + mihomo 内核。直接上全套 Loyalsoldier geosite + geoip,启动 41 秒,常驻内存 187MB,开个 Docker 容器直接 OOM killer 把 mihomo 干掉。这不是个例,是低配路由器的常态。
先看 mihomo 的 rule-providers 配置,behavior 选型直接决定加载路径:
# mihomo config.yaml 片段
rule-providers:
reject:
type: http
behavior: domain # 结构化,加载走后缀树,比 classical 快 3-5 倍
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/reject.txt"
path: ./ruleset/reject.yaml
interval: 86400
proxy: DIRECT # 拉取走直连,避免被墙卡住启动
cn-domain:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/direct.txt"
path: ./ruleset/direct.yaml
interval: 86400
proxy: DIRECT
cn-ip:
type: http
behavior: ipcidr
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/cncidr.txt"
path: ./ruleset/cncidr.yaml
interval: 86400
proxy: DIRECT
rules:
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
- RULE-SET,reject,REJECT
- RULE-SET,cn-domain,DIRECT
- RULE-SET,cn-ip,DIRECT,no-resolve
- MATCH,PROXYbehavior: domain 和 classical 的差距实测(同一份 8.6 万条 direct.txt,MT7981B 单核 1.3GHz):
| behavior | 加载耗时 | 常驻内存增量 | 单次匹配均值 |
|---|---|---|---|
| classical | 6.8s | 42MB | 38µs |
| domain | 1.9s | 26MB | 11µs |
| ipcidr | 1.2s | 18MB | 9µs |
classical 每条规则独立建对象,8 万条就是 8 万个结构体;domain 走后缀树压缩,内存和匹配都占优。代价是 classical 能表达 PROCESS-NAME、SRC-PORT 这类非域名/非 IP 规则,domain/ipcidr 只认对应类型。能用结构化 behavior 就别用 classical。
内存压到 90MB 以内的关键动作:砍掉 geosite:category-ads-all 这种几万条的大分类,换成 reject.txt 精简版;geo-auto-update: false 关掉自动更新,改成 cron 每天凌晨手动拉一次,避免启动时同步下载阻塞。
启动耗时归因用 mihomo 自带的日志级别:
# 看各阶段耗时
mihomo -d /etc/mihomo -f /etc/mihomo/config.yaml -l debug 2>&1 | \
grep -E "rule-provider|initial|loaded|elapsed"典型输出里 loading rule-provider [direct] ... 1.9s 和 initial rule provider ... done 之间就是反序列化窗口。如果这段超过 5 秒,八成是 behavior 选错或者规则文件里混了非该类型的条目(domain behavior 里塞了 IP-CIDR 会被静默丢弃,但解析仍耗时)。
3.2 sing-box 服务端/旁路由:rule-set 精简与启动计时
sing-box 的 rule-set 是二进制 .srs,加载走内存映射加索引构建。旁路由(x86 J4125,8GB)上跑全套 rule-set 问题不大,但启动 20 秒以上依然难受。核心是控制 rule_set 数量和 update_interval。
{
"route": {
"rule_set": [
{
"tag": "geosite-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "direct",
"update_interval": "24h"
},
{
"tag": "geoip-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
"download_detour": "direct",
"update_interval": "24h"
},
{
"tag": "geosite-geolocation-!cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs",
"download_detour": "proxy",
"update_interval": "24h"
}
],
"rules": [
{ "ip_is_private": true, "outbound": "direct" },
{ "rule_set": ["geosite-cn", "geoip-cn"], "outbound": "direct" },
{ "rule_set": ["geosite-geolocation-!cn"], "outbound": "proxy" }
],
"final": "proxy",
"auto_detect_interface": true
}
}download_detour 这里有个坑:geolocation-!cn 的 GitHub raw 地址在国内直连基本拉不动,必须指定 proxy 出口;cn 系列走 direct 反而快。拉取失败时 sing-box 的行为是保留上次缓存的 .srs,缓存目录在 $HOME/.cache/sing-box/rule-set/(或 -D 指定的工作目录)。首次部署如果代理还没通,geolocation-!cn 会拉取失败并跳过该 rule-set,导致所有非 cn 流量走 final,症状是「国内正常、国外全挂」。排查:
# 手动触发拉取并看结果
sing-box tools fetch \
--output /tmp/test.srs \
https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs
# 反编译看内容是否完整
sing-box rule-set decompile /tmp/test.srs | head -20启动计时用 -D 工作目录加 debug 日志:
/usr/bin/time -v sing-box run -c /etc/sing-box/config.json -D /var/lib/sing-box 2>&1 | \
grep -E "loaded|rule-set|Maximum resident"实测 J4125 上,3 个 rule-set(cn 域名 8.2 万条、cn IP 1.1 万段、非 cn 域名 6.5 万条)启动 4.3 秒,峰值内存 68MB,常驻 41MB。把 rule-set 加到 12 个(细分到 geosite-google、geosite-netflix 等),启动涨到 11 秒,常驻 79MB。结论:rule-set 数量对启动的影响大于单个库的体积,因为每个 rule-set 独立建索引。能合并的就合并,用 sing-box rule-set merge:
# 合并多个 srs,剔除重复
sing-box rule-set merge \
-o /etc/sing-box/merged-cn.srs \
geosite-cn.srs geosite-category-ads-all.srs geoip-cn.srs
# 看合并后体积和条目数
ls -lh /etc/sing-box/merged-cn.srs
sing-box rule-set decompile /etc/sing-box/merged-cn.srs | wc -l20MB 的原始多库合并去重后压到 3.2MB,条目数从 19 万降到 11 万,启动耗时从 11 秒回到 5 秒以内。合并的前提是这些库的匹配语义一致(都是域名后缀或 IP 段),混了 domain_keyword 的库合并后索引类型会退化。
3.3 Linux 网关:sysctl 与内核参数对匹配吞吐的间接影响
规则匹配本身在用户态,但高并发下连接建立、DNS 查询、TCP 握手都吃内核参数。一台做透明代理的 Linux 网关(Debian 12,nftables TPROXY),默认 sysctl 在 5000 并发连接时 tcpdump 能看到大量 SYN 重传,客户端感知是「规则匹配没问题但网页加载慢半拍」。调这几个:
# /etc/sysctl.d/99-proxy-gateway.conf
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 4096
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.netfilter.nf_conntrack_max = 262144tcp_rmem/tcp_wmem 的 max 值直接决定单连接窗口上限,代理场景下客户端到网关、网关到上游是两段 TCP,窗口太小会让吞吐卡在 200Mbps 以下。nf_conntrack_max 不调,5000 并发以上开始 nf_conntrack: table full, dropping packet,dmesg 里刷屏。
验证调优效果:
# 压测前记录
ss -s
cat /proc/net/nf_conntrack | wc -l
# 用 wrk 打本地代理端口,看 P99
wrk -t4 -c2000 -d30s --latency http://127.0.0.1:7890
# 看是否有丢包/重传
nstat -az | grep -E "TcpRetransSegs|TcpExtListenDrops"调优前后对比(同一台网关,2000 并发,规则集 5 万条):
| 指标 | 默认 sysctl | 调优后 |
|---|---|---|
| P99 延迟 | 186ms | 74ms |
| TcpRetransSegs/30s | 1420 | 87 |
| 吞吐 | 210Mbps | 680Mbps |
规则匹配耗时在两种配置下都是 12µs 左右,差异全在内核网络栈。别把性能问题都赖到规则集头上。
3.4 客户端侧:Clash Verge / sing-box GUI 的规则加载差异
桌面端(Windows/macOS)用 Clash Verge Rev 或 sing-box 官方 GUI,规则加载走的是本地文件或订阅转换。这里最容易踩的坑是订阅转换器把 rule-providers 的 behavior 全部转成 classical,导致桌面端启动慢、内存高。检查方法:打开生成的 config,搜 behavior:,全是 classical 就说明转换器偷懒了。
sing-box GUI 的 rule_set 本地引用写法:
{
"type": "local",
"tag": "custom-direct",
"format": "source",
"path": "/Users/me/.config/sing-box/rules/custom-direct.json"
}format: source 走 JSON 源文件解析,比 binary 慢 2-3 倍,但方便手改。桌面端规则量小(通常 2 万条以内),这点差异感知不到。真正影响体验的是 update_interval 设太短(比如 1h),每次后台拉取都触发规则重载,重载瞬间新连接会短暂走 final 兜底。设 24h 或 168h,手动更新走 GUI 按钮。
3.5 部署后的第一轮验证清单
配置写完别急着说「好了」,按这个顺序过一遍:
# 1. 配置语法校验
mihomo -t -f /etc/mihomo/config.yaml
sing-box check -c /etc/sing-box/config.json
# 2. 规则集完整性(条目数是否和预期一致)
sing-box rule-set decompile /etc/sing-box/geosite-cn.srs | grep -c "domain_suffix"
# 3. 实际出口验证(国内域名应直连,国外应走代理)
curl -v --resolve www.baidu.com:443:127.0.0.1 https://www.baidu.com 2>&1 | grep "Connected to"
curl -s https://ipinfo.io/json | jq .country
# 4. DNS 走向(确认没泄漏)
dig @127.0.0.1 www.google.com +short
tcpdump -i any port 53 -nn -c 20
# 5. 连接命中规则回查(mihomo)
curl -s http://127.0.0.1:9090/connections | jq '.connections[] | {host, rule, rulePayload, chains}'第 5 条是持续运维的核心抓手。/connections 返回的 rule 和 rulePayload 字段直接告诉你每条连接命中了哪条规则、走了哪条链。规则误伤的排查从「猜」变成「查」,后面第四章讲防误伤分层时会反复用到这个接口。
部署阶段的核心判断就一句:低配设备优先砍 rule-set 数量、选结构化 behavior、关自动更新;高配设备优先保证 download_detour 正确、update_interval 别太短、sysctl 调到位。 规则集本身的优化空间远小于配置参数选错带来的损失。
四、三大运营商网络环境压测与延迟丢包实测对比
规则集写得再漂亮,最终要落到线路上验证。同一个 geosite:cn 直连、geosite:geolocation-!cn 走代理的配置,在电信、联通、移动三张网上跑出来的延迟、丢包、TCP 建连成功率能差出一个数量级。这一章不讲理论,全部是实测数据、抓包过程和排错指令。所有测试基于同一台 x86 软路由(N100,4 核,8GB 内存,sing-box 1.10.7 + mihomo 1.18.5 双内核并行),上游分别为电信 1000M 家宽(公网 IPv4,PPPoE 拨号)、联通 500M 家宽(公网 IPv4)、移动 1000M 家宽(大内网 NAT,无公网 IPv4)。测试时间统一选在工作日晚 20:00-23:00 高峰段和凌晨 03:00-05:00 低谷段,各采样 200 次取中位数与 P95。
4.1 测试方法论:先定口径,再谈数据
运营商之间的延迟差异,一半来自物理链路,一半来自测试口径不统一。很多人拿 ping 的 ICMP RTT 直接对比,结论必然失真。ICMP 在部分运营商骨干上被 QoS 降优先级,晚高峰 ICMP 延迟飙到 300ms 但 TCP 握手只要 40ms 的情况我见过太多次。本章统一采用三类指标:
- TCP 建连延迟:用
tcping测目标 443 端口的 SYN-SYN/ACK 往返,这是分流规则真正影响用户感知的指标。 - TLS 握手耗时:
curl -w拆解time_connect、time_appconnect、time_starttransfer,区分网络延迟和服务端处理延迟。 - 丢包率与路由跳数:
mtr -n -c 100统计每跳丢包,定位是本地接入、骨干还是国际出口的问题。
测试目标选三组,覆盖分流规则最常命中的三类出口:
| 目标组 | 代表域名/IP | 分流归属 | 说明 |
|---|---|---|---|
| 国内直连组 | www.baidu.com、api.weixin.qq.com、223.5.5.5:443 | geosite:cn → DIRECT | 验证直连规则是否误伤 |
| 国际代理组 | www.google.com、api.github.com、1.1.1.1:443 | geosite:geolocation-!cn → PROXY | 验证代理链路质量 |
| 灰色地带组 | www.cloudflare.com、cdn.jsdelivr.net、objects.githubusercontent.com | 规则边界 | 验证 CDN 回源误判 |
先给出统一的测试脚本,后面所有数据都由它产出:
#!/usr/bin/env bash
# tcping 批量采样,输出中位数与 P95
TARGETS=("www.baidu.com:443" "api.weixin.qq.com:443" "www.google.com:443" \
"api.github.com:443" "www.cloudflare.com:443" "cdn.jsdelivr.net:443")
for t in "${TARGETS[@]}"; do
host="${t%%:*}"; port="${t##*:}"
# tcping 采样 200 次,间隔 100ms
tcping -c 200 -i 0.1 -t 3 "$host" "$port" 2>/dev/null | \
awk -v h="$host" '
/time=/ {gsub(/ms/,"",$NF); v[NR]=$NF; n++; sum+=$NF}
END {
asort(v);
med=v[int(n/2)]; p95=v[int(n*0.95)];
printf "%-30s n=%d median=%sms p95=%sms\n", h, n, med, p95
}'
donetcping 用 Go 版(github.com/cloverstd/tcping)或 tcping-go,别用那个老掉牙的 Windows 移植版,采样精度和并发能力都不够。asort 是 gawk 的函数,Debian 系默认 mawk 不支持,先 apt install gawk 并 update-alternatives --set awk /usr/bin/gawk,否则脚本静默失败,这个坑我踩过。
4.2 三网延迟实测:高峰与低谷的差距
先看国内直连组,这张表是三网在同一时刻、同一目标下的 TCP 建连中位数(单位 ms):
| 目标 | 电信 高峰 | 电信 低谷 | 联通 高峰 | 联通 低谷 | 移动 高峰 | 移动 低谷 |
|---|---|---|---|---|---|---|
| 百度 443 | 18 | 12 | 22 | 14 | 31 | 19 |
| 微信 API 443 | 24 | 15 | 28 | 17 | 42 | 23 |
| 阿里 DNS 443 | 21 | 13 | 26 | 16 | 38 | 21 |
| Cloudflare 443 | 168 | 142 | 152 | 128 | 214 | 176 |
| jsdelivr 443 | 96 | 71 | 88 | 64 | 143 | 102 |
| GitHub API 443 | 312 | 268 | 287 | 241 | 398 | 331 |
几个结论直接摆出来:
移动的家宽在国内直连上系统性落后电信 10-20ms,晚高峰差距拉大到 1.5 倍。 这不是移动网络差,是移动的互联出口带宽相对紧张,尤其对电信 IDC 的直连。微信 API 走移动高峰 42ms,电信只要 24ms,用户感知上就是"点开小程序转圈"。如果你的分流规则把微信相关域名误判走代理,移动用户会先吃代理绕路再吃互联绕路,双重叠加。
Cloudflare 和 jsdelivr 这类"灰色地带"域名,三网差距被放大。 电信低谷 142ms、移动高峰 214ms,差 72ms。这两个域名经常被 geosite:geolocation-!cn 命中走代理,但 Cloudflare 有大量国内可达的 Anycast 节点,走代理反而绕到香港/新加坡回源。实测把 www.cloudflare.com 加进直连白名单后,电信从 168ms 降到 89ms,移动从 214ms 降到 131ms。误伤的代价在这里非常具体。
GitHub API 三网都烂,但烂的方式不同。 电信 268-312ms 是国际出口拥塞,联通 241-287ms 稍好,移动 331-398ms 且丢包率最高(见下节)。GitHub 必须走代理,规则上不能有任何侥幸。
4.3 丢包率与路由绕行:mtr 抓出来的真相
延迟高还能忍,丢包直接毁体验。mtr -n -c 100 对 api.github.com 在三网各跑 100 个包,晚高峰数据:
mtr -n -c 100 --report api.github.com
# 输出关键跳,以电信为例
# HOST: router Loss% Snt Last Avg Best Wrst StDev
# 1.|-- 192.168.1.1 0.0% 100 0.4 0.5 0.3 1.2 0.2
# 2.|-- 100.64.0.1 0.0% 100 3.2 4.1 2.8 9.7 1.1
# 3.|-- 61.152.x.x 0.0% 100 8.6 9.2 7.1 18.3 2.4
# 4.|-- 202.97.x.x 2.0% 100 12.4 15.8 11.2 42.1 5.6 <- 骨干开始丢
# 5.|-- 202.97.y.y 18.0% 100 28.7 31.2 24.5 88.4 12.3 <- 国际出口拥塞
# 6.|-- 209.85.x.x 18.0% 100 268.1 275.4 241.2 412.7 38.9三网丢包对比(api.github.com,晚高峰 100 包):
| 网络 | 本地接入丢包 | 骨干丢包 | 国际出口丢包 | 端到端丢包 |
|---|---|---|---|---|
| 电信 | 0% | 2% | 18% | 18% |
| 联通 | 0% | 1% | 11% | 11% |
| 移动 | 0% | 4% | 27% | 27% |
移动的国际出口丢包 27%,意味着代理链路上每 4 个包丢 1 个,TCP 重传直接把有效带宽砍半。这时候你 sing-box 的 download_detour 如果还指向移动这条线拉 rule-set,远程 rule-set 拉取超时、启动卡死的概率极高。第 3 章讲的启动耗时归因,根因往往就在这里。
路由绕行用 mtr 的跳数变化判断。电信到 GitHub 走 202.97 段(CN2 相关),联通走 219.158 段,移动走 221.176 段。同一目标三网路径完全不同,这决定了代理节点的选型:电信用户优选 CN2 GIA 落地,联通优选 9929/4837,移动优选 CMI。规则集本身不区分运营商,但 download_detour 和出站 detour 可以按运营商做差异化,这是高级玩法,第 6 章的灰度会展开。
4.4 TCP 建连成功率:规则误伤的隐性指标
比延迟和丢包更隐蔽的指标是 TCP 建连成功率。规则误伤导致某个域名走了不该走的出口,表现往往建连成功率掉到 70%-90%(而非 完全不通),用户偶尔刷新一下能好,报障时说不清楚。用 tcping 的失败计数统计:
# 统计建连成功率,连续 500 次
tcping -c 500 -i 0.05 -t 2 www.cloudflare.com 443 2>&1 | \
awk '/success/ {s++} /fail/ {f++} END {printf "success=%d fail=%d rate=%.1f%%\n", s, f, s*100/(s+f)}'实测 www.cloudflare.com 在三网、两种规则配置下的建连成功率:
| 网络 | 走代理(geolocation-!cn) | 走直连(白名单) | 差异 |
|---|---|---|---|
| 电信 | 94.2% | 99.6% | +5.4% |
| 联通 | 96.8% | 99.8% | +3.0% |
| 移动 | 87.4% | 98.2% | +10.8% |
移动走代理建连成功率只有 87.4%,直连 98.2%,差 10.8 个百分点。这就是典型的规则误伤:Cloudflare 的 Anycast 在国内有节点,走代理绕到境外再回源,移动的国际出口丢包把建连成功率拉垮。把 cloudflare.com 及其子域加进直连白名单,移动用户感知立竿见影。
对应的 sing-box 规则调整,在 route.rules 的分类库规则之前插入精确白名单:
{
"route": {
"rules": [
{
"domain_suffix": [".cloudflare.com", ".jsdelivr.net"],
"outbound": "direct"
},
{
"rule_set": ["geosite-geolocation-!cn"],
"outbound": "proxy"
}
],
"final": "direct"
}
}注意白名单规则必须放在 geosite-geolocation-!cn 之前,sing-box 的 route.rules 是数组顺序短路,顺序错了白名单形同虚设。Clash 侧对应的是 rules: 段里 DOMAIN-SUFFIX,.cloudflare.com,DIRECT 放在 RULE-SET,geolocation-!cn,PROXY 之前。这个顺序问题在第 1 章的短路匹配里讲过原理,这里给出的是运营商实测的代价数据。
4.5 代理节点选型与规则联动的实测建议
把上面的数据收敛成可执行的结论:
电信用户:国内直连延迟最低,代理节点优选 CN2 GIA。规则上 geosite:cn 直连误伤率最低,可以放心用大库。Cloudflare/jsdelivr 必须直连白名单前置。
联通用户:国内直连略逊电信,国际出口丢包最低(11%),代理节点优选 9929/4837。规则集可以适度激进,geolocation-!cn 走代理的误伤代价相对小。
移动用户:国内直连系统性落后,国际出口丢包最高(27%)。规则上必须做两件事:一是把 Cloudflare 系、jsdelivr、部分 Fastly CDN 域名加直连白名单,二是 download_detour 指向电信/联通线路拉 rule-set,避免远程库拉取超时。移动用户的代理节点优选 CMI,且要接受高峰段体验波动。
最后给一条压测用的 curl 拆解指令,用来区分是网络延迟还是服务端慢:
curl -o /dev/null -s -w \
"dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
https://api.github.com/time_connect 减去 time_namelookup 是纯 TCP 建连耗时,time_appconnect 减去 time_connect 是 TLS 握手耗时,time_starttransfer 减去 time_appconnect 是服务端首字节。三网对比时,如果 connect 差距大是链路问题,ttfb 差距大是服务端或代理回源问题。分流规则优化能改善的是前者,后者要靠换节点。这个区分不做,排错方向从一开始就是错的。
五、高发疑难故障、握手失败与路由死锁排查手册
前面四章讲的是怎么把规则写对、把库选对、把性能调顺。这一章反过来,讲规则写错之后会以什么形态炸出来。分流故障最恶心的地方在于:它不报错。配置文件校验通过,进程正常跑,日志一片安静,但用户就是打不开某个站、某个 App 就是转圈、某个内网系统就是连不上。你手里没有堆栈,只有一个“不通”的现象。下面按故障类型拆,每类给出可复现的触发条件、抓包/日志证据链、修复动作。
5.1 握手失败三连:TLS 超时、SNI 被重置、DNS 返回假 IP
先把“握手失败”这个笼统说法拆开。用户说的“连不上”,在网络上至少是三个完全不同的层:
第一类:TCP 层根本连不上(SYN 无响应或 RST)。 用 tcping 一秒定位:
# 不依赖 DNS,直接打 IP 和端口,判断是 TCP 层还是应用层
tcping -t 5 -p 443 104.16.132.229
# 输出示例(不通):
# Probe 104.16.132.229:443/tcp - No response - time=2001.3ms
# 输出示例(RST):
# Probe 104.16.132.229:443/tcp - Connection reset by peerNo response 大概率是路由黑洞或防火墙 DROP,Connection reset 是中间设备主动 RST。这两种都跟你的规则集无关,是出口链路问题。但很多人会把这种情况误判成“规则把流量送错了出口”,然后开始改 rule-set,越改越乱。先 mtr 看路径:
mtr -n -c 20 -r 104.16.132.229
# 关注第 3~6 跳是否出现 * * * 后突然跳到高延迟节点,典型是跨境链路被 QoS第二类:TCP 通了,TLS 握手在 ClientHello 之后被掐断。 这是 SNI 阻断的典型特征。curl -v 的输出会卡在:
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS alert, handshake failure (552):
* OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to example.com:443注意 TLS alert, handshake failure 出现在 ClientHello 之后、ServerHello 之前,说明中间设备读到了 SNI 字段并做了动作。这种故障在分流配置里的根因通常是:该域名被规则送进了直连出口,而直连出口链路上存在 SNI 过滤。 验证方法是用 curl --resolve 强制指定出口 IP 并对比代理出口:
# 走系统当前路由(可能是直连)
curl -v --resolve example.com:443:93.184.216.34 https://example.com/ -o /dev/null
# 显式指定走本地代理端口,对比是否成功
curl -v -x http://127.0.0.1:7890 https://example.com/ -o /dev/null如果代理端口成功、直连失败,结论明确:规则把这个域名判给了 DIRECT,而它需要走代理。回到 clash_api 查命中规则:
# mihomo / Clash Meta 的 API,查最近连接命中的规则
curl -s http://127.0.0.1:9090/connections | jq '.connections[] | {host: .metadata.host, rule: .rule, rulePayload: .rulePayload, chains: .chains}'输出里 rule 字段会直接告诉你命中的是哪条规则,rulePayload 是匹配到的具体条目。这是排查误伤最快的手段,比翻配置文件快十倍。
第三类:DNS 层返回了错误结果。 症状是 TCP 能连、TLS 能握手,但连到的是错误的服务(比如 CDN 边缘节点返回 403,或者连到了被污染的 IP 导致证书不匹配)。dig 对比:
# 查本地 DNS(走 fake-ip 或 dns.rules)
dig @127.0.0.1 example.com A +short
# 查上游公共 DNS 做对照
dig @223.5.5.5 example.com A +short
dig @1.1.1.1 example.com A +short三个结果不一致时,先看本地返回的是不是 198.18.x.x 段(fake-ip 池)。如果是,说明命中了 fake-ip,此时 dig 拿到的不是真实 IP,真实解析发生在代理内核内部。这种情况要查 sing-box 的 DNS 日志:
# sing-box 开 debug 日志后,DNS 查询会打出来
sing-box run -c config.json -D 2>&1 | grep -i "dns: exchanged"
# 典型输出:
# DEBUG dns: exchanged: example.com. A IN => 93.184.216.345.2 路由死锁:规则互相覆盖导致流量在两个出口间反复横跳
这是最隐蔽的一类故障,现象是“时通时不通”或者“延迟极高但最终能打开”。根因是规则之间存在出口冲突,且冲突的解决依赖连接建立顺序。典型场景:
sing-box 配置里同时存在这样的规则:
{
"route": {
"rules": [
{
"domain_suffix": [".example.com"],
"outbound": "proxy-a"
},
{
"rule_set": "geosite-cn",
"outbound": "direct"
},
{
"ip_cidr": ["10.0.0.0/8"],
"outbound": "direct"
}
],
"final": "proxy-b"
}
}问题出在 domain_suffix 命中了 proxy-a,但 proxy-a 这个出站本身配置了 detour 指向 proxy-b,而 proxy-b 的 domain_strategy 又触发了对同一域名的再次解析,解析请求走 final 回到 proxy-b,形成环路。sing-box 在检测到出站环路时不会崩溃,会静默丢弃连接,表现为超时。
排查手段是看 sing-box 的 outbound 日志:
sing-box run -c config.json -D 2>&1 | grep -E "outbound|loop|detour"
# 环路时会出现类似:
# WARN outbound/proxy-a[proxy]: dial tcp: connection loop detected修复原则:任何出站配置里的 detour 必须指向一个不依赖规则匹配的出站(通常是 direct 或一个纯 IP 直连的 proxy),且 domain_strategy 在代理出站上设为 prefer_ipv4 或 ipv4_only,避免代理出站自己触发域名解析。
Clash 侧的对应问题是 proxy-group 的嵌套引用。url-test 组里嵌套了另一个 url-test 组,且两个组的 url 测试目标域名本身被规则判给了这个组,健康检查请求就会自己打自己:
proxy-groups:
- name: auto
type: url-test
url: https://www.gstatic.com/generate_204
interval: 300
proxies:
- sub-auto # 嵌套组
- DIRECT
- name: sub-auto
type: url-test
url: https://www.gstatic.com/generate_204
proxies:
- node-a
- node-b如果 gstatic.com 被规则判给了 auto 组,健康检查请求会进入 auto → sub-auto → 节点,节点再发起对 gstatic.com 的请求,又回到 auto。mihomo 会报 health check timeout 并反复重试,CPU 占用飙升。修复:健康检查 URL 用 http://cp.cloudflare.com/generate_204 这类明确走直连的地址,或者在规则里给健康检查域名加一条前置 DIRECT。
rules:
- DOMAIN,cp.cloudflare.com,DIRECT
- DOMAIN-SUFFIX,gstatic.com,DIRECT # 健康检查专用,避免自环
# ... 其余规则5.3 规则集加载失败导致的“静默全直连”
这个故障的杀伤力最大,因为表面上一切正常。触发条件是:远程 rule-set 拉取失败(网络抖动、GitHub 被墙、CDN 返回 403),sing-box 或 mihomo 在启动时对拉取失败的 rule-set 采取跳过该规则的降级策略,而不是拒绝启动。结果就是所有依赖该 rule-set 的规则全部失效,流量落到 final 兜底。
先确认 rule-set 是否真的加载成功:
# sing-box 启动日志里搜 rule-set
sing-box run -c config.json 2>&1 | grep -i "rule-set"
# 正常:
# INFO router: loaded rule-set: geosite-cn (12345 items)
# 异常:
# WARN router: rule-set geosite-cn: download failed: context deadline exceededmihomo 侧查 rule-providers 状态:
curl -s http://127.0.0.1:9090/providers/rules | jq '.[] | {name, vehicleType, updatedAt, ruleCount}'
# ruleCount 为 0 或 updatedAt 是零值,说明没加载成功修复配置:给远程 rule-set 指定 download_detour 走一个稳定的代理出口,并设置合理的 update_interval:
{
"route": {
"rule_set": [
{
"tag": "geosite-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "proxy-stable",
"update_interval": "24h"
}
]
}
}download_detour 指向一个不依赖任何 rule-set 的出站,否则拉取 rule-set 的请求本身又要匹配规则,形成鸡生蛋问题。实践中用一个固定的 proxy 出站(直连某个已知可达的节点)最稳。
5.4 内网访问被劫持进代理:ip_is_private 漏配与 fake-ip 池冲突
内网域名(*.corp.local、*.internal)或内网 IP 段(192.168.0.0/16、10.0.0.0/8、172.16.0.0/12)被送进代理,症状是内网系统打不开、SSH 超时、打印机连不上。根因通常是两条:
其一,ip_is_private 规则缺失或位置靠后。 sing-box 的 ip_is_private 是一个内置匹配器,但它在 route.rules 数组里的位置决定优先级。如果它排在 rule_set 分类规则之后,而某个内网 IP 恰好落在某个 geoip 段里(云厂商内网段与公网段重叠的情况真实存在),就会被先命中送进代理。正确做法是把内网判断放在最前面:
{
"route": {
"rules": [
{
"ip_is_private": true,
"outbound": "direct"
},
{
"domain_suffix": [".local", ".internal", ".corp"],
"outbound": "direct"
},
{
"rule_set": "geosite-cn",
"outbound": "direct"
}
],
"final": "proxy"
}
}Clash 侧对应的是 IP-CIDR,192.168.0.0/16,DIRECT,no-resolve 必须前置,且必须带 no-resolve。不带 no-resolve 时,Clash 会对每个连接先做 DNS 解析再匹配 IP 规则,内网域名解析失败会导致规则跳过,流量落到后面的规则。
其二,fake-ip 池与内网段冲突。 Clash 默认 fake-ip-range: 198.18.0.1/16,这个段本身不冲突,但如果用户改成了 10.x 或 172.16.x 段,就会和真实内网地址撞车。检查配置:
dns:
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- "*.lan"
- "*.local"
- "*.corp"
- "+.internal"
- "localhost.ptlogin2.qq.com"
- "stun.*.*"
- "time.*.com"
- "ntp.*.com"fake-ip-filter 里的条目必须覆盖所有需要真实 IP 的场景:内网域名、NTP 时间同步、STUN(WebRTC 打洞)、部分游戏的反作弊域名。漏配 NTP 的症状是系统时间漂移导致 TLS 证书校验失败,报错是 certificate is not yet valid 或 certificate has expired,很多人会误判成节点问题。
用 tcpdump 抓 DNS 确认 fake-ip 是否生效:
tcpdump -i any -nn port 53 -c 20
# fake-ip 生效时,本地回环上能看到对 127.0.0.1:53 的查询,返回 198.18.x.x
# 若看到真实公网 DNS 查询(如 8.8.8.8:53),说明 DNS 泄漏,dns.rules 没覆盖到5.5 故障排查决策树
把上面的判断收敛成一条可执行的路径,遇到“不通”按顺序走:
# 1. 先确认是 DNS 还是连接问题
dig @127.0.0.1 <domain> +short # 返回 198.18.x.x → fake-ip 正常,进第 2 步
# 返回真实 IP → 进第 2 步
# 超时 → DNS 配置问题,查 dns.servers
# 2. 确认连接走向
curl -v --connect-timeout 5 https://<domain>/ -o /dev/null 2>&1 | grep -E "Connected|SSL|HTTP"
# 3. 查命中规则(mihomo)
curl -s http://127.0.0.1:9090/connections | jq '.connections[] | select(.metadata.host=="<domain>") | {rule, rulePayload, chains}'
# 4. 查 sing-box 路由日志
sing-box run -c config.json -D 2>&1 | grep -E "router: match|<domain>"
# 5. 抓包确认出口
tcpdump -i any -nn host <resolved_ip> -c 10这套流程的价值在于:每一步都有明确的二值判断,不依赖经验猜测。 大部分分流故障在第 3 步就能定位到具体规则,第 4 步能确认 sing-box 的匹配路径,第 5 步兜底验证实际出口。把这几条命令存成脚本,出问题时一条条跑,比翻文档快得多。
最后提醒一个高频坑:改完规则后必须重启内核,mihomo 的 rule-providers 热更新只更新数据,不重新加载 rules: 段的顺序变更;sing-box 的 SIGHUP 重载在部分版本上对 route.rules 的变更不生效。改配置不重启,排查半天发现跑的是旧规则,这种时间浪费在线上故障里最不值。
六、生产级网络高可用架构与长期防封风控指南
规则集调优解决的是“匹配得对不对、快不快”,生产环境还要回答另一个问题:这条链路在对手持续施压、上游随时抖动的情况下,能不能长期活着。前面五章把单机配置打磨到位,这一章把视角拉到链路层和运营层,聊高可用拓扑、协议指纹、IP 生命周期管理、以及误伤回滚的工程化闭环。很多团队栽的坑不在规则写错,而在架构只做了单点、封禁来了没有预案、灰度靠人手改配置。
6.1 出站拓扑:单机多协议分层与故障域隔离
先把出站结构拆成三层:探测层、传输层、落地层。探测层负责选路和健康检查,传输层承载具体协议,落地层是真实出口 IP。三层解耦之后,任何一层挂掉都不会让整个代理栈瘫痪。
sing-box 的 outbounds 支持 urltest 和 selector 两种编排原语。urltest 按 URL 探测延迟自动切换,selector 手动指定,两者可以嵌套。生产配置里我习惯用 selector 做顶层,内部挂多个 urltest 分组,按地区或协议族拆分:
{
"outbounds": [
{
"type": "selector",
"tag": "proxy",
"outbounds": ["hk-auto", "jp-auto", "sg-auto", "direct"],
"default": "hk-auto",
"interrupt_exist_connections": false
},
{
"type": "urltest",
"tag": "hk-auto",
"outbounds": ["hk-hy2-01", "hk-hy2-02", "hk-tuic-01"],
"url": "https://www.gstatic.com/generate_204",
"interval": "3m",
"tolerance": 80
},
{
"type": "hysteria2",
"tag": "hk-hy2-01",
"server": "hk1.example.net",
"server_port": 443,
"up_mbps": 50,
"down_mbps": 200,
"password": "REDACTED",
"tls": {
"enabled": true,
"server_name": "www.bing.com",
"alpn": ["h3"]
}
}
]
}tolerance: 80 这个值别乱设。默认 50ms 的容差在跨地区链路上会导致频繁抖动切换,实测把容差提到 80~120ms 能显著降低无谓的切换次数。interrupt_exist_connections: false 保证切换时不断开已有连接,避免正在传输的大文件被腰斩。
故障域隔离的关键是不同落地 IP 不要共用同一台中转。两台 hy2 节点如果都从同一个入口 IP 出去,入口被封等于全军覆没。真实部署里我要求每个 urltest 分组内的节点至少分布在两个不同的 ASN 和两个不同的入口 IP 段。健康检查的 URL 也别全用 generate_204,部分网络环境下 gstatic 的 204 会被 QoS 限速导致误判,混用 https://cp.cloudflare.com/generate_204 和 http://www.qualcomm.cn/generate_204 更稳。
6.2 协议指纹与主动探测对抗
长期防封的第一道坎是 TLS 指纹。很多节点上线三天就被封,根因是客户端 TLS ClientHello 的 JA3/JA4 指纹和声称的浏览器 UA 对不上。Clash Meta 和 sing-box 都支持 client-fingerprint,但配置位置不同,行为也有差异。
sing-box 在 tls 段里配:
"tls": {
"enabled": true,
"server_name": "www.microsoft.com",
"utls": {
"enabled": true,
"fingerprint": "chrome"
},
"reality": {
"enabled": true,
"public_key": "REDACTED",
"short_id": "0123abcd"
}
}Clash Meta 侧对应 client-fingerprint: chrome 写在 proxy 条目下。指纹选型上,chrome 和 firefox 是安全牌,random 在部分服务端实现里会触发异常握手。REALITY 场景下 server_name 必须指向一个真实存在、支持 TLS1.3 且证书链完整的站点,指向一个自签或过期证书的域名,主动探测一抓一个准。
验证指纹是否生效,别只看配置有没有报错。用服务端抓包看 ClientHello 的扩展顺序:
# 服务端侧抓 ClientHello,导出后用 ja3 工具比对
tcpdump -i eth0 -w /tmp/tls.pcap 'tcp port 443 and tcp[((tcp[12] & 0xf0) >> 2)] = 0x16'
tshark -r /tmp/tls.pcap -Y 'tls.handshake.type == 1' -T fields -e tls.handshake.ja3拿到的 JA3 哈希跟 chrome 的标准指纹对一下,对不上说明 utls 没真正接管握手,常见原因是编译时没带 with_utls 标签。这个坑我见过至少五次,配置写得漂漂亮亮,二进制根本没编进去。
主动探测的另一面是端口与协议的一致性。443 端口跑一个非 TLS 的明文协议,探测方一个 SYN 加几个字节就能识别。所有对外端口要么走完整 TLS,要么用 REALITY 伪装,别图省事开裸 TCP。
6.3 IP 生命周期管理与封禁检测
IP 被封不是“突然发生”,是有前兆的。生产环境要建立 IP 健康度监控,而不是等用户报障。核心指标三个:ICMP 可达性、TCP 握手成功率、TLS 握手耗时。三个指标同时劣化基本可以判定 IP 进入观察名单。
写个轻量探测脚本挂 cron,每 5 分钟跑一轮:
#!/bin/bash
# ip_health.sh - 落地 IP 健康探测
TARGETS=("hk1.example.net:443" "jp1.example.net:443" "sg1.example.net:443")
for t in "${TARGETS[@]}"; do
host="${t%%:*}"; port="${t##*:}"
# TCP 握手耗时
hs=$(tcping -c 3 -i 1 -t 3 "$host" "$port" 2>/dev/null | awk '/avg/{print $NF}')
# TLS 握手耗时
tls=$(curl -o /dev/null -s -w '%{time_appconnect}' --connect-timeout 5 \
--resolve "www.bing.com:$port:$host" "https://www.bing.com:$port/generate_204")
# ICMP 丢包
loss=$(ping -c 10 -i 0.2 -W 1 "$host" 2>/dev/null | awk -F', ' '/packet loss/{print $3}')
echo "$(date +%FT%T) $host tcp_avg=${hs}ms tls=${tls}s loss=${loss}"
done判定阈值我一般这么设:TCP 握手均值超过 300ms 或丢包超过 20% 持续两个探测周期,标记为“降级”,从 urltest 分组里摘掉;TLS 握手失败率超过 50%,直接判定“疑似封禁”,触发 IP 轮换。别等 IP 彻底不通再动,那时候用户已经骂了半小时。
IP 轮换的工程化做法是预置备用 IP 池。落地机不要只绑一个 IP,提前在云厂商控制台开好弹性 IP 或额外网卡,封禁发生时改一条 outbounds 的 server 字段加 sing-box reload 就能切。reload 走 SIGHUP:
# 平滑重载配置,不断开已有连接
kill -HUP $(pidof sing-box)
# 验证新配置生效
sing-box check -c /etc/sing-box/config.json && echo "config ok"
curl -s http://127.0.0.1:9090/proxies | jq '.proxies["hk-auto"].now'clash_api 的 /proxies 接口能实时看到当前选中的节点,配合 /connections 能看到每条连接的命中链。这两个接口是运维的眼睛,别只拿来给面板用。
6.4 灰度与误伤回滚的闭环
规则集变更的杀伤力被严重低估。改一条 domain_suffix 可能让某个 SaaS 全公司不可用,而且症状隐蔽:用户以为是对方服务挂了,排查两小时才发现是分流规则把流量送错了出口。生产环境必须把规则变更当代码变更管。
最小可行的闭环是四步:版本化、灰度、观测、回滚。
版本化:规则集源文件进 git,每次变更打 tag。sing-box 的 rule_set 远程引用带 update_interval 的,别让它自动更新到最新,锁定到具体 commit 的 raw URL:
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/<commit-sha>/geosite-cn.srs",
"update_interval": "168h",
"download_detour": "direct"
}灰度:别一次性全量推。多台落地机或多种客户端分组,先推 10% 的机器,观察 24 小时。观测指标聚焦误伤率,用 clash_api 的 /connections 拉取最近连接,按 rule 字段聚合,统计走代理的国内域名占比:
# 拉取活跃连接,统计命中规则分布
curl -s http://127.0.0.1:9090/connections | \
jq -r '.connections[] | "\(.metadata.host // .metadata.destinationIP)\t\(.rule)\t\(.chains[-1])"' | \
awk -F'\t' '{print $2}' | sort | uniq -c | sort -rn | head -20如果 geosite-cn 或 final 规则的命中量突然暴涨,说明新规则集漏匹配了本该被更精确规则命中的域名,立刻回滚。
回滚:保留上一版配置和规则集在本地,回滚命令一条:
# 回滚到上一版配置并重载
cp /etc/sing-box/config.json.bak /etc/sing-box/config.json
kill -HUP $(pidof sing-box)
sing-box check -c /etc/sing-box/config.json回滚之后别急着收工,用 tcpdump 抓一轮 DNS 和出口流量,确认误伤域名的走向恢复正常:
# 抓特定域名的 DNS 查询和后续连接
tcpdump -i any -nn 'port 53 or host <误伤域名解析出的IP>' -c 50
# 用 curl 验证实际出口
curl -v --resolve "app.example.com:443:<解析IP>" https://app.example.com/healthz 2>&1 | grep -E 'Connected|Trying'6.5 高可用不是堆节点,是砍故障域
最后说个反直觉的结论。很多团队的高可用方案是“堆节点”:十个八个落地机全挂上,觉得总有一个能用。实际跑下来故障率反而更高,因为节点越多,配置漂移越严重,某台机器的规则集版本落后、某台机器的指纹没配、某台机器的 DNS 泄漏,这些问题在节点少的时候还能人工盯,节点一多全靠运气。
真正的高可用是收敛故障域:入口 IP 不超过两个 ASN,落地 IP 按地区分组每组不超过三个,规则集版本全局统一由配置管理下发,健康检查统一由探测层负责。节点数量控制在能人工审计的范围内,比盲目扩容靠谱得多。我经手过一个案例,八台落地机因为其中一台的 geosite 库落后两周,导致该机器上的用户访问某云厂商新段全部绕路,排查花了整整一个下午。节点少一半,这个坑根本不会出现。
长期防封的本质是降低被识别的确定性:指纹一致、协议合规、IP 轮换有节奏、流量特征不突兀。规则集优化解决的是“自己别出错”,这一章解决的是“别被对面盯上”。两件事都做到位,链路才能撑过以月为单位的运营周期。
七、核心疑问与实战常见问题(FAQ)
Clash 和 sing-box 加载 Geosite/GeoIP 规则集时,内存占用差异有多大?如何优化?
Clash(尤其是 Clash.Meta)默认将 Geosite 全量加载为内存中的 domain 列表,加载 geosite:cn 和 geosite:geolocation-!cn 后常驻内存约 300–500MB;sing-box 使用 rule_set 二进制格式(.srs),按需 mmap 加载,同样规则集内存占用约 80–120MB。优化手段:Clash 侧改用 rule-providers 的 behavior: domain 并开启 lazy: true,只加载实际命中的规则集;sing-box 侧使用远程 rule_set 并设置 download_detour 走直连,同时开启 cache_file 持久化,避免每次启动重新下载。实测 sing-box 1.10 在 4GB 内存的 OpenWrt 设备上,加载 geosite-cn + geoip-cn 后 RSS 稳定在 95MB 左右。
sing-box 中 geoip 和 geosite 规则的匹配顺序如何影响分流结果?常见误伤场景有哪些?
sing-box 按规则数组顺序自上而下匹配,首条命中即终止。常见误伤:把 geoip:cn 放在 geosite:cn 之前,导致国内域名解析到海外 IP 时被误判为代理流量;正确做法是先匹配 geosite:cn 直连,再用 geoip:cn 兜底。另一个坑是 domain_suffix 规则写在 geoip 之后,导致 .cn 域名被 geoip 规则抢先命中。建议顺序:私有网络 IP → geosite:private → geosite:cn → geoip:cn → 代理。排查时用 sing-box check -c config.json 验证规则语法,再用 curl -v --resolve 指定域名测试实际出口 IP。
如何用 mtr 和 tcpdump 定位 Clash/sing-box 分流误伤导致的国内网站访问慢?
先复现问题:curl -v --connect-timeout 5 https://example.cn 观察 TLS 握手耗时和实际连接 IP。若连接 IP 为海外地址,说明分流误判。用 mtr -rwc 100 -z example.cn 查看路径中是否出现海外跳点;同时 tcpdump -i any -n host <目标IP> and port 443 -w capture.pcap 抓包,在 Wireshark 中检查 SNI 与目标 IP 是否匹配。常见根因:Geosite 规则集版本过旧导致新域名未收录,或 Clash 的 fallback 组健康检查超时触发切换。修复后对比 tcping 延迟:误伤时国内站点 RTT 常从 30ms 升至 180ms 以上,修正规则后应回落至 50ms 以内。
Clash 的 rule-providers 远程规则集如何配置缓存和更新策略以避免性能抖动?
推荐配置:interval: 86400(24 小时更新一次),lazy: true 开启懒加载,behavior 按规则类型选择 domain 或 ipcidr,format 用 yaml 或 text。关键参数是 proxy: DIRECT 让规则集下载走直连,避免更新时占用代理带宽。若使用 Clash.Meta,可加 health-check: false 关闭规则集健康检查以减少 CPU 开销。实测 5000 条 domain 规则集在 lazy 模式下首次匹配延迟增加约 2ms,但内存下降 60%。更新策略建议错峰:不同规则集设置不同 interval(如 43200 和 86400),避免同时下载造成瞬时 CPU 峰值。