5.5 Policy Routing:地图之外的选择权
上一节我们讲到 fib_nh 就像是一个尽职的向导,手里拿着写明出接口和网关地址的便签,指引着数据包怎么离开内核。我们建立了一个非常直观的认知:目的地决定路由。只要知道你想去哪(目标 IP),路由表就能告诉你怎么走。
这个认知在 99% 的情况下是够用的。但作为一个正在探究底层机制的工程师,你可能会遇到那剩下的 1%。
想象这样一个场景:你有一台机器,插了两根网线。
- 一根是
eth0,接的是内网,用来管理,不花钱。 - 一根是
eth1,接的是外网,按流量计费,很贵。
现在你跑了一个程序,它需要从 10.0.0.1 下载数据。按照老规矩,内核查路由表发现 10.0.0.1 走 eth0 能通,于是把流量全打出去了。
但你不想这样。你心里有本账:虽然目标地址一样,但如果这个流量是系统备份产生的,我就想走昂贵的 eth1(因为快),如果是普通浏览,我才走 eth0。
如果只看目的地,传统的路由表无能为力。它眼里只有目标,不问出身。
这就是 Policy Routing(策略路由) 要解决的问题。它让路由决策不再单纯基于「我要去哪」,而是可以基于「我是谁」、「我要干什么」甚至「我用了什么协议」。在引入这个机制之前,让我们先看看在它的对立面——即没有策略路由的世界里,事情是怎么运作的。
没有 Policy Routing 的日子:两张表
在内核配置项 CONFIG_IP_MULTIPLE_TABLES 没有被开启的情况下,内核的路由世界是非常简单的:只有两张表。
-
Local Table (RT_TABLE_LOCAL, ID 255) 这是内核的「私人领地」。里面全是给本机 IP 地址的路由(比如
127.0.0.1或者你配给eth0的 IP)。这张表非常敏感,只有内核自己能往里面加东西。管理员(User)如果试图用ip route往 Local 表里塞条目,会被拒绝。这张表决定了「哪些地址是属于我的」。 -
Main Table (RT_TABLE_MAIN, ID 254) 这是我们的「世界地图」。绝大多数你通过
ip route add命令配置的路由,都在这张表里。它决定了「如果地址不是我的,该往哪扔」。
这个初始化过程发生在 net/ipv4/fib_frontend.c 的 fib4_rules_init() 方法里。
历史的注脚: 在 2.6.25 之前的内核里,这两个表还是两个全局变量:
ip_fib_local_table和ip_fib_main_table。那时候代码里到处都是直接访问这两个变量的逻辑。 后来内核开发者意识到这太不灵活了——如果你想加一张表?你得改代码重编译。于是他们重构了一下,把所有对表的操作统一收敛到了fib_get_table()方法。不管你有没有开启策略路由,也不管你有几张表,大家都是用fib_get_table(net, table_id)来拿表指针。
这种「统一访问」的感觉,就像是把「专用抽屉」变成了「带编号的储物柜系统」,无论柜子有多少个,拿钥匙开锁的动作是一模一样的。
策略路由开启时:255 张地图
当你开启了 CONFIG_IP_MULTIPLE_TABLES,世界变了。
内核不再局限于 Local 和 Main 两张表,而是支持最多 255 个路由表。 启动时,默认会初始化三张表:
- Local (255)
- Main (254)
- Default (253)
(注:关于 Default 表的具体用途和策略路由规则集 fib_rules 的详细交互,我们将在第 6 章深聊。这里先专注于「表」本身的管理机制。)
现在的问题是:这么多表,我怎么往里面塞东西?
管理员的操作界面:Netlink 与 IOCTL
作为一个内核工程师,你肯定熟悉 ip route 命令。但你知道它在内核眼里是什么吗?是一段 Netlink 消息。
1. 增删路由:ip route add/del
当你敲下:
ip route add 192.168.1.0/24 dev eth0
你的用户空间工具(iproute2)实际上通过 Netlink socket 发送了一个 RTM_NEWROUTE 消息给内核。
内核那边接招的是 inet_rtm_newroute() 方法(位于 net/ipv4/fib_frontend.c)。
- 它会解析你带来的参数(目标网段、出接口、优先级等)。
- 创建对应的
fib_info和fib_alias。 - 把它挂到对应的 FIB 表(默认是 Main 表)的哈希或 TRIE 结构里。
当你敲下 ip route del ... 时,流程类似,只是消息类型变成了 RTM_DELROUTE,内核由 inet_rtm_delroute() 接手,负责从 FIB 表中摘下对应的条目。
这里有个反直觉的细节,值得停下来想一想: 路由并不总是意味着「允许通行」。
你可以这样配:
ip route add prohibit 192.168.1.17 from 192.168.2.103
这行命令是在路由表里加了一个「禁止令」。内核在查路由时,如果匹配到了这一条,不仅不会转发数据包,还会直接丢弃,并回复一个 ICMP 「Packet Filtered」(被过滤)的错误消息。 这在防火墙或策略控制场景里非常有用——路由表本身就是一种规则集。
查看路由:ip route show
这对应的是 RTM_GETROUTE 消息,由 inet_dump_fib() 处理。
- 默认情况下,
ip route show只看 Main 表。 - 如果你想看 Local 表,必须显式指定:
ip route show table local。
2. 老派操作:route add/del
虽然 ip 命令是现在的标准,但 route 命令依然存在。它的内核接口是完全不同的另一条路——IOCTL。
route add发送SIOCADDRTIOCTL。route del发送SIOCDELRTIOCTL。
这两个 IOCTL 都由 ip_rt_ioctl() 方法处理(也在 net/ipv4/fib_frontend.c)。
这是为了兼容古老的网络工具留下的接口,虽然功能上和 Netlink 看起来差不多,但在内核内部,IOCTL 的处理路径通常比 Netlink 要死板一些。
3. 动态路由协议:BGP, OSPF 等
除了人手敲命令,路由表的另一个主要数据来源是路由守护进程。 这些是运行在骨干网路由器上的重型软件(如 Quagga, Bird, Zebra)。它们实现了 BGP、OSPF 等复杂协议。
这些进程在后台跑着,通过协议跟邻居路由器聊天。一旦发现网络拓扑变了(比如某条光纤断了),它们会立刻调用 Netlink API,狂发 RTM_NEWROUTE 或 RTM_DELROUTE 消息,瞬间更新内核里的 FIB 表。
对于内核来说,它不在乎这些路由是管理员手动敲进去的,还是 OSPF 协议算出来的——它们最终都是 fib_info 结构体,挂在一模一样的表里。
例外与微调:再次回到 FIB Exception
我们在这一节的开头提到,虽然我们正在讨论「表」级别的管理,但千万不要忘了上一节讲的 FIB nexthop exception。
- 如果是因为 ICMP Redirect 导致下一跳变了。
- 或者是因为 Path MTU Discovery 导致 MTU 变了。
这些变更不会去动那张庞大的、共享的 FIB 路由表。它们只会修改特定 fib_nh 头上的那张小哈希表(exception table)。
这是一种极佳的隔离设计:不要让个例污染全局规则。
如果把 PMTU 发现的路径直接改到全局 Main 表里,那么所有去往那个网段的流量可能都会被错误地套用一个还没验证过的 MTU 值,这简直是灾难。通过 exception 机制,内核只在「确实需要的那条流」上做微调。
小结
这一节我们把视角从微观的 fib_nh 拉到了宏观的 FIB Tables 架构上。
我们理解了:
- 双表模式:没有策略路由时,内核只认 Local 和 Main 两张表。
- 统一访问:通过
fib_get_table(),内核把对表的操作抽象化了,为支持多表打下了基础。 - 用户接口:
ip route命令背后的 Netlink 消息(RTM_NEWROUTE等)是管理员和路由守护进程操控 FIB 的方式。 - 路由即策略:路由条目不仅仅是导航,还可以是
prohibit这样的禁令。
现在的 FIB 体系已经比较清晰了:有表,有条目,有下一跳,还有针对下一跳的微调机制。 但就像我们在开头提到的「双网卡」场景一样,光有表还不够,我们还需要一套规则来决定「什么时候查哪张表」。
这就是下一章的主角——FIB Rules。那里才是策略路由真正的灵魂所在。