跳到主要内容

ch06_7

6.7 策略路由(Policy Routing)

上一节我们聊了组播,那是关于「一对多」的艺术。这一节,我们要把视角拉回来,重新审视那个看似简单的动作——查表

在传统的路由观念里,数据包就像一个只看目的地的旅客:不管你是谁、不管你从哪来、不管你背负着什么标记,只要你要去同一个地方,我就把你送上同一辆车。

但现实世界不是这样的。有时候,你需要让从专线进来的流量走昂贵的优质链路,而让来自公网的流量走廉价的拥塞链路;有时候,你需要根据 IP 包头的 TOS 字段来分流;有时候,你甚至需要根据防火墙打上的标记来决定路由。

这就是策略路由。它打破了「目的地决定一切」的规则,允许系统管理员定义「如果满足条件 A,就查表 B;如果满足条件 C,就查表 C」。

在这一节,我们重点讨论 IPv4 的策略路由实现(IPv6 的我们留到第 8 章再去折腾)。为了不让你的思维打架,我把策略路由里的条目称为规则,而把路由表里的条目称为路由。千万别搞混了:规则决定查哪张表,表决定往哪跳。


策略路由的管理

以前我们习惯用 route 命令来管理路由,但在策略路由这块,route 命令就显得力不从心了。你得靠 iproute2 包里的 ip rule 命令。这是一套完全不同的管理接口。

让我们来看看怎么玩转这些规则。

添加规则

假设你想让所有 TOS 字段为 0x04 的数据包去查表 252。你可以这样下命令:

ip rule add tos 0x04 table 252

这一行命令背后发生了什么?

当你敲下回车,内核会调用 net/core/fib_rules.c 里的 fib_nl_newrule() 方法。它会在你的规则链里插入一条新规矩:以后凡是遇到 TOS 匹配 0x04 的包,别去查默认的 main 表了,去查 table 252

光有规则还不够,表 252 里得有路。你可以这样往表 252 里塞路由:

ip route add default via 192.168.2.10 table 252

这里的 tos 只是众多选择器中的一种。你可以匹配源地址、目的地址、入接口、防火墙标记(fwmark)等等。具体的参数表可以去查 man 8 ip-rule,或者翻到本章末尾的「快速参考」找 Table 6-1。

删除规则

不想玩了?删掉就行。

ip rule del tos 0x04 table 252

这个动作在内核里由 fib_nl_delrule() 接手,它会去规则链里把你之前加的那条给摘掉。

查看规则

想看看现在系统里都有哪些乱七八糟的规矩?

ip rule list
# 或者
ip rule show

这两个命令其实是一回事,它们最终都会调用内核里的 fib_nl_dumprule() 方法,把你定制的所有规则一股脑倒出来给你看。

搞清楚了怎么管理,我们就不得不扒开内核看看这东西到底是怎么造出来的。


内核实现细节

这里有一点很容易让人晕头:源码树里有好几个叫 fib_rules.c 的文件。

  • net/core/fib_rules.c:这是真正的核心基础设施,它不特定于 IPv4 或 IPv6,是一个通用的框架。
  • net/ipv4/fib_rules.c:这是 IPv4 对这个框架的实现。
  • net/ipv6/fib6_rules.c:这是 IPv6 的实现。

我们重点关注 IPv4 的实现。

核心数据结构

在 IPv4 的世界里,每一条规则最终都会被映射成一个 fib4_rule 结构体。定义在 net/ipv4/fib_rules.c 里:

struct fib4_rule {
struct fib_rule common;
u8 dst_len;
u8 src_len;
u8 tos;
__be32 src;
__be32 srcmask;
__be32 dst;
__be32 dstmask;
#ifdef CONFIG_IP_ROUTE_CLASSID
u32 tclassid;
#endif
};

你可以看到,这里面不仅有源地址(src)、目的地址(dst),还有 TOS 字段。这些就是用来匹配数据包的「过滤器」。如果数据包的头部信息和这里面的字段对上了,这条规则就命中了。

启动时的默认规则

什么都没做的时候,你的系统里其实已经有三条铁律了。内核启动时,fib_default_rules_init() 方法会悄无声息地帮你创建三个默认策略:

  1. Local 表 (RT_TABLE_LOCAL):专门处理本地地址(比如 127.0.0.1)和广播地址。
  2. Main 表 (RT_TABLE_MAIN):这就是我们平时用 ip route 看到的那张表,所有没打标记的常规流量都在这。
  3. Default 表 (RT_TABLE_DEFAULT):这是最后的救命稻草,如果前面都没匹配上,就用这张表(通常是空的,或者丢给网关)。

查找流程:快路径与慢路径

这是整个机制里最精妙的部分。当内核需要为一个数据包找路由时,它会调用 fib_lookup() 方法。

include/net/ip_fib.h 里,你会看到两个长得像但其实不同的 fib_lookup()。如果内核配置里没开 CONFIG_IP_MULTIPLE_TABLES(也就是没用策略路由),那就走那个简单的、查单表的逻辑。

但只要开启了策略路由,事情就变得有趣了。内核会面临两种情况:

  1. 快路径:变量 net->ipv4.fib_has_custom_rulesfalse。这意味着你从来没有动过默认规则,系统还是原装出厂设置。既然规则没变,那就不需要遍历规则链,直接按顺序查 Local → Main → Default 表。这省去了遍历规则的开销。
  2. 慢路径:一旦你执行了 ip rule add 或者 ip rule delfib4_rule_configure()fib4_rule_delete() 就会把 net->ipv4.fib_has_custom_rules 设为 true。这时候,内核知道事情不简单了,它会调用 _fib_lookup(),进而进入 fib_rules_lookup()

fib_rules_lookup() 做的事情很繁琐但很必要:它拿着数据包,在规则链表里从头走到尾,对每一条规则调用 fib_rule_match()。如果规则匹配上了,就去查该规则指定的那个路由表;如果没匹配上,就继续下一条。

这个逻辑虽然看起来笨重,但它是灵活性的代价。你想要自由,就得付出 CPU 周期。


这里我们聊完了策略路由的架子。但有了策略路由,我们就能做更多花哨的事了。

下一节,我们要讨论一种更具体的「高级玩法」——多路径路由。也就是当路由表里告诉你「下一跳有两个,都可以走」的时候,内核该怎么选?是轮流?还是加权?这又是一个关乎平衡与性能的话题。