第 5 章 IPv4 路由子系统
5.8 快速参考面板(Quick Reference)
写代码的时候,IDE 里的自动补全很好用,但当你在大段内核代码里跳来跳去时,或者在 grep 结果里翻找时,你需要的是一个更直接的东西——一个贴在墙上的「速查表」。
这一节就是那张表。
这里没有故事,没有铺垫,只有我们在这一章里拆解过的那些核心数据结构、API 函数和宏定义。它们散落在 net/ipv4 的几个关键文件里,现在我把它们聚在一起,方便你在未来的调试(或者填坑)生涯中快速检索。
模块与文件清单
首先,我们把「案发现场」的位置再确认一遍。IPv4 路由子系统的实现主要集中在以下模块:
fib_frontend.c: FIB 的「前台」,处理用户空间来的请求(比如ip route add)。fib_trie.c: 核心查找逻辑,也就是那棵高效的 LC-trie 树住的地方。fib_semantics.c: 处理 FIB 条目的语义,比如fib_info的管理。route.c: 路由缓存的老家(虽然现代内核里缓存机制已经大变样,但核心依然在这里)。fib_rules.c: 策略路由的实现者。注意,这个模块只有在你开启了CONFIG_IP_MULTIPLE_TABLES时才会被编译进去——如果你需要基于源地址或别的条件来做路由决策,这就是你需要的开关。
代码之外,这几个头文件值得你加到书签里:
include/net/ip_fib.h: FIB 的核心定义。fib_lookup.h: 查找逻辑的头文件。include/net/route.h: 路由层与更高层的接口。
别忘了目的缓存(dst_entry)的通用实现,它住在 net/core/dst.c 和 include/net/dst.h 里。
核心 API 方法
以下是本章提到的关键函数。如果你在阅读源码或写自己的内核模块时需要和 FIB 打交道,这些大概率是你需要调用的接口。
路由表操作
-
int fib_table_insert(struct fib_table *tb, struct fib_config *cfg);- 做什么:向指定的 FIB 表(
tb)插入一条路由。 - 参数:
cfg包含了用户空间传下来的路由配置(目标地址、网关、掩码等)。 - 注意:这是 Netlink socket 触发
ip route add后最终落入内核的那一跳。
- 做什么:向指定的 FIB 表(
-
int fib_table_delete(struct fib_table *tb, struct fib_config *cfg);- 做什么:从指定的 FIB 表中删除一条路由。
- 对应命令:
ip route del。
-
struct fib_table *fib_trie_table(u32 id);- 做什么:分配并初始化一个基于 TRIE 结构的 FIB 路由表。如果你想创建一个自定义的路由表(不仅仅是
local或main),这是你要找的构造函数。
- 做什么:分配并初始化一个基于 TRIE 结构的 FIB 路由表。如果你想创建一个自定义的路由表(不仅仅是
FIB 条目管理
-
struct fib_info *fib_create_info(struct fib_config *cfg);- 做什么:根据配置
cfg构造一个fib_info对象。 - 机制:这是路由条目的「肉」。它会检查是否已经有相同的
fib_info存在(为了节省内存),如果没有才创建新的。
- 做什么:根据配置
-
void free_fib_info(struct fib_info *fi);- 做什么:释放一个
fib_info对象。 - 条件:只有当引用计数归零(
fib_dead标志被设置)时,它才会真正被释放,并递减全局计数器fib_info_cnt。
- 做什么:释放一个
-
void fib_alias_accessed(struct fib_alias *fa);- 做什么:标记一个
fib_alias条目为「已访问」。 - 细节:它只是简单地把
fa->fa_state设置为FA_S_ACCESSED。这在垃圾回收或统计时可能会用到,用来区分哪些路由是「热」的。
- 做什么:标记一个
查找与 TRIE 遍历
struct leaf *fib_find_node(struct trie *t, u32 key);- 做什么:在 TR 树
t中查找匹配key的节点。 - 返回:成功返回
leaf节点,失败返回NULL。这是 LPM(最长前缀匹配)算法在数据结构层面的直接体现。
- 做什么:在 TR 树
重定向与异常处理
-
void ip_rt_send_redirect(struct sk_buff *skb);- 做什么:发送一个 ICMPv4 Redirect 消息。
- 场景:当内核发现主机在走一条「冤枉路」(通过本机转发,但本机不是最佳下一跳)时,调用这个函数好心地告诉对方「别绕了,走那边」。
-
void __ip_do_redirect(struct rtable *rt, struct sk_buff *skb, struct flowi4 *fl4, bool kill_route);- 做什么:处理接收到的 ICMPv4 Redirect 消息。
- 核心逻辑:这就是创建 FIB nexthop exception (FNHE) 的地方。如果
kill_route为真,它会彻底干掉旧路由;否则,它会创建一个异常条目,让特定的流量绕过 FIB 表直奔新网关。
-
void update_or_create_fnhe(struct fib_nh *nh, __be32 daddr, __be32 gw, u32 pmtu, unsigned long expires);- 做什么:正如其名,更新或创建一个 FNHE。
- 参数:指定了下一跳
nh、目标地址daddr、新网关gw、PMTU 以及过期时间。这是 Redirect 和 PMTU 发现机制修改路由行为的统一入口。
度量值查询
u32 dst_metric(const struct dst_entry *dst, int metric);- 做什么:从
dst_entry中提取指定的度量值(比如 MTU、初始窗口等)。
- 做什么:从
核心宏定义
内核代码里充满了宏,这里挑几个我们在解析 FIB 逻辑时遇到过的关键角色。
FIB 查找结果提取
这些宏通常接收一个 fib_result 结构体作为参数,把查找结果里的关键字段抠出来。
FIB_RES_GW(res): 返回下一跳的网关地址 (nh_gw)。FIB_RES_DEV(res): 返回下一步要走的网络设备 (net_device)。FIB_RES_OIF(res): 返回输出接口的索引 (nh_oif)。FIB_RES_NH(res): 返回完整的fib_nh结构体。- 细节:如果开启了多路径路由,这里会根据
res->nh_sel作为索引,从fib_info的nexthop数组里选出正确的那一个。
- 细节:如果开启了多路径路由,这里会根据
设备行为检查
在决定「要不要转发」或「听不听话」之前,内核得先问问网卡的态度。
IN_DEV_FORWARD(in_dev): 检查该设备是否开启了 IP 转发。如果这个关了,它就是个安静的终点,不是路由器。IN_DEV_RX_REDIRECTS(in_dev): 检查该设备是否接收 ICMP Redirect。如果你要做路由器,通常会关掉这个,防止被别人的 Redirect 搞乱。IN_DEV_TX_REDIRECTS(in_dev): 检查该设备是否发送 ICMP Redirect。
TRIE 树结构遍历
IS_LEAF(node): 判断这个节点是不是叶子节点(终点)。IS_TNODE(node): 判断这个节点是不是内部节点(Trie Node,还要继续往下找)。
多路径路由迭代
change_nexthops(fi): 定义在fib_semantics.c里的一个宏。它提供了一个遍历fib_info里所有nexthop的循环机制。当你需要检查每一条可能的路径时,你会用到它。
关键数据表
表 5-1:路由度量值
内核不仅管路怎么走,还管路上的路况。这里有 15 种(RTAX_MAX)度量值,其中一些是专门给 TCP 用的。
| Linux 符号 | TCP 相关 (Y/N) | 含义 |
|---|---|---|
RTAX_UNSPEC | N | 未指定 |
RTAX_LOCK | N | 锁定度量值(防止被更新) |
RTAX_MTU | N | 路径最大传输单元 |
RTAX_WINDOW | Y | TCP 广播窗口大小 |
RTAX_RTT | Y | 往返时间 |
RTAX_RTTVAR | Y | 往返时间方差 |
RTAX_SSTHRESH | Y | 慢启动阈值 |
RTAX_CWND | Y | 拥塞窗口 |
RTAX_ADVMSS | Y | 对方 MSS (建议) |
RTAX_REORDERING | Y | 包重排序阈值 |
RTAX_HOPLIMIT | N | 跳数限制 |
RTAX_INITCWND | Y | 初始拥塞窗口 |
RTAX_FEATURES | N | 特性标志位 |
RTAX_RTO_MIN | Y | 最小重传超时时间 |
RTAX_INITRWND | Y | 初始接收窗口 |
(来源:include/uapi/linux/rtnetlink.h)
表 5-2:路由类型与错误码
当路由查找命中时,不同的 type 意味着不同的命运。这里列出了 fib_props 数组中的映射关系:特定路由类型对应的错误码和作用域。
| Linux 符号 | 错误码 | 作用域 | 含义 |
|---|---|---|---|
RTN_UNSPEC | 0 | RT_SCOPE_NOWHERE | 未指定(通常不出现在最终结果里) |
RTN_UNICAST | 0 | RT_SCOPE_UNIVERSE | 普通单播路由 |
RTN_LOCAL | 0 | RT_SCOPE_HOST | 本地地址 |
RTN_BROADCAST | 0 | RT_SCOPE_LINK | 广播地址 |
RTN_ANYCAST | 0 | RT_SCOPE_LINK | 任播地址 |
RTN_MULTICAST | 0 | RT_SCOPE_UNIVERSE | 组播路由 |
RTN_BLACKHOLE | -EINVAL | RT_SCOPE_UNIVERSE | 黑洞(静默丢弃,不报错) |
RTN_UNREACHABLE | -EHOSTUNREACH | RT_SCOPE_UNIVERSE | 不可达(丢弃并发送 ICMP 目标不可达) |
RTN_PROHIBIT | -EACCES | RT_SCOPE_UNIVERSE | 禁止(被管理员拒绝,发送 ICMP 禁止访问) |
RTN_THROW | -EAGAIN | RT_SCOPE_UNIVERSE | 也就是「继续查下一张表」(策略路由用) |
RTN_NAT | -EINVAL | RT_SCOPE_NOWHERE | NAT(旧用法) |
RTN_XRESOLVE | -EINVAL | RT_SCOPE_NOWHERE | 需要外部解析(如通过 daemon) |
路由标志位
当你敲下 route -n 或 ip route show 时,输出里那一串缩写字母(UG, UH)并不是乱码。它们是内核对这条路由的「批注」。
以下是常见标志位的含义,对应表 5-3 的示例输出:
U(Route is up): 路由是激活的。H(Target is a host): 目标是一个具体的主机(通常掩码是 255.255.255.255)。G(Use gateway): 使用网关(数据包不是直接发到目标网段,而是要经过中间人)。R(Reinstate route): 恢复动态路由(被路由守护进程重启)。D(Dynamically installed): 动态安装(由 redirect 或守护进程创建)。M(Modified): 已被修改(由 redirect 或守护进程修改)。A(Installed by addrconf): 由 addrconf 自动配置(通常是 IPv6 相关,但也出现在这里)。!(Reject route): 拒绝路由(对应RTN_PROHIBIT等类型)。
表 5-3:路由表示例
| Destination | Gateway | Genmask | Flags | Metric | Ref | Use | Iface |
|---|---|---|---|---|---|---|---|
| 169.254.0.0 | 0.0.0.0 | 255.255.0.0 | U | 1002 | 0 | 0 | eth0 |
| 192.168.3.0 | 192.168.2.1 | 255.255.255.0 | UG | 0 | 0 | 0 | eth1 |
解读:
- 第一行:去往链路本地地址
169.254.0.0/16的流量直接走eth0(没网关,所以没 G)。 - 第二行:去往
192.168.3.0/24的流量,需要先发给网关192.168.2.1,然后从eth1出去。
本章回响
这一章,我们像剥洋葱一样剥开了内核路由子系统。
最外层是 ip route 命令,那是你看到的;往里是 FIB 表,那是内核存储规则的地方;最深处是 LC-trie 树和 fib_lookup 算法,那是每一秒都在处理百万级流量的引擎。
如果你只记得本章的一件事,请记住这个:路由决策不是一个简单的匹配,而是一个从查找、到缓存、再到动态修正(Redirect/FNHE)的完整闭环。 表 5-1 的那些度量值,表 5-2 的那些错误码,以及那些以 fib_ 开头的结构体,都是为了让这个闭环既能跑得快,又能在网络拓扑发生变化时灵活地调整姿态。
下一章,我们将从「怎么走」转向「怎么发」。我们将离开路由层的冷静决策,进入邻居层(Neighbor Subsystem)的热烈握手——看看 ARP 和 ND 是如何把抽象的 IP 地址变成真实的以太网帧,真正把数据推到网线上的。
练习题
练习 1:understanding
题目:在 Linux 内核的路由查找过程中,fib_lookup() 函数是核心入口。请简述 fib_lookup() 函数通过输入参数 flowi4 和输出参数 fib_result 进行路由查找的基本流程,并说明当查找成功时,内核如何基于 fib_result 构建 dst_entry (目的缓存)。
答案与解析
答案:fib_lookup() 使用 flowi4(包含目标地址、源地址、TOS 等信息)作为键值,首先在 Local 表中查找,若失败则在 Main 表中进行最长前缀匹配(LPM)。查找成功后,它填充 fib_result 结构体(包含前缀长度、fib_info 指针、路由类型等)。随后,内核根据 fib_result 中的信息(如 type 是 RTN_LOCAL 还是 RTN_UNICAST)创建一个 dst_entry 对象(嵌入在 rtable 中),并设置其 input 或 output 回调函数(如 ip_local_deliver 或 ip_forward)。
解析:本题考察对 IPv4 路由子系统核心数据流的理解。flowi4 定义了查找的“钥匙”,fib_result 存储了查找的“结果”。fib_info 是路由条目的具体参数载体。查找成功后,内核必须将这些静态的 FIB 信息转化为动态可用的路由缓存对象(dst_entry),其中最重要的就是设置处理数据包的回调函数,这决定了数据包是本地接收、转发还是丢弃。
练习 2:application
题目:假设你是一名网络管理员,需要配置 Linux 服务器禁止来自 192.168.1.0/24 网段的流量访问 10.0.0.5。请写出实现这一策略的 ip route 命令,并结合 fib_props 和 RTN_PROHIBIT 的原理,解释内核收到匹配该规则的数据包时会返回什么错误码以及发送什么 ICMP 消息。
答案与解析
答案:命令:ip route add prohibit 10.0.0.5 from 192.168.1.0/24。
原理:内核中 fib_props 数组定义了不同路由类型的行为。RTN_PROHIBIT 类型对应的错误码是 -EACCES。当数据包匹配此规则时,路由查找返回该错误,内核随即调用 ip_error() 处理,丢弃数据包并向发送方回复 ICMP “Destination Unreachable” 消息,代码为 “Packet Filtered” (ICMP_PKT_FILTERED)。
解析:本题考察如何应用策略路由(Policy Routing)概念进行流量过滤。理解 RTN_PROHIBIT 是 fib_type 的一种,它不是简单地丢弃数据包,而是有特定的交互行为(发送 ICMP)。通过查找 fib_props 数组中 RTN_PROHIBIT 对应的 error 字段,可以确定具体的内核行为。
练习 3:thinking
题目:在早期的内核版本中(< 3.6),Linux 使用路由缓存来加速查找,但在 3.6 版本后将其移除,转而完全依赖 FIB TRIE。请从“性能”和“安全性/稳定性”两个维度,分析内核开发团队做出这一改动(采用 FIB TRIE 而非 Routing Cache)的主要原因。
答案与解析
答案:1. 性能:随着路由表规模的增长(互联网核心路由表条目极多),维护庞大的哈希缓存及其一致性(如路由失效更新)的开销变得非常大。LC-trie(基于最长前缀匹配的树结构)本身查找速度非常快(O(key length)),且内存效率高,使得额外的缓存层变得不再必要。
- 安全性与稳定性:路由缓存容易受到“Shadow Master”类 DoS 攻击。攻击者可以通过发送大量随机目标 IP 的数据包,迫使内核不断进行缓存未命中查找并填充缓存,耗尽系统内存和 CPU。移除缓存后,直接在 FIB TRIE 上查找,消除了这种基于缓存溢出的攻击面。
解析:本题考察对内核网络子系统演进的深度思考。这是一个经典的“用空间换时间”到“算法优化”的转变案例。理解这一点需要认识到:虽然缓存通常能加速访问,但在高动态变化或特定攻击场景下,缓存的管理成本(锁竞争、表项刷新)和脆弱性(易被攻击)可能超过其带来的收益。FIB TRIE 提供了足够高的查找效率,从而允许移除复杂的缓存逻辑。
要点提炼
Linux 内核通过 FIB(转发信息库) 与路由查找机制来决定数据包的去向,核心函数 fib_lookup() 根据目标地址等参数查询路由表,最终生成包含 input 和 output 函数指针的 dst_entry(路由缓存项),将复杂的查表过程转化为对特定回调函数(如 ip_local_deliver 或 ip_forward)的调用,从而实现本机接收与转发的逻辑分流。
fib_info 结构体 是路由条目在内核中的完整“身份证”,它封装了除目的地之外的所有元数据。它不仅记录了路由来源(如静态配置或内核生成)、作用域和优先级,还通过 fib_metrics 数组管理 MTU、RTT 等 TCP 性能参数。这种设计将路由的决策属性与物理路径分离,使得内核能通过引用计数高效管理路由条目的生命周期,并支持多路径路由配置。
路由并非总是意味着允许通行,内核通过 fib_props 映射表 将不同类型的路由(如 RTN_PROHIBIT)转化为具体的操作行为。当路由查找命中“禁止”类型时,内核不会静默丢包,而是依据配置触发 ICMP “目标不可达”或“过滤”消息,这种机制将路由表本身升级为一套高效的流量控制策略系统。
为了应对网络环境的动态变化,内核在 fib_nh(下一跳) 层面引入了 Exception(例外)机制。针对特定目标地址,内核利用哈希表记录 ICMP 重定向带来的网关变更或 PMTU 发现带来的 MTU 调整。这种“便签式”的微调避免了频繁修改庞大的全局路由表,实现了对个别路径的精准修正与隔离。
当多条路由指向相同目的地和物理路径但属性(如 TOS、优先级)不同时,内核采用 fib_alias 机制来优化内存。它允许多个轻量级的 fib_alias 结构体共享同一个存储实际路径信息的 fib_info,这种设计避免了为微小差异复制大量路由数据,显著提升了大规模路由表(如 BGP 场景)的内存利用率。