暗色模式

Linux 策略路由:从 ip rule 多路由表到 fwmark 分流

技术教程
2026-09-22
3
0
本文要点
  • Linux 选路不是「查一张路由表」,而是先查规则、再查表:内核的 RPDB(Routing Policy Database)按优先级依次匹配 ip rule,命中哪条,就去它指定的表里找路由
  • 系统默认只装三条规则:0: local32766: main32767: default。平时敲 ip route 看到的是 main 表,换个源地址就可能查到别的表去
  • 加一条 ip rule add from 10.10.1.1 table 100,同一个目的地址会因源地址不同走出两条路:实测 ip route get 1.1.1.1 from 10.10.1.1 得到 dev lo table 100,换成 10.10.2.1 立刻变成 via 172.16.0.1 dev eth0
  • ip rule add 不写 pref 时新规则插在 32766 之前并逐条递减(实测依次拿到 32765、32764)——后加的规则优先级反而更高,配多条规则务必显式写 pref
  • 按标记分流是策略路由的主战场:nftables 在 type route hook output 链里 meta mark set,再由 ip rule add fwmark 0x1 table 200 接住。实测 80 端口被打标后落进黑洞表(curl 退出码 28 超时),443 端口没打标照常走 main 表(HTTP 200)
  • ⚠️ iproute2 5.15 的 ip route get 不支持 mark 参数ip route get 1.1.1.1 mark 0x1 直接报 RTNETLINK answers: Invalid argument——想验证 fwmark 分流,只能靠真实流量或 nft 的计数器
  • 规则的动作未必是「查表」:prohibitunreachableblackhole 可以直接给结论,三者报错各不相同(Operation not permitted / Network is unreachable / connect 返回 EINVAL)
  • ip rule 加的规则重启即失效,它不写任何配置文件;要持久化得落到 netplan 的 routing-policy 或 systemd-networkd 的 [RoutingPolicyRule]
  • 全部命令在 Ubuntu 22.04 LTS(内核 5.15.0-30、iproute2 5.15.0、1 核 1.9G 云主机)上真实执行,截图即真实输出

Linux 策略路由:从 ip rule 多路由表到 fwmark 分流

一台机器一条默认路由,是绝大多数教程里 Linux 网络的样子:default via 172.16.0.1 dev eth0,所有出网的包都从这儿走,没有第二种可能。

可现实里的需求经常是「看人下菜」:同一个目的地址,A 程序的流量走线路一、B 程序的走线路二;或者本机有两个地址,想让某个地址作为源发出的包走指定出口。这些需求用 ip route 表达不出来——因为传统路由表只能回答「去往某个网段该走哪个网关」,回答不了「这个包是谁发的」。

要表达「谁发的」,得用策略路由(policy routing):内核在查路由表之前,先按一套规则决定「这次该查哪张表」。这套规则就是本文要拆的 ip rule

选路是两步:先规则,后表

先把默认状态看清楚:

ip rule show
echo "--- main 表 ---"
ip route show table main
echo "--- local 表(前 5 条)---"
ip route show table local | head -5
0:    from all lookup local
32766:    from all lookup main
32767:    from all lookup default
--- main 表 ---
default via 172.16.0.1 dev eth0 proto static 
172.16.0.0/12 dev eth0 proto kernel scope link src 172.16.0.84 
--- local 表(前 5 条)---
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1 
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1 
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1 
local 172.16.0.84 dev eth0 proto kernel scope host src 172.16.0.84 
broadcast 172.31.255.255 dev eth0 proto kernel scope link src 172.16.0.84 

ip rule show 与 main、local 两张表:默认三条规则按 0/32766/32767 的优先级排列

ip rule show 的输出格式是 优先级: 匹配条件 动作。三条默认规则的含义:

  • 0: from all lookup local——优先级最高,匹配所有包,去查 local 表。这张表由内核自动维护,装的是本机所有地址和广播地址。它的存在解释了一个常见现象:为什么给网卡配了 IP,本机 ping 自己立刻能通,连默认路由都不需要有。
  • 32766: from all lookup main——这就是平时敲 ip route 看到的那张表。你的默认网关、静态路由都在这儿。
  • 32767: from all lookup default——兜底规则。这张表通常根本没有内容,甚至内核里不存在这张 FIB 表:本机实测 ip route show table default 会直接报 Error: ipv4: FIB table does not exist。它是一条「查不到也算查过」的收尾。

关键在优先级(pref):数字小的先匹配,匹配上就不再往下看。默认规则把 0、32766、32767 占了,中间这一大片空档(1~32765)就是留给自定义规则的位置。

让指定源地址走另一张表

策略路由最小的一个实验,不需要第二块网卡,甚至不需要改默认路由。我们造两张「只有自己知道」的表,让不同源地址的包查到不同的结果:

ip addr add 10.10.1.1/32 dev lo 2>/dev/null; ip addr add 10.10.2.1/32 dev lo 2>/dev/null
ip route replace default dev lo table 100
ip rule add from 10.10.1.1 table 100 2>/dev/null
ip rule show
echo "--- 源地址 10.10.1.1 发出 ---"; ip route get 1.1.1.1 from 10.10.1.1
echo "--- 源地址 10.10.2.1 发出 ---"; ip route get 1.1.1.1 from 10.10.2.1
0:    from all lookup local
32765:    from 10.10.1.1 lookup 100
32766:    from all lookup main
32767:    from all lookup default
--- 源地址 10.10.1.1 发出 ---
1.1.1.1 from 10.10.1.1 dev lo table 100 uid 0 
    cache <local> 
--- 源地址 10.10.2.1 发出 ---
1.1.1.1 from 10.10.2.1 via 172.16.0.1 dev eth0 uid 0 
    cache 

源地址分流实测:同一个目的地址 1.1.1.1,10.10.1.1 走表 100 落到 lo,10.10.2.1 走 main 表正常出网

同一个目的地址 1.1.1.1,两条结果完全不同:

  • 源地址是 10.10.1.1 → 命中新加的 32765 规则 → 查表 100 → 表 100 里写的是 default dev lo,于是选中 dev lo
  • 源地址是 10.10.2.1 → 32765 规则不匹配 → 继续往下走 32766 的 main 表 → 拿到真实的 via 172.16.0.1 dev eth0

这里最值得记住的命令是 ip route get。它不修改任何东西,只是完整地跑一遍选路逻辑然后告诉你结果。它的参数可以模拟各种条件:

ip route get 1.1.1.1 from 10.10.1.1     # 模拟这个源地址发出的包
ip route get 1.1.1.1 iif eth0           # 模拟从这个接口进来的包
ip route get 1.1.1.1 uid 1000           # 模拟这个用户产生的包

排查策略路由问题时,别猜,直接 ip route get 加上条件问内核。它输出里的 table 100 就是「这次查的不是 main 表」的铁证——普通路由查询不会打印 table 字段。

另外注意那条新规则的优先级 32765ip rule add 时没写 pref,内核把它插在 main(32766)前面一位。再加一条会拿到 32764——优先级递减,也就是后加的规则更优先。这跟很多人「后加的在后面兜底」的直觉正好相反,配多条规则时务必显式写 pref NUMBER

按标记分流:fwmark 与真实流量验证

按源地址分流只是开胃菜。真实场景里更常见的是「同一个源地址,不同类型的流量走不同出口」——比如 80 端口走线路 A、443 端口走线路 B。源地址区分不了它们,得给包打标记(fwmark),再让规则按标记分流。

分工是这样的:打标记是防火墙的活(nftables/iptables 的 mangle 层),按标记选路ip rule 的活。

nft delete table inet demo 2>/dev/null
nft add table inet demo
nft 'add chain inet demo output { type route hook output priority mangle ; }'
nft add rule inet demo output tcp dport 80 meta mark set 0x1 counter
ip rule del fwmark 0x1 table 200 2>/dev/null
ip rule add fwmark 0x1 table 200
ip route replace blackhole default table 200
echo "--- 80 端口:被 nft 打标 → 查表 200(黑洞)---"
curl -s -m 5 -o /dev/null -w "HTTP code=%{http_code}\n" http://www.baidu.com; echo "curl 退出码=$?"
echo "--- 443 端口:未打标 → 查 main 表(正常出网)---"
curl -s -m 8 -o /dev/null -w "HTTP code=%{http_code}\n" https://www.baidu.com; echo "curl 退出码=$?"
nft list chain inet demo output
ip rule show
--- 80 端口:被 nft 打标 → 查表 200(黑洞)---
HTTP code=000
curl 退出码=28
--- 443 端口:未打标 → 查 main 表(正常出网)---
HTTP code=200
curl 退出码=0
table inet demo {
    chain output {
        type route hook output priority mangle; policy accept;
        tcp dport 80 meta mark set 0x00000001 counter packets 2 bytes 120
    }
}
0:    from all lookup local
32764:    from all fwmark 0x1 lookup 200
32765:    from 10.10.1.1 lookup 100
32766:    from all lookup main
32767:    from all lookup default

fwmark 分流实测:80 端口被打标后落进表 200 的黑洞(curl 退出码 28),443 端口没打标照常 200;nft 计数器显示命中 2 个包

这段实测里有三个要点:

① 表 200 是故意写成黑洞的,只为让差别看得见。 ip route replace blackhole default table 200 的意思是「这张表的所有流量一律丢弃」。如果表 200 里写的是一条正常网关,两条 curl 都会成功,反而看不出规则有没有生效。用一个必然失败的出口做对照,是验证策略路由最省事的办法。

② 结果是干净的对照: 80 端口被打了 0x1 的标记 → 命中 32764: from all fwmark 0x1 lookup 200 → 查表 200 → 黑洞丢掉 → curl 等了 5 秒超时,退出码 28CURLE_OPERATION_TIMEDOUT),HTTP code 是 000。443 端口没被标记 → 落到 32766 的 main 表 → 正常出网,HTTP code=200、退出码 0

③ nft 的计数器是分流真的发生了的第二重证据。 counter packets 2 bytes 120 说明「tcp dport 80 打标记」这条规则确实命中了 2 个包(就是 curl 发出的 SYN 和它的重传)。写策略路由时养成给关键规则挂 counter 的习惯——规则有没有被匹配到,计数器比猜可靠

还有两个坑要提前说:

  • nft 那条链必须是 type route hook output(不是常见的 type filter)。只有在路由决策点上执行的链里改 mark,内核才会因为 mark 变化重新做一次路由查找;放在 filter 链里改,路由早就查完了,改了也白改。
  • ⚠️ ip route get 在 iproute2 5.15 上不支持 mark 参数。本机实测 ip route get 1.1.1.1 mark 0x1 直接报 RTNETLINK answers: Invalid argument,网上不少文章里的这条「验证命令」在这台机器上根本跑不通。要看 fwmark 分流的效果,只能用真实流量(上面的 curl 对照)或 nft 计数器——所以第 ① 点的对照设计不是花活,是必需品。

规则的动作不止「查表」

ip rule 的匹配条件后面跟的动作,除了 lookup 表号,还可以直接是三种拒绝动作。它们不需要建表,写起来就是一条规则:

ip rule add to 10.99.0.0/16 prohibit pref 5000
ip rule add to 10.98.0.0/16 unreachable pref 5001
ip rule add to 10.97.0.0/16 blackhole pref 5002
ip rule show | head -7
ip route get 10.99.1.1   # prohibit:被策略拒绝
ping -c1 -W2 10.98.1.1   # unreachable:无路可走
ping -c1 -W2 10.97.1.1   # blackhole:静默丢弃
0:    from all lookup local
5000:    from all to 10.99.0.0/16 prohibit
5001:    from all to 10.98.0.0/16 unreachable
5002:    from all to 10.97.0.0/16 blackhole
32766:    from all lookup main
32767:    from all lookup default
RTNETLINK answers: Permission denied
ping: connect: Network is unreachable
ping: Do you want to ping broadcast? Then -b. If not, check your local firewall rules

三种动作的区别,直接体现在报错上:

动作含义本机实测到的报错
prohibit管理策略禁止,明确拒绝RTNETLINK answers: Permission denied
unreachable无路可走ping: connect: Network is unreachable
blackhole静默丢弃,连错误都不回RTNETLINK answers: Invalid argumentping 则打印一句莫名其妙的「Do you want to ping broadcast?」

最后那句 ping 的提示值得单独说:它是被误导出来的。blackhole 会让 connect() 返回 EINVAL,而 pingEINVAL 当成了「目标是个广播地址,你得加 -b」,于是弹出一句和广播毫无关系的提示。看到这句话别去查广播,查 ip rule——包被黑洞吞掉了。这也是「同一个 errno 在不同工具嘴里是不同故事」的典型例子。

优先级与持久化

关于 pref,只有一条规则:数字小的先匹配,命中即停。 想插队就写小数字(1~32765),想兜底就写大数字。给规则分类留号段是常见做法,比如 1000 段做源地址分流、2000 段做 fwmark 分流,以后排查时一眼能看出这条规则是谁加的。

关于持久化,要有心理准备:ip ruleip route 一样,写完只在内存里,重启就没了。 它不碰任何配置文件。Ubuntu 上要让它开机自动生效,得写进 netplan:

network:
  version: 2
  ethernets:
    eth0:
      routing-policy:            # 对应 ip rule
        - from: 10.10.1.1
          table: 100
      routes:
        - to: default
          via: 172.16.0.1
          table: 100             # 对应 ip route ... table 100

(上面只是写法示意,本文的演示机没改网络配置文件——改之前请确认你有带外通道,别把 SSH 关在门外。)用 systemd-networkd 的话,对应的是 [RoutingPolicyRule] 段;用 NetworkManager 则是 nmclirouting-rule

⚠️ 最后是一条真正的红线:ip rule 的优先级是全局的,一条写错的规则可以把 SSH 也带走。 比如 ip rule add from all lookup 100 pref 100 而表 100 是空的,本机所有出站流量立刻失去路由,远程连接当场断掉,只能去控制台救。所以:

  1. 规则里的匹配条件尽量写窄(按源地址、按 fwmark),不要 from all
  2. pref 别乱写小数字,把 1~999 留给系统级规则;
  3. 加完规则立刻用 ip route get 加条件验证一遍,特别要验证你自己这条 SSH 连接的场景

清理

实验做完把规则、表、地址都撤掉,恢复成出厂的三条默认规则:

ip rule del from 10.10.1.1 table 100
ip rule del fwmark 0x1 table 200
ip route flush table 100
ip route flush table 200
ip addr del 10.10.1.1/32 dev lo
ip addr del 10.10.2.1/32 dev lo
nft delete table inet demo
ip rule show
ip -br addr
0:    from all lookup local
32766:    from all lookup main
32767:    from all lookup default
lo               UNKNOWN        127.0.0.1/8 
eth0             UP             172.16.0.84/12 

ip rule del 必须把匹配条件写全(这也是它比 add 难用的地方,写错一条会报 RTNETLINK answers: No such file or directory);表内容用 ip route flush table N 一次清空。注意 flush table main 是绝对禁忌——那会把默认路由一起清掉。

坑位清单

  1. ip route get 是策略路由的第一诊断命令,记得带 from/iif/uid 条件去模拟真实场景,输出里的 table N 就是分流生效的证据。
  2. 不写 pref 的新规则会插到 main 前面且逐条递减(32765、32764……),后加的反而更优先。多条规则一律显式写 pref。
  3. fwmark 必须在 type route 的链里打(hook output + priority mangle),在 filter 链里改 mark 不会触发重新选路。
  4. iproute2 5.15 的 ip route get 不支持 mark 参数,验证 fwmark 只能靠真实流量或 nft counter
  5. 验证时给对照实验设计一个必然失败的出口(黑洞表),否则「成功」和「规则没生效」看起来一模一样。
  6. blackhole 的报错会被工具二次翻译ping 会把它说成「广播地址」,别被带偏。
  7. 规则只在内存里,重启即失效;持久化写 netplan routing-policy / systemd-networkd [RoutingPolicyRule]
  8. 别用 from all + 小 pref,那是把自己的 SSH 送上断头台。

小结

策略路由的全部逻辑可以压成一句话:内核先按 ip rule 从上往下匹配,命中哪条规则,就去它指定的表里查路由。

想清楚这三件事,剩下的都是查文档:

  • 谁来匹配——from(源地址)、to(目的地址)、fwmark(标记)、iif/oif(进出接口)、uidrange(用户),ip rule help 里列全了;
  • 匹配后去哪——lookup N 查表,或 prohibit/unreachable/blackhole 直接下结论;
  • 优先级多少——pref 小的先赢,命中的第一条说了算。

这三步也是容器网络、多线路出口、VPN 分流(from all fwmark 0x... lookup 100 那一套)的共同底座:所谓「分流规则」,拆开看都是这几行 ip rule 加几张路由表。

想先把基础补一遍的,可以看 Linux 网络配置:从 ip 命令到 netplan 静态 IP;排查网络不通的常规套路在 Linux 网络排查:从 ping 到 tcpdump 的故障定位手册;如果对「标记 + 规则」这套组合想再深入一层,可以配合 Linux 防火墙:从 iptables 到 nftables 完整指南Linux 网络模拟:tc 与 netem 注入延迟、抖动与丢包 一起看;想看规则这套思路在另一层的实现,Linux 网络命名空间:从 ip netns 到 veth 虚拟网卡互联 里的 netns 也值得对照。

发表评论

暂无评论,快来抢沙发吧!