背景

前面两篇记录了怎么用 iWAN 隧道 + 自建 VPS/L2TP 做跨境出口。那两篇解决的是”整条账号的所有流量走哪个出口”,但实际用下来需求更细:YouTube 这类大流量留在默认出口,Claude/ChatGPT 这类走专门的美国出口,Google 其它服务和默认出口一致——这就不是”按账号分流”能解决的了,得精确到”按域名分流”。

这篇记录这个过程踩的坑,比预想的深得多:从一个看起来人畜无害的 Fake-IP 怪现象,一路挖到 DoH 兼容性、陈年连接跟踪记录,最后还撞上了 IPv6 双栈这个完全没预料到的硬骨头。

先排除一个弯路:/routing rule 不支持 address-list

RouterOS v7 做策略路由,直觉会想用 /routing rule,配合之前聊过的 DNS 自动填充地址组的技巧(/ip dns static type=FWD address-list=),指望能这样写:

1
/routing rule add dst-address-list=via-iwan action=lookup table=via-iwan

这条命令实际上跑不通——/routing rule 只支持 src-address/dst-address(单个 CIDR)/interface/routing-mark 这几个纯值匹配字段,压根没有 dst-address-list 这个参数,没法直接引用一个动态维护的地址组。

真正能把”目标地址命中某个 address-list”跟策略路由接起来的,是另一套完全独立的机制——/ip firewall mangle 打标记:

1
2
3
4
5
6
/routing table add name=via-iwan fib

/ip firewall mangle add chain=prerouting dst-address-list=via-iwan \
action=mark-routing new-routing-mark=via-iwan passthrough=yes

/ip route add dst-address=0.0.0.0/0 gateway=<隧道容器veth IP> routing-table=via-iwan

/routing rule 和 mangle mark-routing 是两条并行的策略路由路径,各有各的匹配能力,不能互相替代。

开源规则导入:此路不通

想着能不能直接拿现成的域名分流规则包(类似 GFWList、Clash 规则集)导进设备省事,查下来是不行的——网关的 DPI 应用识别库是厂商自己闭源维护的信号库,没有开放格式能塞第三方规则;倒是 IP 地址组支持批量文本导入(格式是IP 空格 备注,ANSI 编码的 .conf 文件),但这只能导 IP,不能导域名,而且公开的 IP 段这种东西本身就不稳定(尤其是走 CDN 的服务),不如域名动态解析可靠。

最后还是回到”域名动态解析 → 自动维护地址组”这条路上,找一个最简单的案例练手。

Telegram:最好的练手案例

Telegram 官方自己维护了一份固定的 IP 段清单(core.telegram.org/resources/cidr.txt),不用猜、不用等 DNS,直接拿这份清单配置静态路由做验证,用来确认”策略路由分流”这个机制本身是通的:

1
2
/ip route add dst-address=91.108.4.0/22 gateway=<隧道容器veth IP>
... (其余几段同理)

这个走通之后,才开始上真正麻烦的部分——基于 DNS 动态解析的域名分流。

麻烦的开始:10 秒 TTL 和一场 Fake-IP 迷案

拿两个域名练手测试动态域名分流机制,其中一个域名解析的 TTL 只有 10 秒,条目加进地址组几乎是一闪而过,一开始纠结的是”怎么在这么短的窗口里确认机制生效了”,结果测出来的现象比这个问题本身诡异得多。

第一个误区:用 /resolve 触发验证,压根不算数

最初的想法是用路由器自己的 /resolve 命令手动触发一次解析,紧接着查地址组,图个不用跟 TTL 赛跑。这个思路从根上就是错的——MikroTik 官方说得很清楚:只有局域网设备自己发出的查询才会触发地址组写入,路由器自己用 /resolve 查的不算。地址组填充这个副作用,绑定的是”路由器代答一次查询”这个动作,不是”路由器自己解析成功”这件事。

改成用局域网里真实的客户端设备去查,才算是走对了触发路径。

诡异的现象:查出来的 IP 像是假的

换成正确的触发方式后,解析结果却出现了两个长得很像、数值连号的地址,刚好落在 198.18.0.0/15 这个段里——这是 RFC 2544 为网络测试保留的网段,但更出名的身份是 Clash/Mihomo/sing-box 这类代理软件 Fake-IP 模式的默认地址池。两个完全不同的域名解析出两个几乎挨着的占位 IP,是 Fake-IP “第一次见到某个域名就按顺序发一个池里占位 IP”的典型特征。

一度怀疑是查询被导向了某个开着 Fake-IP 的本地代理服务,但排查发现查询目标确实是路由器自己的 DNS 代理地址,不是走错了地方。真正的根因是:这两条静态 DNS 条目的 forward-to 填的不是字面 IP,而是一个引用了 DoH(DNS over HTTPS) 服务商预设的命名条目。MikroTik 官方文档明确写着:

“Currently DoH is not compatible with FWD type static entries”

FWD 类型遇到 DoH 转发目标直接不兼容,这是官方承认的架构限制(DoH 要求全程证书校验,转发给一个”不走 DoH 加密”的流程被认为破坏了 DoH 的安全模型),不是 bug,也没有 workaround,只能把 forward-to 换成普通的字面 IP(8.8.8.8/223.5.5.5 这类),彻底不碰 DoH。

修完一个,另一个还是老样子

把两个测试域名的 forward-to 都改成字面 IP、清了缓存,其中一个域名立刻解析出了真实 IP,另一个却纹丝不动,还是原来那个 Fake-IP 风格的地址,连数值都一模一样。两条配置状态对称、修法一致,结果却一个好一个不好,说明问题不是配置本身。

查连接跟踪表才找到真相:里面躺着一条源地址是路由器普通 WAN 口的陈年 UDP 连接记录——这是在加”让这个查询走隧道”那条路由之前遗留下来的旧查询,一直没过期,卡住了后续查询继续沿用修复之前的旧路径。UDP 虽然连接跟踪比 TCP 松散,但已确认的记录还是会影响同一个五元组的后续决策。手动清掉这条记录,问题才算真正解决:

1
/ip firewall connection remove [find dst-address~"8.8.8.8"]

顺带验证了一次完整架构

排查过程里跑了一次 /tool traceroute 8.8.8.8,发现第一跳直接进了 iWAN 隧道容器,第二跳是 VPS 上 L2TP 服务端的地址,再往后几跳已经是 Google 自己的网段——相当于把”iWAN隧道 → 境外网关 → L2TP → VPS → 真实互联网”这一整条此前搭建的链路,在排查这个小问题的过程中意外地端到端验证了一遍。

三路分流落地

机制理顺之后,实际配置反而是最简单的部分:Google 系服务和 YouTube 归到走默认 iWAN 账号的地址组,Claude/ChatGPT 归到走专门美国出口账号的地址组,各自配一遍”域名 FWD + 地址组 + mangle打标记 + 自定义路由表”:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Google(含YouTube) → 默认账号
/ip dns static add name=google.com type=FWD forward-to=8.8.8.8 address-list=via-iwan match-subdomain=yes
/ip dns static add name=googleapis.com type=FWD forward-to=8.8.8.8 address-list=via-iwan match-subdomain=yes
/ip dns static add name=gstatic.com type=FWD forward-to=8.8.8.8 address-list=via-iwan match-subdomain=yes
/ip dns static add name=youtube.com type=FWD forward-to=8.8.8.8 address-list=via-iwan match-subdomain=yes
/ip dns static add name=googlevideo.com type=FWD forward-to=8.8.8.8 address-list=via-iwan match-subdomain=yes
... (其余同理)

# Claude/GPT → 美国专线账号
/ip dns static add name=claude.ai type=FWD forward-to=8.8.8.8 address-list=via-iwan-usa match-subdomain=yes
/ip dns static add name=openai.com type=FWD forward-to=8.8.8.8 address-list=via-iwan-usa match-subdomain=yes
/ip dns static add name=chatgpt.com type=FWD forward-to=8.8.8.8 address-list=via-iwan-usa match-subdomain=yes
... (其余同理)

googlevideo.com 这个域名特别容易漏:youtube.com 本身只是网页壳子,真正的视频流走的是这个单独的 CDN 域名,漏配的典型症状是”网页能打开,视频一直转圈”。这类”主站域名 + 单独媒体/API CDN 域名”的组合在 Twitter(twimg.com)、Instagram(cdninstagram.com)、GitHub(githubusercontent.com) 上也是一样的坑,配置的时候得想到主站之外还有没有单独的资源域名。

最后一个意外:IPv6 把整套方案绕过去了

以为大功告成,结果发现只要客户端走 IPv6 访问,这一整套分流直接失效——域名分流配置看起来生效了,访问却还是走的默认路径,像是完全没配过一样。

根子在于:RouterOS 把 IPv4 和 IPv6 当成两套完全独立的子系统,/ip firewall address-list//ip firewall mangle//ip route 全都只管 IPv4,IPv6 有另一套平行的 /ipv6 ... 命名空间。前面搭的那一整套规则,一个字节都没碰过 IPv6 流量——如果客户端解析到了目标域名的 IPv6 地址、系统又优先选了 IPv6 直连(Happy Eyeballs 的正常行为),这个连接完全不会经过任何一条配置过的规则,直接从没人管的默认 IPv6 出口飞出去了。

第一反应是”那把 IPv6 配平行的一套不就行了”,但卡在了两个硬约束上:

  1. 隧道本身大概率就不支持 IPv6——SD-WAN 隧道协议从头到尾分配的都是 IPv4 地址池,容器的网卡虽然确实有 IPv6 地址,但那是局域网侧自动分配给容器自己用来管理的,跟”隧道能不能背 IPv6 流量”完全是两码事。真要让 IPv6 流量钻进这条隧道,得另外搭一层 NAT64 转换(比如拿 Tayga 这类工具再起一个容器),为了几个域名的分流上这么重的基建不划算。
  2. 不能大而化之地关掉局域网的 IPv6——这条路直接堵死,逼着必须找一个”只影响这几个域名、不影响局域网其它一切”的精细方案。

RouterOS 的静态 DNS 没有一个干净的”只抑制 AAAA、保留 A”的记录类型——type=NXDOMAIN 会把这个域名所有记录类型都干掉,不是想要的效果。

最后想明白的思路是:不去动 DNS 解析这一层,AAAA 照常解析,在 IPv6 防火墙层直接拒绝连去这些目标地址的连接,逼客户端的 Happy Eyeballs 机制自动回落到 IPv4,回落之后自然就被已经跑通的 IPv4 那条流水线接管了:

1
2
3
4
5
/ipv6 firewall filter add chain=forward dst-address-list=via-iwan \
action=reject reject-with=icmp-no-route

/ipv6 firewall filter add chain=forward dst-address-list=via-iwan-usa \
action=reject reject-with=icmp-no-route

这里有个事先没想到、后来查文档确认的关键点:MikroTik 官方文档写明 address-list 这个参数在静态 DNS 的 A 和 AAAA 记录里都可用——也就是说前面为 IPv4 分流配的那批 FWD 条目,其实早就在往 /ipv6 firewall address-list 的同名列表里自动写入 AAAA 解析出来的 IPv6 地址了,完全不需要再额外配一遍 DNS 这一层,只要在 IPv6 防火墙上补一条引用同名地址组的拒绝规则就行。

用 reject 而不是 drop 是关键细节:reject 立刻告诉客户端”这条路不可达”,操作系统马上改用 IPv4 重试,几乎没有感知延迟;换成静默 drop,客户端要等自己的连接超时才会重试,体验上会有明显卡顿。

小结

这次最深的一个教训是:同一份排查里,表面看起来像同一个问题的两个现象,根因可能完全不相关——两个域名都卡在 Fake-IP,一个是 DoH 不兼容,另一个是陈年连接跟踪记录,中间一度怀疑过”国内外 DNS 谁更干净”这种完全跑偏的方向,靠 /tool traceroute 这种最朴素的工具才把路径问题和配置问题彻底分开。

另一个教训是:域名分流这件事默认只覆盖 IPv4,这不是哪个产品的缺陷,是 IPv4/IPv6 本来就是两套独立的协议栈——做策略路由、做防火墙、做 NAT,但凡涉及”按网络层处理流量”的配置,都得想一遍”IPv6 这边要不要配对应的一份”,尤其是隧道/VPN 类产品很大一部分压根不支持 IPv6,一旦客户端双栈优先走了 v6,整套精心设计的分流规则会被完整绕过而不会有任何报错提示——这种”配置生效但现象上完全看不出来”的坑,比报错型的 bug 更难发现。