使用 RouterOS 做策略路由时,被 conntrack「标记污染」坑了一次的完整排查记录

这篇是一次真实排查过程的整理:
场景是用 MikroTik RouterOS 做双线/多 WAN 策略路由,本机需要访问某个特定 IP 走指定 WAN。
看起来很简单的一件事,结果却被 conntrack / mangle / output 链联合作死了一回。

这里把「问题现象 → 原因分析 → 排查过程 → 最终解决方案和建议」完整记录下来,方便以后自己和别人少踩坑。


一、问题背景

需求很简单:

让路由器本机访问某个外部 IP(例如 47.109.74.244)时,强制走 cell 这个 WAN(比如 4G/5G 蜂窝)。

常规做法是:

  1. /ip firewall mangle 里给这类连接打 connection-mark
  2. 再根据 connection-mark 给数据包打 routing-mark
  3. 再在 /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)的连接跟踪有几个容易踩坑的特性:

  1. 连接簇(Connection family)
    conntrack 不只有简单的「NEW / ESTABLISHED / INVALID」,还有一类叫 RELATED 的状态。
    意思是:某些包虽然 src/dst 地址不一样,但被认为是原连接的派生流量,归入同一个连接簇。
  2. ICMP 错误、TCP RST 等都可能是 RELATED
    比如对 47.109.74.247 建立 TCP 连接时发生错误,产生了 ICMP error,
    这个 ICMP 包的 src/dst 地址已经变了,但它属于 RELATED,会 继承原连接的 conntrack 信息
  3. NAT 前后地址不同
    conntrack 在内部以 NAT 后的地址为主记录连接信息,
    而你在 mangle 里看的是 NAT 前或 NAT 后的地址;
    十分容易出现「看起来不是同一连接,但实际上被认为是同一个连接簇」的情况。
  4. 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 mangleoutput 链查看计数器;
  • 发现去 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 的组合,导致了继承和污染。


六、问题本质总结

  1. conntrack 不只是简单地「源地址 + 目的地址」
    它还会通过 RELATED 等机制,把看起来“地址不一样”的包归入同一连接簇。
  2. chain=output 中对 connection-mark 做逻辑,风险极高
    因为 RouterOS 本机的系统流量太多、太杂,极容易通过 conntrack 产生「无意继承」。
  3. 一旦某个连接被打上错误的 connection-mark,后续所有 RELATED 流量都会一起带坑
    这就是所谓的「标记污染」。

七、解决方案一:在 output 链做「严格白名单式排除」

如果仍然想在 chain=output 里处理本机流量,可以这样改:

  1. 首先排除所有你不想被策略路由影响的流量,比如:
   # 排除 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
  1. 然后才写真正的标记规则:
   # 只对本机去 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 ruledst-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

为什么这种方法更好?

  1. routing rule 工作在路由查找层,而非防火墙层
  • 不经过 mangle;
  • 不依赖 conntrack;
  • 不受 NAT / RELATED / ASSURED 等状态影响;
  • 纯粹是「对哪些目标地址,用哪个 routing-table」。
  1. 对本机流量生效,不需要特别说明 RouterOS 的 routing decision 对转发流量和本机发出的流量,都适用同一套规则,
    所以 /routing rule 中写 dst-address=47.109.74.243 table=cell
    会自然而然影响 router 自己访问这个 IP 的流量。
  2. 不会产生“标记污染” 不使用 connection-mark / routing-mark,自然也就不可能有「继承错误标记」的问题。

对于“仅仅是想让本机访问某几个 IP 走某个 WAN”的场景来说,
/routing rule 是当前 RouterOS 推荐的现代做法,既简单又稳定。


九、解决方案三:在 prerouting 上处理 router 源地址(进阶)

如果确实需要用 mangle(比如做更复杂的策略路由,涉及转发流量),
可以将「本机流量」的策略移动到 prerouting 链,并通过 固定源地址 来识别 router 的流量。

大致思路

  1. 给 router 配一个稳定的“逻辑源 IP”(例如一个 loopback IP);
  2. 让本机某类流量使用这个 IP 作为源地址;
  3. 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”。


十、经验教训与建议

  1. 不要轻易在 chain=output 里做 connection-mark 除非非常清楚自己在做什么,否则很容易出现「标记污染」——本机各种系统流量被误导到错误的出口。
  2. 如果必须在 output 链操作,一定要先做“白名单式排除”dst-addressout-interface、协议/端口等条件,把所有不希望被策略路由影响的流量先 action=return
  3. 对“本机到特定目标 IP 走特定 WAN”的需求,优先考虑 /routing rule 用法简单清晰:
   /routing rule
   add action=lookup dst-address=<目标IP或网段> table=<对应路由表>

不依赖 mangle 和 conntrack,是最稳、最干净的方式。

  1. 避免滥用 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 或策略路由,希望这篇踩坑记录能帮你少走一些弯路。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注