52、IP 路由相关机制深度解析

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 数据包能够准确、高效地传输到目标地址。理解这些机制和函数的工作原理,对于网络工程师和开发者来说至关重要,它有助于优化网络配置、解决网络问题以及开发更高效的网络应用程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值