登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

ip rule 配合 fwmark 实现策略路由的设计

来源:17golang原创

时间:2026-11-13 19:00:47 426浏览 收藏

我在一台有两条出口的 Linux 主机上第一次用策略路由时,最容易混在一起的是三个对象:防火墙写入的 fwmark、负责匹配的 ip rule,以及真正保存下一跳的路由表。设计原则其实很简单:先在正确的链上给报文留下标记,再让 RPDB 按标记选择独立表,最后在独立表里只放这条出口需要的路由。

官方地址:https://man7.org/linux/man-pages/man8/ip-rule.8.html

下面用 0x1 表示“走出口 A”,用表 100 表示出口 A 的路由集合。示例地址是文档示意地址,不代表真实网络;重点是对象之间的边界和检查顺序。

先把 mark、ip rule 和路由表分开

ip rule 管理的是路由策略数据库(RPDB)。规则按 priority 从小到大匹配,命中后可以把查找交给某张路由表。fwmark 不是下一跳,也不是接口名称,它只是报文上的一个 32 位标记,供规则选择器读取。

我会先把数据关系画成三层:Netfilter 负责“写标记”,RPDB 负责“选表”,路由表负责“给出出口”。这样排障时不会把“规则命中了”误认为“下一跳一定可用”。

说明图:报文标记、RPDB 规则与独立路由表之间的静态关系
图1:静态结构说明图,查看 Netfilter 写入 nfmark、ip rule 读取 fwmark,以及表 100 返回出口路由的关系;这不是截图或运行证据。
# 给出口 A 单独建立一张路由表;示例网段仅用于说明结构
ip route add table 100 192.0.2.0/24 dev eth1 src 192.0.2.10
ip route add table 100 default via 192.0.2.1 dev eth1

# priority 数字越小越先匹配,掩码只比较 mark 的低 8 位
ip rule add priority 100 fwmark 0x1/0xff table 100

# 查看 RPDB,确认自定义规则是否位于预期位置
ip rule show

这里的关键不是表号 100,而是显式 priority 和标记掩码。系统通常还会有 priority 0 的 local、32766 的 main、32767 的 default 规则;自定义规则若没有合适的优先级,可能看起来存在,却在预期之前或之后被其它规则处理。

用掩码给标记预留可扩展空间

如果只写 fwmark 1,初期很直观,但后面想同时表达出口、业务类型或回程域时容易重构。更稳妥的做法是约定字段:低 8 位存出口编号,高 8 位存业务域,规则只匹配自己负责的位。

# 只让低 8 位决定出口 A,其他业务位可以继续保留
ip rule add priority 100 fwmark 0x01/0xff table 100

# 只让高 8 位决定另一组策略,避免覆盖出口字段
ip rule add priority 110 fwmark 0x0100/0xff00 table 200

# 在 mangle 表的正确链上写入低 8 位;--set-mark 带掩码可避免改动其它位
iptables -t mangle -A OUTPUT -d 203.0.113.0/24 \
  -j MARK --set-mark 0x01/0xff

掩码设计的收益是规则职责清楚,代价是必须把位段约定写进运维文档。不要让不同脚本随意复用同一位,否则 ip rule 命中结果会变得难以解释。Netfilter 手册也明确区分了报文 mark 与连接 mark;两者不是同一个字段。

本机流量和转发流量要在不同位置打标

我踩过的一个坑是只在 FORWARD 链上打标,然后用本机发起的连接测试。FORWARD 处理的是转发路径,本机进程产生的流量通常要在 OUTPUT 阶段完成标记;转发报文则常在 PREROUTING 阶段打标,才能影响后续路由查找。

# 本机进程发出的流量:在 OUTPUT 里按目标网段打标
iptables -t mangle -A OUTPUT -d 203.0.113.0/24 \
  -j MARK --set-mark 0x01/0xff

# 经过本机转发的流量:在 PREROUTING 里按入口接口打标
iptables -t mangle -A PREROUTING -i lan0 -d 203.0.113.0/24 \
  -j MARK --set-mark 0x01/0xff

# 用计数器观察规则是否真的收到匹配报文
iptables -t mangle -L OUTPUT -v -n
iptables -t mangle -L PREROUTING -v -n

这两条链不是可以互换的“习惯写法”。打标点必须早于你要影响的路由决策,而且要与流量类型对应。还要考虑回程:只给去程选择出口,不代表返回包会自动沿同一策略回来;多出口场景通常需要对连接做一致性标记。

用 CONNMARK 让一条连接沿同一条策略

单个报文的 nfmark 会随着包处理而变化,而连接级的 ctmark 可以把第一次判断保存下来,再为后续包恢复。我的经验是把“首次分类”和“恢复已有分类”分开写,避免每个包都重新依据临时条件覆盖标记。

说明图:报文 nfmark、连接 ctmark、fwmark 规则和出口路由表的静态关系
图2:连接标记关系说明图,查看 nfmark 与 ctmark 的保存/恢复边界,以及它们如何共同指向出口路由表;这不是截图或运行证据。
# 先恢复已有连接标记,避免后续报文重新分类
iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark

# 新连接第一次命中条件时设置报文标记
iptables -t mangle -A PREROUTING -m conntrack --ctstate NEW \
  -i lan0 -d 203.0.113.0/24 -j MARK --set-mark 0x01/0xff

# 将本次报文的 nfmark 保存到连接 ctmark,供后续包恢复
iptables -t mangle -A PREROUTING -m mark --mark 0x01/0xff \
  -j CONNMARK --save-mark --nfmask 0xff --ctmask 0xff

如果主机既转发又本机发包,还要为 OUTPUT 设计对应的 restore/save 位置;不要默认 PREROUTING 能覆盖本机产生的所有流量。使用 nftables 时也可以表达相同的标记关系,但字段命名和规则语法不同,迁移时要重新确认 hook 和查找时机。

按“规则—标记—路由”三层排查

出现“规则写了但没有走出口 A”时,我不会先改网关,而是逐层缩小范围:

  1. 先看 ip rule show,确认 priority、fwmark 掩码和 table 编号;规则存在不等于优先级正确。
  2. 再看 mangle 链的 -v 计数,确认目标流量经过了 OUTPUT 或 PREROUTING,而不是只检查 FORWARD。
  3. 最后用带 mark 的路由查询对比主表查询,确认表 100 里确实有到目标网段的路由。
# 不发送真实报文,只查询指定标记下的路由选择
ip route get 203.0.113.8 mark 0x1

# 对照不带 mark 的默认查询,便于判断是否真的切换了表
ip route get 203.0.113.8

# 检查自定义表中的直连路由和默认路由
ip route show table 100

如果带 mark 和不带 mark 的结果相同,优先检查规则优先级、掩码是否匹配,以及表 100 是否有可用路由;如果规则命中但路由失败,再看网关、接口地址和回程路径。不要把 mark 当作安全边界,它只是路由选择输入,真正的过滤仍应由防火墙规则承担。

我的取舍:什么时候值得用 fwmark

当策略条件能稳定映射到源地址、入口接口、服务类别或连接状态,并且确实需要多张路由表时,fwmark 很灵活。它不适合被当成“所有网络问题的万能开关”:只有一条出口、没有转发、也不需要按业务分流时,普通 main 表通常更容易维护。

我最终会把配置拆成三份:标记规则说明“谁写入什么位”,ip rule 说明“哪些位选择哪张表”,路由表说明“这张表有哪些网段和下一跳”。三份内容能独立检查,变更时也更容易回滚。

常见问题

为什么 ip rule 明明存在却没有生效?

优先检查 priority 是否被更小数字的规则抢先处理,再确认 fwmark 掩码与实际 mark 一致,最后检查目标路由是否真的存在于指定 table。

只给首包设置 mark 够不够?

短链路测试可能看不出问题,但多包连接或回程流量通常需要 CONNMARK 保存和恢复连接级分类,否则后续包可能重新走默认路径。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>