IP 路由相关机制深度解析
1. 路由类型与设备获取
在路由操作中,若本地表查找得到的源地址路由类型不是
RTN_LOCAL
类型,那么该表项是无效的。
RTN_LOCAL
表示找到的地址是配置在系统本地接口上的。当源地址的路由类型为
RTN_LOCAL
时,会通过调用宏
FIB_RES_DEV
(在第 162 行)获取
net_device
的引用。接着,在第 164 行增加
net_device
结构体中的使用计数,并在第 168 行返回
net_device
指针,最后调用
fib_res_put()
函数释放
fib_table
中的引用。
函数
__in_dev_get()
会返回
net_device
结构体中的
void *ip_ptr
元素,该元素指向
in_device
结构体实例。而
in_device
结构体包含重要元素
ifa_list
,它是
in_ifaddr
结构体类型,构成了一个 IP 地址链表。这在系统中非常重要,因为每个物理
net_device
都可以被分配别名 IP 地址和标签,例如
eth0:0
、
eth0:1
等。
2. 源 IP 地址选择
在 Linux 系统中,对于任何在主机系统上创建的 IP 数据包,在发送到目标地址之前,必须选择一个源地址。源地址信息对于目标系统至关重要,它能让目标系统知道数据包的来源,从而向源地址发送回复。如果不提供源地址信息,通信的一半将无法完成,回复也会丢失。
Linux 选择源地址遵循以下规则:
- 应用程序可能已经在使用套接字,此时源地址可能已经被选择,或者可以通过
bind()
调用请求源地址。
- 进行路由查找以找到目标路由。若找到目标路由,则检查路由中的
src
参数;若未找到,则由内核选择源地址进行通信。
- 若应用程序或路由查找都未提供源地址,内核会搜索为网络接口配置的 IP 地址列表。
inet_select_addr()
函数用于选择配置在网络设备上的 IP 地址(即源 IP)。该函数的输入参数包括
net_device
指针、非本地系统的 IP 地址和范围。如果输入的 IP 地址为零,则会选择入口设备上配置的任何主地址。从入口设备上配置的多个 IP 地址中选择源 IP 地址,是基于提供的输入范围和目标地址的位置。
范围可以是
RT_SCOPE_LINK
、
RT_SCOPE_HOST
、
RT_SCOPE_SITE
或
RT_SCOPE_UNIVERSE
。在第 724 行获取
in_device
实例的指针,然后使用内核提供的宏
for_primary_ifa
遍历为
net_device
配置的 IP 地址列表。该宏用于搜索网络设备的
in_device
实例中的
ifa_list
。
范围在选择源 IP 地址时起着重要作用。此函数会选择范围与目标地址范围相同或更小的入口地址。如果入口地址的范围大于目标地址的范围,则跳过该地址并继续搜索。另一个选项是在第 758 行搜索所有接口,以找到具有适当范围的地址。
以下是源地址选择规则的表格总结:
| 选择情况 | 规则 |
| ---- | ---- |
| 应用程序已使用套接字 | 源地址可能已选或可通过
bind()
调用请求 |
| 路由查找 | 找到目标路由则检查
src
参数,未找到则内核选择 |
| 应用和路由查找均未提供 | 内核搜索网络接口配置的 IP 地址列表 |
下面是
inet_select_addr()
函数选择源 IP 地址的流程图:
graph TD;
A[开始] --> B{输入 IP 地址是否为零};
B -- 是 --> C[选择入口设备主地址];
B -- 否 --> D[获取 in_device 实例指针];
D --> E[使用 for_primary_ifa 遍历 IP 地址列表];
E --> F{入口地址范围 <= 目标地址范围};
F -- 是 --> G[选择该地址];
F -- 否 --> H[跳过该地址,继续搜索];
G --> I[结束];
H --> E;
C --> I;
3. 路由范围
路由范围用于更精确地找到给定目标的路由。在
fn_hash_lookup()
函数中,会比较
fn->fn_scope
和
key->scope
字段,以检查找到的条目是否满足范围标准。范围值越高,需要为目标找到更具体的路由;范围值越低,路由属于目标网络。
常见的路由范围如下:
-
RT_SCOPE_HOST
:表示目标地址是本地主机。
-
RT_SCOPE_LINK
:表示目标地址是本地网络。
-
RT_SCOPE_NOWHERE
:表示没有到目标地址的路由。
-
RT_SCOPE_SITE
:表示站点内部的路由。
-
RT_SCOPE_UNIVERSE
:表示目标地址不是直接连接的,距离超过一跳。
4. 重要路由控制标志和类型
重要的路由控制标志和类型如下表所示:
| 标志/类型 | 含义 |
| ---- | ---- |
|
RTCF_LOCAL
| 表示路由特定于本地 IP 地址,用于从本地接口发起的路由 |
|
RTCF_MULTICAST
| 表示路由是到多播地址 |
|
RTCF_BROADCAST
| 表示路由是到广播地址 |
|
RTCF_ONLINK
| 表示是本地可达的目标 |
|
RTN_UNICAST
| 路由是网关或直接路由 |
|
RTN_LOCAL
| 路由是本地地址 |
|
RTN_BROADCAST
| 本地以广播方式接收和发送数据包 |
|
RTN_MULTICAST
| 表示这是一个多播路由 |
5.
fib_lookup()
函数
fib_lookup()
函数有两个版本:
-
无策略路由时
:当策略路由未启用时,
fib_lookup()
函数接收
struct rt_key
和
fib_result
作为输入参数。它会调用函数指针
tb_lookup
对本地表和主表进行查找,以在本地表或主表中找到目标匹配条目。
tb_lookup
函数指针会解析为
fn_hash_lookup()
函数。
fn_hash_lookup()
函数成功时返回 0,失败时返回非零值。只有当两个表都没有匹配项时,查找才会在第 159 行返回网络不可达错误。本地表的优先级高于主表。这里的查找只涉及本地表和主表,如果在内核中定义了策略路由,则可以配置多个路由表。
-
有策略路由时
:当内核中定义了策略路由(
CONFIG_IP_MULTIPLE_TABLES
)时,会调用另一个版本的
fib_lookup()
函数。在策略路由的情况下,可以配置多个路由表,并根据数据包的路由需求定义规则来选择特定的路由表。
在正常的单路由表路由中,路由决策基于目标地址。而配置了策略路由后,除了目标地址,还可以使用源地址、
tos
字段和
iptables
标记(
fwmark
)作为参数来定义数据包的规则。每个规则都有唯一的优先级,会按优先级升序对规则列表进行排序并搜索给定的规则。
系统中默认有三条规则:
-
local_rule
:优先级为 0,是最高优先级。在搜索规则列表以匹配给定规则时,该规则总是匹配任何规则,并在本地路由表中进行查找。因此,如果有针对本地系统的数据包,无需进一步的路由决策。本地表由内核维护,用于存储本地和广播地址。
-
main_rule
:优先级为 32766,是系统中的主路由表,总是进行匹配并搜索路由。
-
default_rule
:优先级为 32767,位于规则列表的末尾。
任何用户添加的规则都会插入到
local_rule
和
main_rule
之间。
以下是
fib_lookup()
函数在有策略路由时的流程图:
graph TD;
A[开始] --> B[获取 fib_rules_lock];
B --> C[遍历规则列表];
C --> D{是否匹配规则};
D -- 是 --> E{根据策略动作确定策略类型};
E -- RTN_UNICAST --> F[调用 fib_table_get() 获取路由表];
F --> G[调用 fn_hash_lookup() 查找路由];
G -- 成功 --> H[初始化 res->r 并增加引用计数];
H --> I[释放 fib_rules_lock];
I --> J[返回 0];
E -- 其他类型 --> K[返回错误];
D -- 否 --> C;
6.
fn_hash_lookup()
函数
fn_hash_lookup()
函数用于路由表查找,以匹配并找到数据包的目标路由。该函数每次在单个路由表中进行查找,查找前会获取适当的锁以读取表信息。
其输入参数如下:
-
tb
:用于查找数据包目标路由的路由表。
-
key
:在表中查找时使用的搜索键。
-
res
:若路由查找成功,则将其初始化为路由信息。
在查找前,
tb->tb_data
指针(在第 273 行)指向路由表(
fib_table
)关联的 FIB 哈希表(
fn_hash
)。需要在第 275 行以共享模式获取
fn_hash_lock
锁,该锁是一个读写自旋锁(
rwlock
)。
查找算法基于 LPM(最长前缀匹配)算法,此算法用于为目标找到最具体的路由。每个路由表(
fib_table
)都包含一个指向 FIB 哈希表(
fn_hash
)的关联指针,该 FIB 哈希表包含一个
fib
区域数组(
fz_zone
)和一个指向
fib
区域列表的指针(
fn_zone_list
)。基于 32 位的网络掩码(前缀)长度,网络掩码的每一位都关联一个区域,这就是
fn_hash
结构体中定义
fz_zones[33]
的原因。该区域数组的每个元素代表一个单独的区域,
fn_zone_list
指针指向最长网络掩码区域。因此,LPM 算法从最长网络掩码区域开始搜索,以找到更具体的数据包路由(更接近最终目标)。
IP 在其路由表中查找目标路由时按以下顺序执行步骤:
- 搜索匹配的主机地址(IP 地址)。
- 搜索匹配的网络地址。
- 搜索默认条目(默认条目是 ID 为 0 的网络地址)。
匹配的主机地址(主机的 IP 地址)总是在匹配网络地址之前使用。如果主机地址和网络地址都不匹配,则使用默认条目(默认路由),该默认路由是 ID 为 0 的网络地址,并且在路由表中为其定义了默认网关地址。
fn_zone[0]
表示默认条目(默认路由),
fn_zone[32]
表示更具体的路由。通过第 276 行的
for
循环遍历区域列表,从最长网络掩码开始,以找到更具体的路由。在开始搜索区域之前,使用搜索键的目标地址,通过调用第 278 行的
fz_key()
函数,将目标地址与区域的网络掩码进行按位与操作,构建一个测试键。该测试键用于在
fib_node
链中进行查找。
每个区域都有一个指向哈希表(
fz_hash
)的指针,该哈希表的每个桶都指向
fib_node
列表。为了计算要搜索的哈希表桶,调用第 280 行的
fz_chain()
函数。这是另一个
for
循环,用于根据
fz_chain()
函数返回的桶遍历
fib_node
列表。
fz_chain()
函数通过调用
fn_hash()
函数计算哈希值,以获取用于访问
fib_node
列表的哈希表桶。
fn_hash()
函数通过将
key.datum
值(在执行移位操作后)与
fz_hashmask
(0xf)进行按位与操作来计算哈希值,以获取一个哈希表桶。哈希表由 16 个桶组成,这就是
fz_hashmask
值始终为 0xf(15)的原因。
回到
fn_hash_lookup()
函数,在内层循环中获取要遍历的
fib_node
列表后,第一步是将
fz_key()
函数构建的测试键与
fib_node
列表中的键(
f->fn_key
,即一个地址)进行比较,这通过调用第 281 行的
fn_key_eq()
函数完成。
如果
fn_key_eq()
函数返回
true
,即键值匹配,则继续检查匹配的
fib_node
是否有效;如果
fn_key_eq()
函数返回
false
,即键不匹配,则调用第 282 行的
fn_leq_key()
函数,检查测试键值是否大于
fib_node
中的键值。如果是,则继续搜索下一个
fib_node
;否则,跳出内层
for
循环。这是因为列表中的
fib_nodes
按前缀降序排序。
如果控制流程到达第 287 行,并且在内核中定义了
CONFIG_IP_TOS
,并且
fib_node
的
tos
值不等于键的
tos
值,则丢弃该匹配并继续搜索。同时会检查
fib_node
的状态信息,判断其是否为
ACCESSED
或
ZOMBIE
。
ZOMBIE
节点当前未使用,与已删除的路由或失效的接口相关。如果状态为
ZOMBIE
(第 293 行),则丢弃搜索并继续。
fib_node
的范围应至少等于或大于键节点的范围,如果小于键的范围,则在第 296 行丢弃该匹配并继续搜索。
第 298 行调用
fib_semantic_match()
函数,用于检查匹配的
fib_node
是否可用,它会检查该路由是否可接受、下一跳是否存活,以及搜索键中提到的输出接口是否与下一跳关联的接口相同。如果这些条件中有任何一个不满足,
fib_semantic_match()
函数将返回错误。如果没有错误,则将
fib_result
结构体(
res
)初始化为
fn_type
、
fn_scope
和
fz->fz_order
,然后跳转到标签
out
(第 303 行),在第 312 行释放
fib_hash_lock
后返回错误码。
以下是
fn_hash_lookup()
函数的查找流程表格:
| 步骤 | 操作 |
| ---- | ---- |
| 1 | 获取
fn_hash_lock
锁 |
| 2 | 从最长网络掩码区域开始遍历区域列表 |
| 3 | 构建测试键 |
| 4 | 计算哈希表桶 |
| 5 | 遍历
fib_node
列表 |
| 6 | 比较测试键与
fib_node
键 |
| 7 | 检查
fib_node
状态和范围 |
| 8 | 调用
fib_semantic_match()
检查可用性 |
| 9 | 初始化
fib_result
结构体 |
| 10 | 释放
fn_hash_lock
并返回结果 |
下面是
fn_hash_lookup()
函数的流程图:
graph TD;
A[开始] --> B[获取 fn_hash_lock];
B --> C[遍历区域列表];
C --> D[构建测试键];
D --> E[计算哈希表桶];
E --> F[遍历 fib_node 列表];
F --> G{测试键与 fib_node 键匹配};
G -- 是 --> H{fib_node 有效};
H -- 是 --> I{fib_node tos 值匹配};
I -- 是 --> J{fib_node 状态非 ZOMBIE};
J -- 是 --> K{fib_node 范围 >= 键范围};
K -- 是 --> L[调用 fib_semantic_match()];
L -- 成功 --> M[初始化 fib_result 结构体];
M --> N[跳转到 out 标签];
N --> O[释放 fn_hash_lock];
O --> P[返回结果];
G -- 否 --> Q[调用 fn_leq_key()];
Q -- 测试键 > fib_node 键 --> F;
Q -- 测试键 <= fib_node 键 --> C;
H -- 否 --> F;
I -- 否 --> F;
J -- 否 --> F;
K -- 否 --> F;
L -- 失败 --> F;
总结
IP 路由是一个复杂而重要的网络技术,涉及到多个关键机制和函数。从路由类型的判断到设备的获取,从源 IP 地址的选择到不同路由范围的定义,再到
fib_lookup()
函数和
fn_hash_lookup()
函数的精确查找,每个环节都紧密相连,共同确保了 IP 数据包能够准确、高效地传输到目标地址。理解这些机制和函数的工作原理,对于网络工程师和开发者来说至关重要,它有助于优化网络配置、解决网络问题以及开发更高效的网络应用程序。
超级会员免费看

被折叠的 条评论
为什么被折叠?



