这篇是一次真实排查过程的整理:
场景是用 MikroTik RouterOS 做双线/多 WAN 策略路由,本机需要访问某个特定 IP 走指定 WAN。
看起来很简单的一件事,结果却被 conntrack / mangle / output 链联合作死了一回。
这里把「问题现象 → 原因分析 → 排查过程 → 最终解决方案和建议」完整记录下来,方便以后自己和别人少踩坑。
一、问题背景
需求很简单:
让路由器本机访问某个外部 IP(例如
47.109.74.244)时,强制走cell这个 WAN(比如 4G/5G 蜂窝)。
常规做法是:
- 在
/ip firewall mangle里给这类连接打connection-mark; - 再根据
connection-mark给数据包打routing-mark; - 再在
/ip route里根据routing-mark指向对应 WAN。
示意配置(简化版):
# 1. 匹配去 47.109.74.244 的连接,打连接标记
/ip firewall mangle
add chain=output dst-address=47.109.74.244 \
action=mark-connection new-connection-mark=cell-wan-mark passthrough=yes
# 2. 根据连接标记打路由标记
add chain=output connection-mark=cell-wan-mark \
action=mark-routing new-routing-mark=cell passthrough=no
看上去没问题:
路由器本机访问 47.109.74.244 → 连接被标记 → 数据包打上 routing-mark=cell → 走目标路由表即可。
二、异常现象
然而实际运行时,发现了很诡异的情况:
- 明明只想让去
47.109.74.244 的流量走cell; - 结果 路由器本机通过 WireGuard 出去的流量(endpoint 在
192.168.1.2上)也被打上了routing-mark=cell; - 换句话说:与
47.109.74.244 完全无关的流量,被强行扯到cell上走了。
直观感受就是:
有些连接突然不走原本预期的 WAN 了(比如原本应该走光纤出口),而是绕路从 cell 出去了。
更让人困惑的是:
在 mangle 里明明写的是 dst-address=47.109.74.245,它是怎么跑去影响 WireGuard 到 192.168.1.2 的流量的?
三、最初的误解:以为 conntrack 只看「源 + 目的」
一开始的直觉是这样的:
conntrack 判定一个连接,不就是基于五元组(源地址、目的地址、源端口、目的端口、协议)吗?
既然 WireGuard endpoint 在192.168.1.2,和47.109.74.2436完全不一样,怎么会是“同一个连接”的一部分?
换句话说,我以为:
- mangle 规则只会命中
dst-address=47.109.74.243的连接; - 其它目的地址的连接不会受到
connection-mark=cell-wan-mark的影响。
事实证明,这个理解不完整。
四、关键点:conntrack 还有「RELATED」和 NAT 的影响
RouterOS(底层是 Linux conntrack)的连接跟踪有几个容易踩坑的特性:
- 连接簇(Connection family)
conntrack 不只有简单的「NEW / ESTABLISHED / INVALID」,还有一类叫 RELATED 的状态。
意思是:某些包虽然 src/dst 地址不一样,但被认为是原连接的派生流量,归入同一个连接簇。 - ICMP 错误、TCP RST 等都可能是 RELATED
比如对47.109.74.247 建立 TCP 连接时发生错误,产生了 ICMP error,
这个 ICMP 包的 src/dst 地址已经变了,但它属于RELATED,会 继承原连接的 conntrack 信息。 - NAT 前后地址不同
conntrack 在内部以 NAT 后的地址为主记录连接信息,
而你在 mangle 里看的是 NAT 前或 NAT 后的地址;
十分容易出现「看起来不是同一连接,但实际上被认为是同一个连接簇」的情况。 - RouterOS 的系统流量会共享 conntrack/连接
Router 本身的各种服务(DNS、路由探测、ICMP error、系统进程)可能挂在同一个 connection family 里,
再加上 output 链比较特殊,极容易产生「标记跨连接扩散」的现象。
核心问题就在这:
有一些包(比如系统产生的 ICMP/TCP error、NAT 相关包、系统服务流量)被 conntrack 归为与原连接 RELATED,
于是它们继承了connection-mark=cell-wan-mark。
然后你在chain=output上有规则“凡是 connection-mark=cell-wan-mark 就 mark-routing=cell”,
于是这些本来完全无关的包,也被强行打上了routing-mark=cell,
最终导致包括 WireGuard 流量在内的其它连接走错了出口。
这就是典型的 “连接标记污染(connection-mark pollution)”。
五、排查过程
1. 观察症状:WireGuard 流量被错误打标
- 在
/ip firewall mangle的output链查看计数器; - 发现去 WireGuard endpoint(
192.168.1.2)的包,也命中了connection-mark=cell-wan-mark对应的规则; - 即使根本没有匹配
dst-address=47.109.74.24的条件。
2. 确认是 conntrack 导致的继承,而不是规则写错
- 检查所有 mangle 规则,确认没有写错 dst-address;
- 确认没有其它地方给这些连接打相同的
connection-mark; - 排除简单的配置错误后,就只能从「conntrack 继承」的角度去考虑。
3. 分析链路:问题规则都在 chain=output
关键观察:
- 所有「打
connection-mark」和「打routing-mark」的规则,都写在chain=output; - RouterOS 本机所有发出的流量都经过 output,包括:
- WireGuard handshake
- 系统 DNS 请求
- 路由探测、ICMP error
- NAT 辅助包
- 以及各种系统服务包
因此:
只要 有一个来自本机的「系统包」被错误地归入某个 connection family,
然后在 output 链中继承/匹配到connection-mark=cell-wan-mark,
就会导致后续同族流量全部被打上routing-mark=cell,
于是本机到其它目的地址的流量也跑偏。
4. 验证:加「排除 LAN / WireGuard」规则后问题立即消失
尝试在 output 链前面加入排除规则,例如:
# 例:排除 LAN
/ip firewall mangle
add chain=output dst-address=192.168.1.0/24 action=return
# 例:排除 WireGuard 接口
add chain=output out-interface=wg1 action=return
结果:
- 和 LAN/WireGuard 相关的流量,输出时直接
return,不会再往后面的标记规则走; - 立刻不再出现 WireGuard 流量被
routing-mark=cell污染的问题。
这从侧面证明:问题根源确实是 chain=output + connection-mark 的组合,导致了继承和污染。
六、问题本质总结
- conntrack 不只是简单地「源地址 + 目的地址」
它还会通过 RELATED 等机制,把看起来“地址不一样”的包归入同一连接簇。 - 在
chain=output中对 connection-mark 做逻辑,风险极高
因为 RouterOS 本机的系统流量太多、太杂,极容易通过 conntrack 产生「无意继承」。 - 一旦某个连接被打上错误的
connection-mark,后续所有 RELATED 流量都会一起带坑
这就是所谓的「标记污染」。
七、解决方案一:在 output 链做「严格白名单式排除」
如果仍然想在 chain=output 里处理本机流量,可以这样改:
- 首先排除所有你不想被策略路由影响的流量,比如:
# 排除 WireGuard 接口
/ip firewall mangle
add chain=output out-interface=wg1 action=return
# 排除 LAN 段
add chain=output dst-address=192.168.1.0/24 action=return
# 排除运营商网关/探测地址(按需)
add chain=output dst-address=10.254.15.129 action=return
# 排除 DNS(如需要)
add chain=output protocol=udp dst-port=53 action=return
- 然后才写真正的标记规则:
# 只对本机去 47.109.74.243 的连接做 connection-mark
add chain=output dst-address=47.109.74.243 \
action=mark-connection new-connection-mark=cell-wan-mark passthrough=yes
# 再根据 connection-mark 打 routing-mark
add chain=output connection-mark=cell-wan-mark \
action=mark-routing new-routing-mark=cell passthrough=no
这种做法能明显降低风险,前提是你:
- 把所有重要的内部流量都排除出去;
- 并且后续规则只针对非常明确的目标地址/端口。
缺点是:排除列表需要维护,而且理论上仍然有被 RELATED 影响的可能(只是概率非常低)。
八、解决方案二:用 /routing rule 直接按目的地址选路(推荐)
本次排查之后,发现更稳、更“干净”的办法是:
不再用 mangle + connection-mark + routing-mark 来控制「本机到某特定 IP 的出口」,
而是直接用/routing rule按dst-address绑定路由表。
配置示例:
/routing rule
add action=lookup dst-address=47.109.74.243 table=cell
然后在 /ip route 中,确保 table=cell 有正确的默认路由,指向 cell 这个 WAN:
/ip route
add dst-address=0.0.0.0/0 gateway=<cell-gw> routing-table=cell
为什么这种方法更好?
- routing rule 工作在路由查找层,而非防火墙层
- 不经过 mangle;
- 不依赖 conntrack;
- 不受 NAT / RELATED / ASSURED 等状态影响;
- 纯粹是「对哪些目标地址,用哪个 routing-table」。
- 对本机流量生效,不需要特别说明 RouterOS 的 routing decision 对转发流量和本机发出的流量,都适用同一套规则,
所以/routing rule中写dst-address=47.109.74.243 table=cell,
会自然而然影响 router 自己访问这个 IP 的流量。 - 不会产生“标记污染” 不使用 connection-mark / routing-mark,自然也就不可能有「继承错误标记」的问题。
对于“仅仅是想让本机访问某几个 IP 走某个 WAN”的场景来说,/routing rule 是当前 RouterOS 推荐的现代做法,既简单又稳定。
九、解决方案三:在 prerouting 上处理 router 源地址(进阶)
如果确实需要用 mangle(比如做更复杂的策略路由,涉及转发流量),
可以将「本机流量」的策略移动到 prerouting 链,并通过 固定源地址 来识别 router 的流量。
大致思路
- 给 router 配一个稳定的“逻辑源 IP”(例如一个 loopback IP);
- 让本机某类流量使用这个 IP 作为源地址;
- 在
chain=prerouting中用src-address=<loopback IP>来标记。
示例(仅是示意,不是完整配置):
/ip address
add address=172.31.255.1/32 interface=loopback
/ip firewall mangle
add chain=prerouting src-address=172.31.255.1 dst-address=47.109.74.243 \
action=mark-routing new-routing-mark=cell passthrough=no
这种做法:
- 本质是在「进入路由决策前」对特定源/目的做标记;
- 避开了
chain=output的各种系统流量干扰; - 更适合做大规模策略路由,把 router 自己和转发流量统一纳入一个设计框架里。
缺点是实现复杂度更高,适合大规模/复杂网络,而不是简单的“本机几个 IP 要走某个 WAN”。
十、经验教训与建议
- 不要轻易在
chain=output里做 connection-mark 除非非常清楚自己在做什么,否则很容易出现「标记污染」——本机各种系统流量被误导到错误的出口。 - 如果必须在 output 链操作,一定要先做“白名单式排除” 用
dst-address、out-interface、协议/端口等条件,把所有不希望被策略路由影响的流量先action=return。 - 对“本机到特定目标 IP 走特定 WAN”的需求,优先考虑
/routing rule用法简单清晰:
/routing rule
add action=lookup dst-address=<目标IP或网段> table=<对应路由表>
不依赖 mangle 和 conntrack,是最稳、最干净的方式。
- 避免滥用 connection-mark 做一切策略 连接标记适合处理“按连接维度”的复杂策略,但其副作用(尤其配合 RELATED/NAT)也不容忽视。
对于简单的策略,routing rule + 多 routing-table 往往更优雅。
十一、总结
这次问题的本质是:
- 用传统的 mangle + connection-mark + routing-mark 方式,在
chain=output上控制本机流量出口; - 忽略了 conntrack 的 RELATED/NAT 行为,导致
connection-mark被“继承”到本不相关的流量上; - 进而造成 WireGuard 等流量也被错误打上
routing-mark,走错 WAN; - 最终通过「排除 LAN/WG」以及采用
/routing rule直接按目的地址选路,解决了问题。
简单一句话的教训:
本机策略路由,能用
/routing rule就尽量用它;
真的要用 mangle,就远离chain=output,或者至少先把不该碰的流量统统排除掉。
如果你也在 RouterOS 上做多 WAN 或策略路由,希望这篇踩坑记录能帮你少走一些弯路。