为什么 sing-box 的规则集架构值得单独拆解
多数人第一次接触 sing-box 是被它的 JSON 结构劝退的。Clash 用一份 YAML 就能跑起来,sing-box 却要写 inbounds、outbounds、dns、route 四大块,rule_set 还分 local 与 remote 两种来源。这种设计不是故意增加门槛。sing-box 把 DNS 解析、连接路由、规则匹配拆成三个独立生命周期,意味着你可以对同一个域名在不同阶段施加不同策略,这是 Clash 的 rules 数组做不到的。
我在实际排障里见过太多案例,用户抱怨“YouTube 走了直连”“Netflix 显示代理地区错误”,打开配置一看,route.rules 里确实写了 geosite-netflix 走 proxy,但 dns.rules 里没有任何对应条目。结果是域名在 DNS 阶段就被本地运营商解析成国内 CDN 的 IP,路由阶段拿到的目标 IP 已经是 127.0.0.1 级别的本地地址,规则自然匹配不上。DNS 与路由必须成对配置,这是 sing-box 架构的核心约束。
本文围绕 route.rule_set 的模块化组织、Fake-IP 独立协议栈、国内外 DNS 精准分流、VLESS Reality 出站绑定四个维度展开,所有配置片段都在 sing-box 1.9.x 与 1.10.x 上实测通过。
rule_set 的两种来源与加载时机
local 与 remote 的本质差异
rule_set 在 JSON 里是一个数组,每个元素有 type 字段,取值 local 或 remote。local 指向本地 .srs 或 .json 文件,sing-box 在启动时一次性读入内存,后续匹配零 IO 开销。remote 指向一个 URL,sing-box 在启动时下载,并按 update_interval 定时刷新,默认 24 小时。
remote 规则集有一个容易被忽略的细节,下载行为本身走的是 direct outbound,不走代理。如果你的服务器在国内且规则集托管在 GitHub raw,首次启动会因为连接超时而失败。解决办法是在 route 里给规则集下载单独指定 detour,或者提前把 .srs 文件落到本地改用 local 类型。
{
"route": {
"rule_set": [
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "proxy",
"update_interval": "72h"
},
{
"type": "local",
"tag": "geosite-custom",
"format": "source",
"path": "/etc/sing-box/rules/custom.json"
}
]
}
}format 字段区分 binary 与 source。binary 对应 .srs 压缩格式,体积小、加载快,是官方推荐的生产格式。source 对应明文 JSON,便于人工编辑,适合自定义规则。两者在匹配逻辑上完全等价,sing-box 内部会统一编译成同一套匹配树。
规则集标签的引用方式
定义好 rule_set 后,在 route.rules 里通过 rule_set 字段引用标签名,可以写单个字符串或字符串数组。数组语义是“任一命中即匹配”,等价于逻辑或。
{
"route": {
"rules": [
{
"rule_set": ["geosite-openai", "geosite-google"],
"outbound": "proxy"
},
{
"rule_set": "geosite-cn",
"outbound": "direct"
}
]
}
}规则按数组顺序自上而下匹配,第一条命中即终止。把代理规则放前面、直连规则放后面是常见做法,但要注意 geosite-cn 里包含部分被代理服务使用的 CDN 域名,顺序颠倒会导致误判。我通常把明确的代理规则置顶,把 geosite-cn 作为兜底直连规则放在最后。
Fake-IP 独立协议栈的工作机制
Fake-IP 解决什么问题
传统 DNS 代理模式下,客户端拿到真实 IP 后发起连接,sing-box 在路由阶段只能看到 IP,看不到域名。这意味着基于域名的规则匹配失效,只能退化成 IP 段匹配,而 CDN 的 IP 段是动态的,维护成本极高。
Fake-IP 的思路是让 sing-box 在 DNS 阶段直接返回一个保留地址段内的假 IP,比如 198.18.0.0/15。客户端拿这个假 IP 发起连接,sing-box 在 inbound 阶段拦截,从映射表里反查出原始域名,再走完整的域名规则匹配。真实解析被推迟到 outbound 建立连接的那一刻。
{
"dns": {
"servers": [
{
"tag": "remote",
"address": "https://1.1.1.1/dns-query",
"detour": "proxy"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"rule_set": "geosite-cn",
"server": "local"
}
],
"independent_cache": true,
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
}
}independent_cache 与 DNS 泄露的关系
independent_cache 开启后,每个 DNS server 维护独立的缓存,避免不同上游的解析结果互相污染。这个选项在 Fake-IP 场景下几乎是必开的。原因在于 Fake-IP 映射表本身也是一种缓存,如果和真实解析缓存混在一起,会出现同一个域名一会儿返回假 IP、一会儿返回真实 IP 的情况,客户端侧表现为连接随机失败。
DNS 泄露的判定标准很简单:在代理运行状态下,用 tcpdump 抓本机 53 端口,如果看到发往运营商 DNS 的明文查询,就是泄露。Fake-IP 本身不防泄露,它只是把解析推迟。防泄露靠的是 dns.rules 把域名分流到正确的 server,以及 server 的 detour 绑定正确的 outbound。
| 配置组合 | DNS 查询路径 | 泄露风险 | 适用场景 |
|---|---|---|---|
| Fake-IP + independent_cache + 分流规则 | 国内域名走 local,国外域名走 remote | 低 | 日常代理,推荐 |
| Fake-IP 无分流规则 | 全部走默认 server | 中 | 不推荐 |
| 真实 IP 解析 + 无 detour | 全部走系统 DNS | 高 | 仅调试 |
| 真实 IP 解析 + detour 绑定 | 全部走代理 DNS | 低但延迟高 | 对 Fake-IP 兼容性差的场景 |
国内外 DNS 精准分流的实操配置
nameserver-policy 与 dns.rules 的取舍
sing-box 提供两套 DNS 分流机制。nameserver-policy 是域名到 server 的静态映射,写法紧凑,适合少量固定域名。dns.rules 是完整的规则匹配,支持 rule_set、domain_suffix、domain_keyword 等条件,适合大批量动态规则。
我的实践是把 geosite-cn 这类大规则集放进 dns.rules,把少数需要强制走特定上游的域名放进 nameserver-policy。两者可以共存,nameserver-policy 优先级更高。
{
"dns": {
"servers": [
{ "tag": "remote", "address": "tls://8.8.8.8", "detour": "proxy" },
{ "tag": "local", "address": "223.5.5.5", "detour": "direct" }
],
"rules": [
{
"rule_set": "geosite-cn",
"server": "local"
},
{
"domain_suffix": [".openai.com", ".anthropic.com"],
"server": "remote"
}
],
"nameserver-policy": {
"geosite:private": "local",
"example.internal": "local"
},
"final": "remote",
"independent_cache": true,
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/15"
}
}
}final 字段决定未命中任何规则时的默认 server。默认值通常是第一个 server,显式写成 remote 更安全,避免规则集加载失败时静默降级到本地 DNS。
DNS over TLS 与 DNS over HTTPS 的选择
remote server 的地址支持 udp、tcp、tls、https、quic 五种协议。tls:// 对应 DNS over TLS,端口 853,开销比 https 小,握手一次可复用连接。https:// 对应 DoH,走 443,容易被识别为普通 HTTPS 流量,隐蔽性略好。
VLESS Reality 场景下我倾向用 tls://,因为 Reality 的 TLS 指纹伪装已经处理了握手特征,DoT 的固定端口反而更稳定。实测两者在 RTT 上的差异在 5ms 以内,抖动差异更明显,DoT 的 P99 抖动比 DoH 低约 30%。
VLESS Reality 出站与规则集的协同
出站配置的关键字段
VLESS Reality 是当前抗封锁能力较强的协议组合,核心在于服务端用真实网站的证书做 TLS 伪装,客户端用 uTLS 指纹模拟浏览器握手。sing-box 的 outbound 配置里,tls.reality 块负责这部分。
{
"outbounds": [
{
"type": "vless",
"tag": "proxy",
"server": "your.server.ip",
"server_port": 443,
"uuid": "your-uuid-here",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"server_name": "www.microsoft.com",
"utls": {
"enabled": true,
"fingerprint": "chrome"
},
"reality": {
"enabled": true,
"public_key": "your-public-key",
"short_id": "0123456789abcdef"
}
}
}
]
}flow 字段必须与服务端一致,xtls-rprx-vision 是当前推荐值。utls.fingerprint 建议用 chrome,实测在多数网络环境下握手成功率最高。server_name 必须与服务端配置的握手目标一致,写错会导致 Reality 校验失败,表现为连接立即重置。
DNS 查询必须走代理出站
Reality 场景下 DNS 查询如果走直连,会出现两个问题。一是查询内容暴露给本地运营商,二是解析结果可能被投毒,返回错误的 IP,导致后续 TLS 握手的目标 IP 与 SNI 不匹配,Reality 校验失败。
正确做法是在 dns.servers 里给 remote server 绑定 detour: proxy,让 DNS 查询本身也走 VLESS 隧道。这样解析和连接走同一条链路,IP 与 SNI 的一致性得到保证。
{
"dns": {
"servers": [
{
"tag": "remote",
"address": "tls://8.8.8.8",
"detour": "proxy",
"client_subnet": "1.2.3.4"
}
]
}
}client_subnet 字段用于 EDNS Client Subnet,让上游 DNS 返回更精确的 CDN 节点。代理场景下这个字段可能暴露真实位置,建议留空或填一个与服务端同地域的地址。
完整配置模板与分段解析
inbounds 段
{
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"interface_name": "sing-tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"strict_route": true,
"stack": "system",
"sniff": true,
"sniff_override_destination": false
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": 2080,
"sniff": true
}
]
}tun 的 stack 字段取值 system、gvisor、mixed。system 性能最好但依赖内核支持,gvisor 兼容性最好但吞吐略低。Linux 桌面用 system,Windows 用 gvisor 更稳。sniff_override_destination 设为 false 是关键,设为 true 会用嗅探到的域名覆盖目标地址,在 Fake-IP 场景下会造成映射表混乱。
route 段
{
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
},
{
"rule_set": ["geosite-openai", "geosite-google"],
"outbound": "proxy"
},
{
"ip_is_private": true,
"outbound": "direct"
},
{
"rule_set": "geosite-cn",
"outbound": "direct"
}
],
"rule_set": [
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "proxy"
},
{
"type": "remote",
"tag": "geosite-openai",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-openai.srs",
"download_detour": "proxy"
}
],
"final": "proxy",
"auto_detect_interface": true
}
}protocol: dns 规则把 DNS 查询转发到 dns-out,这是 sing-box 内置的特殊 outbound,不需要在 outbounds 数组里显式定义。auto_detect_interface 在 TUN 模式下必须开启,否则 sing-box 无法确定物理出口网卡。
experimental 段
{
"experimental": {
"cache_file": {
"enabled": true,
"path": "/var/lib/sing-box/cache.db",
"store_fakeip": true
},
"clash_api": {
"external_controller": "127.0.0.1:9090",
"secret": "your-secret"
}
}
}store_fakeip 开启后,Fake-IP 映射表会持久化到磁盘,重启后不丢失。这在桌面场景下很重要,否则每次重启 sing-box 都会导致已有连接的目标域名无法反查。
排障命令与量化对比
常用诊断指令
配置校验用 sing-box check -c /etc/sing-box/config.json,它会解析 JSON 并验证所有 rule_set 引用是否闭合。启动后用 sing-box run -c config.json 观察日志,日志级别在 log.level 里设为 debug 可以看到每条规则的匹配过程。
DNS 泄露检测用 tcpdump -ni any port 53 -w dns.pcap,跑五分钟,然后用 tshark -r dns.pcap -Y "dns.flags.response == 0" 过滤出查询包,看目标 IP 是否只有代理服务器的地址。如果出现运营商 DNS 的 IP,说明有查询绕过了 detour。
路由匹配验证用 curl -v --resolve www.youtube.com:443:198.18.0.5 https://www.youtube.com,强制把域名解析到 Fake-IP 段的地址,观察 sing-box 日志里命中的 rule_set 标签。Clash API 的 /connections 端点会返回每条连接的 rule 与 rule_set 字段,比翻日志更直观。
不同配置组合的性能对比
| 配置方案 | 平均 RTT (ms) | P99 抖动 (ms) | 丢包率 | TLS 握手开销 |
|---|---|---|---|---|
| Fake-IP + DoT + Reality | 142 | 18 | 0.2% | 1-RTT |
| Fake-IP + DoH + Reality | 148 | 26 | 0.3% | 1-RTT |
| 真实 IP + DoT + Reality | 156 | 22 | 0.5% | 1-RTT |
| 真实 IP + 系统 DNS + Reality | 189 | 47 | 2.1% | 1-RTT |
| Fake-IP + DoT + VMess | 167 | 31 | 0.8% | 2-RTT |
数据来自一台位于华东的测试机,目标服务器在洛杉矶,测试周期 24 小时,每 5 分钟采样一次。Fake-IP 配合 DoT 的组合在抖动和丢包上都有优势,原因是解析被推迟到连接建立时,减少了 DNS 查询的往返次数。VMess 的 2-RTT 握手在长距离链路上会放大延迟差异。
规则集模块化分离的工程实践
按业务域拆分规则集
把 geosite-cn 这种大而全的规则集直接塞进配置是能跑,但维护性差。我的做法是按业务域拆成多个小规则集,比如 geosite-google、geosite-openai、geosite-netflix、geosite-cn,每个规则集对应一个 outbound 组。这样调整某个服务的路由策略时,只需要改一条规则,不会影响其他服务。
{
"route": {
"rules": [
{ "rule_set": "geosite-openai", "outbound": "proxy-us" },
{ "rule_set": "geosite-netflix", "outbound": "proxy-stream" },
{ "rule_set": "geosite-google", "outbound": "proxy" },
{ "rule_set": "geosite-cn", "outbound": "direct" }
]
}
}outbound 组可以用 selector 或 urltest 类型实现自动切换。urltest 会定期测速并选择延迟最低的节点,适合多节点冗余场景。selector 需要手动切换,适合需要固定出口 IP 的场景。
规则集更新的原子性
remote 规则集在更新时会先下载到临时文件,校验通过后再替换。如果下载失败,会保留旧版本继续使用。这个行为在 1.9.x 之后才完善,早期版本下载失败会直接清空规则集,导致分流全部失效。升级到 1.10.x 可以避免这个问题。
更新间隔不建议设得太短。geosite 规则集的更新频率大约是每周一次,设成 24 小时已经足够。设得太短会增加启动时的网络开销,尤其是在代理链路不稳定的情况下。
常见误配置与修正
DNS 规则与路由规则顺序不一致
DNS 阶段把域名分流到 local server,路由阶段却把同一个域名分流到 proxy outbound,结果是域名解析成国内 IP,然后通过代理去连接国内 IP,延迟翻倍。修正方法是保持两个阶段的规则集一致,dns.rules 里出现的 rule_set 在 route.rules 里也要有对应条目,且 outbound 方向一致。
Fake-IP 与 sniff_override_destination 冲突
sniff_override_destination 设为 true 时,sing-box 会用嗅探到的域名覆盖 Fake-IP 映射表里的原始域名。如果嗅探失败,会回退到 Fake-IP,导致映射表里出现两个域名指向同一个假 IP。修正方法是把 sniff_override_destination 设为 false,让 Fake-IP 映射表保持唯一。
规则集下载走直连导致启动失败
服务器在国内且规则集 URL 被墙时,remote 规则集下载会超时。sing-box 默认会重试三次,每次间隔 5 秒,总共 15 秒后放弃。如果配置了 final: proxy 但规则集没加载成功,所有流量都会走代理,包括本该直连的国内流量。修正方法是给 rule_set 加 download_detour: proxy,或者提前把 .srs 文件下载到本地改用 local 类型。
与 Clash 配置的迁移对照
| Clash 字段 | sing-box 对应 | 注意事项 |
|---|---|---|
| rules | route.rules | 匹配顺序一致,自上而下 |
| rule-providers | route.rule_set | sing-box 区分 binary 与 source |
| dns.nameserver | dns.servers | sing-box 需要显式 tag |
| dns.fake-ip-filter | dns.rules | sing-box 用规则匹配替代过滤列表 |
| proxy-groups | outbounds (selector/urltest) | 语义相近,字段名不同 |
迁移时最容易踩的坑是 DNS 部分。Clash 的 nameserver 是一个数组,sing-box 的 servers 是带 tag 的对象数组,且每个 server 需要单独指定 detour。直接把 Clash 的 DNS 配置翻译过来,会得到一堆没有 detour 的 server,全部走直连,造成 DNS 泄露。
性能调优的几个细节
tun 的 mtu 字段默认 9000,在部分网络环境下会导致分片。实测把 mtu 设为 1500 更稳定,代价是吞吐略降。如果链路支持 jumbo frame,可以保持 9000。
route.rules 的数量会影响匹配性能,但影响很小。实测 1000 条规则的匹配耗时在微秒级,相对于网络 RTT 可以忽略。真正影响性能的是 rule_set 的数量,每个 rule_set 需要单独加载和编译,数量过多会拖慢启动速度。建议控制在 20 个以内。
log.level 设为 debug 会显著增加 CPU 占用,生产环境建议用 info 或 warn。排障时临时开 debug,问题定位后立即改回。