← 返回首页

规则一条没写错,但一条都没命中

2026-10-14 · 11 分钟阅读

家里那套代理跑了几天,能上网,速度也正常。直到我去翻 mihomo 的连接列表,发现一件怪事:

192.168.20.1 -> 173.194.43.139 | rule: Match  | chains: ['一元🇯🇵日本 10', '兜底']
192.168.20.1 -> 157.240.7.20   | rule: Match  | chains: ['一元🇯🇵日本 07', '兜底']
192.168.20.1 -> 4.145.79.80    | rule: Match  | chains: ['北美俄勒冈 1000E#S3', '兜底']

rule: Match 是空的意思——所有规则都没匹配上,全部落到最后那条兜底。

可配置文件里明明写着 GEOSITE,youtube,流媒体、GEOSITE,google,Google、GEOSITE,microsoft,直连……几十条精确规则,一条都没生效。

诡异的是:它没报错,也没断网。 兜底组里也是真节点,YouTube 照样能开。所以如果你只看"能不能上网",这个故障可以潜伏很久。

先搞清楚流量是怎么来的

我家的拓扑是这样:终端网关指向 iKuai(.1),iKuai 上配了一条端口分流的"下一跳网关"规则——凡是目的地落在「代理IP」这个 IP 分组里的包,下一跳统统丢给 192.168.20.21,也就是跑 mihomo 的那台容器。

终端(网关=.1)
  → iKuai 端口分流:目的 ∈「代理IP」分组 → 下一跳 192.168.20.21
  → mihomo TUN
  → 代理出口

这里有个关键点:iKuai 是"按 IP"做这个决定的。 它看的是目的 IP 在不在那个有四万多个网段的 IP 分组里,压根不知道域名是什么。

根因:包里没有域名

所以当包到达 mihomo 的 TUN 网卡时,它是一个纯粹的 IP 包,没有域名。

而 mihomo 的规则引擎是按域名匹配的。GEOSITE,youtube 要判断的是"这个连接要去 youtube.com 吗"——可它手上只有 173.194.43.139。

它能做的只剩两条路:

于是那几十条精心写下的域名规则,全部形同虚设。

对照组很能说明问题。同一时刻,另一台直接用 mihomo 做 DNS的机器(.13)连接长这样:

192.168.20.13 -> api.github.com     | rule: GeoSite github    | chains: ['us-vless65503', '美国节点', 'Github']
192.168.20.13 -> api.telegram.org   | rule: GeoSite telegram  | chains: ['链-HK-8443', '香港节点', 'Telegram']
192.168.20.13 -> api.themoviedb.org | rule: GeoSite geolocation-!cn | chains: ['一元🇯🇵日本 10', '兜底']

域名清清楚楚,规则精确命中。差别就在于:这台机器的 DNS 走 mihomo,mihomo 知道它要访问谁。

我一开始想错了方向

第一反应是"那就让 mihomo 能拿到域名"——把终端 DNS 指到 mihomo,或者让 iKuai 改用域名分流。

这两条路都不好走:

两条路都是在"让上游多干活",而我忽略了一件事:域名其实就在包里。

域名一直都在,只是没人去读

HTTPS 握手的时候,客户端会在 TLS ClientHello 里带上 SNI——那是明文的,写着它要访问哪个域名。HTTP 请求里有 Host 头,QUIC 也同理。

也就是说,虽然 IP 包本身不含域名,但包里的载荷含。

mihomo 有个功能专门干这个:sniffer(流量嗅探)。开启后它会把 SNI / Host 抠出来,还原成域名连接,重新走一遍规则匹配。

我的配置里完全没有这一段。这就是漏掉的那一环。

配置

sniffer:
  enable: true
  force-dns-mapping: true
  parse-pure-ip: true
  override-destination: true          # ← 关键
  sniff:
    HTTP:
      ports: [80, 8080-8880]
      override-destination: true
    TLS:
      ports: [443, 8443]
    QUIC:
      ports: [443, 8443]
  skip-domain:
    - "Mijia Cloud"
    - "+.push.apple.com"

几个字段值得说清楚:

验证

改配置前先备份、先校验语法(mihomo 有 -t 参数,不会真启动):

$ cp config.yaml config.yaml.bak-sniffer-$(date +%Y%m%d%H%M%S)

$ docker run --rm -v /opt/docker/mihomo/config.yaml:/cfg.yaml:ro \
    metacubex/mihomo:latest -t -f /cfg.yaml
...
configuration file /cfg.yaml test is successful

然后重启容器。等连接重新积累起来,再看同一张表:

192.168.20.1 -> s.youtube.com                          | rule: GeoSite youtube  | chains: ['链-HK-hy2', '香港节点', '流媒体']
192.168.20.1 -> rr3---sn-a5meknd6.googlevideo.com      | rule: GeoSite youtube  | chains: ['链-HK-hy2', '香港节点', '流媒体']
192.168.20.1 -> client.wns.windows.com                 | rule: GeoSite microsoft| chains: ['直连']

改之前全是 Match,改之后精确命中了。

值得注意的是出口的变化:YouTube 之前走的是兜底组里随机挑的日本节点,现在稳定走 流媒体 组 → 香港节点;微软的流量回到了直连。这才是配置里本来想表达的意思。

一个必须说清的限制

嗅探不是万能的。没有 SNI 的连接嗅探不出来——比如基于纯 IP 的私有协议、部分 Apple 推送(17.57.145.x)、一些 Tailscale DERP 连接。

这些连接会继续落到 Match / 兜底。这不是配置错了,是协议本身就没带域名。

所以正确的预期是:绝大多数 HTTPS 流量(也就是日常浏览的绝大部分)能精确分流,纯 IP 的协议仍走兜底。 想连这些也精确分流,只能靠 IP 库,那是另一个方向了。

教训

「能上网」不等于「规则在生效」。
域名规则在纯 IP 转发的链路上会静默失效——不报错、不断网、只是全部退化到兜底。只有去看连接列表里的 rule 字段才能发现。

更一般的经验:把决策点放在哪一层,决定了那一层能拿到多少信息。 iKuai 按 IP 分流是它能力的边界,它拿不到域名是正常的;但不能因此假定下游也拿不到——包里其实有,只是需要一个显式的开关去读它。

排查顺序也值得记一下:先看连接列表的 rule 字段,再看规则文件。 规则写错和规则没被使用,是两种完全不同的故障,而 rule: Match 这个信号一秒就能区分它们。

mihomoClash MetaTUNsnifferiKuai踩坑