Skip to content

Latest commit

 

History

History
2325 lines (1894 loc) · 79.7 KB

File metadata and controls

2325 lines (1894 loc) · 79.7 KB

Linux 内核 IP 路由子系统深度分析

基于 Linux 内核源码(主线 master 分支,commit 8a30aeb0d)


目录

  1. 总体架构概览
  2. FIB(转发信息库)核心数据结构
  3. LC-Trie 压缩前缀树(fib_trie)
  4. 路由查找流程(fib_lookup)
  5. 路由结果缓存:dst_entry 与 rtable
  6. 路由缓存的演化:从全局缓存到 per-CPU 替代方案
  7. Policy Routing(策略路由)
  8. ECMP(等价多路径路由)
  9. 邻居子系统(ARP/NDP)
  10. IPv6 路由子系统
  11. 路由通知机制(fib_notifier)
  12. Netlink/rtnetlink 与用户态交互
  13. 完整数据流图

1. 总体架构概览

Linux 内核的 IP 路由子系统是网络栈的核心组件,负责决定每个数据包如何被转发。其设计经历了数十年的演进,从早期的哈希路由缓存(Linux 3.6 前),到基于 LC-Trie 的 FIB 查找,再到现代的 per-CPU dst_entry 缓存机制。

用户空间
  ip route / ip rule (iproute2)
        |
        | Netlink (AF_NETLINK, NETLINK_ROUTE)
        v
+-------+--------------------------------------------------+
|                    rtnetlink 子系统                       |
|   net/core/rtnetlink.c  +  net/ipv4/fib_frontend.c       |
+------------------+---------------------------------------+
                   |
         +---------+---------+
         |                   |
   +-----v------+     +------v------+
   |  fib_rules |     |  fib_table  |
   | (策略路由)  |     | (路由表)    |
   +-----+------+     +------+------+
         |                   |
         |     fib_lookup()  |
         +--------+----------+
                  |
          +-------v-------+
          |   LC-Trie     |   net/ipv4/fib_trie.c
          | (key_vector)  |
          +-------+-------+
                  |
          +-------v-------+
          |  fib_alias    |   路由别名(同前缀多路由)
          |  fib_info     |   路由属性(协议、flags)
          |  fib_nh       |   下一跳信息
          +-------+-------+
                  |
          +-------v-------+
          |  dst_entry /  |   路由缓存结果
          |  rtable       |
          +-------+-------+
                  |
          +-------v-------+
          |  neighbour    |   ARP/NDP 邻居解析
          |  (arp_tbl)    |
          +-------+-------+
                  |
          +-------v-------+
          |  net_device   |   硬件层发送
          +---------------+

整个路由查找的关键路径可以概括为:

  1. 数据包进入,构建 flowi4(流标识)
  2. 调用 fib_lookup(),遍历策略规则,在对应路由表执行 fib_table_lookup()
  3. LC-Trie 查找返回 fib_result,选择下一跳(ECMP 支持多路径)
  4. 分配/复用 rtable(含 dst_entry),关联到 neighbour 条目
  5. 通过 neighbour->output() 发送数据包

2. FIB(转发信息库)核心数据结构

2.1 fib_table

定义位于 include/net/ip_fib.h,第 257-264 行:

struct fib_table {
    struct hlist_node   tb_hlist;
    u32                 tb_id;
    int                 tb_num_default;
    struct rcu_head     rcu;
    unsigned long      *tb_data;
    unsigned long       __data[];
};

fib_table 是路由表的顶层抽象。tb_id 标识路由表编号:

  • RT_TABLE_LOCAL = 255(本地地址路由)
  • RT_TABLE_MAIN = 254(主路由表)
  • RT_TABLE_DEFAULT= 253(默认路由表)

这些常量定义在 include/uapi/linux/rtnetlink.h 第 361-362 行。

tb_data 实际指向嵌入在 fib_table.__data[] 中的 struct trie,利用灵活数组成员将 trie 根节点与表头分配在同一块内存。

路由表哈希桶数量由编译选项控制(include/net/ip_fib.h 第 203-206 行):

#ifdef CONFIG_IP_MULTIPLE_TABLES
#define FIB_TABLE_HASHSZ 256
#else
#define FIB_TABLE_HASHSZ 2
#endif

未开启策略路由时只需 2 个桶(LOCAL + MAIN),开启后扩展为 256 个桶支持最多 2^32 个自定义路由表。

2.2 fib_info

fib_info 是多条路由共享的属性集合,定义于 include/net/ip_fib.h 第 136-163 行:

struct fib_info {
    struct hlist_node   fib_hash;       // 全局哈希表链接
    struct hlist_node   fib_lhash;      // 本地地址哈希
    struct list_head    nh_list;        // nexthop 对象链表
    struct net         *fib_net;
    refcount_t          fib_treeref;    // 被 trie 叶节点引用的计数
    refcount_t          fib_clntref;    // 被客户端(dst_entry)引用的计数
    unsigned int        fib_flags;
    unsigned char       fib_dead;
    unsigned char       fib_protocol;  // RTPROT_KERNEL / RTPROT_STATIC 等
    unsigned char       fib_scope;     // RT_SCOPE_UNIVERSE 等
    unsigned char       fib_type;      // RTN_UNICAST / RTN_BLACKHOLE 等
    __be32              fib_prefsrc;   // 首选源地址
    u32                 fib_tb_id;     // 所属路由表 ID
    u32                 fib_priority;  // metric
    struct dst_metrics *fib_metrics;   // MTU/RTT/窗口等路径指标
    int                 fib_nhs;       // 下一跳数量(ECMP)
    bool                fib_nh_is_v6;  // 是否含 IPv6 下一跳
    bool                nh_updated;
    bool                pfsrc_removed;
    struct nexthop     *nh;            // 新式 nexthop 对象(内核 5.x+)
    struct rcu_head     rcu;
    struct fib_nh       fib_nh[] __counted_by(fib_nhs); // 下一跳数组
};

关键点:

  • fib_treeref 记录被 trie 引用次数,fib_clntref 记录被 dst_entry 引用次数,两者用 refcount_t 保证原子性
  • fib_metrics 通过 COW(写时复制)机制共享,减少内存占用
  • fib_nh[] 是 C99 灵活数组,ECMP 路由时包含多个下一跳

2.3 fib_nh 与 fib_nh_common

IPv4 下一跳结构 fib_nh 定义于 include/net/ip_fib.h 第 107-128 行:

struct fib_nh {
    struct fib_nh_common    nh_common;  // 公共下一跳字段
    struct hlist_node       nh_hash;
    struct fib_info        *nh_parent;
    __be32                  nh_saddr;   // 源地址(经 saddr 缓存优化)
    int                     nh_saddr_genid;
    // 宏别名:fib_nh_dev, fib_nh_oif, fib_nh_gw4, fib_nh_weight ...
};

公共基类 fib_nh_common(第 83-105 行)同时被 IPv4 fib_nh 和 IPv6 fib6_nh 继承:

struct fib_nh_common {
    struct net_device      *nhc_dev;
    int                     nhc_oif;       // 出接口索引
    unsigned char           nhc_scope;
    u8                      nhc_family;
    u8                      nhc_gw_family; // 网关地址族
    unsigned char           nhc_flags;     // RTNH_F_DEAD 等
    struct lwtunnel_state  *nhc_lwtstate;  // 轻量隧道状态

    union {
        __be32          ipv4;
        struct in6_addr ipv6;
    } nhc_gw;                              // 网关地址(支持 v4/v6)

    int                     nhc_weight;    // ECMP 权重
    atomic_t                nhc_upper_bound; // ECMP 随机选路上界

    // 路由缓存:per-CPU 出方向 rtable + 入方向 rtable
    struct rtable __rcu * __percpu *nhc_pcpu_rth_output;
    struct rtable __rcu            *nhc_rth_input;
    struct fnhe_hash_bucket __rcu  *nhc_exceptions; // PMTU 异常哈希表
};

nhc_pcpu_rth_output 是 per-CPU 指针数组,这是取代旧全局路由缓存的核心机制(详见第 6 节)。

2.4 fib_result

路由查找的输出结果,定义于 include/net/ip_fib.h 第 173-185 行:

struct fib_result {
    __be32              prefix;     // 匹配的网络前缀
    unsigned char       prefixlen;  // 前缀长度
    unsigned char       nh_sel;     // 选中的下一跳序号(ECMP)
    unsigned char       type;       // 路由类型
    unsigned char       scope;      // 路由范围
    u32                 tclassid;   // TC 分类 ID
    dscp_t              dscp;       // DSCP 值
    struct fib_nh_common *nhc;      // 选中的下一跳公共结构
    struct fib_info     *fi;        // 路由信息
    struct fib_table    *table;     // 所属路由表
    struct hlist_head   *fa_head;   // 叶节点别名链表头
};

2.5 fib_alias

fib_alias 是连接 trie 叶节点与 fib_info 的桥梁,定义于 net/ipv4/fib_lookup.h 第 11-24 行:

struct fib_alias {
    struct hlist_node   fa_list;    // 挂载到 key_vector.leaf 哈希链
    struct fib_info    *fa_info;    // 指向共享的路由属性
    dscp_t              fa_dscp;    // DSCP/TOS 选择器
    u8                  fa_type;    // RTN_UNICAST 等
    u8                  fa_state;   // FA_S_ACCESSED 访问标记
    u8                  fa_slen;    // 前缀有效位长(KEYLENGTH - prefixlen)
    u32                 tb_id;      // 路由表 ID
    s16                 fa_default; // 默认路由标记
    u8                  offload;    // 是否已卸载到硬件
    u8                  trap;
    u8                  offload_failed;
    struct rcu_head     rcu;
};

同一个 trie 叶节点(相同目的前缀)可以挂载多个 fib_alias,以支持:

  • 相同前缀不同 TOS/DSCP 的路由
  • 相同前缀不同 metric 的路由(按 metric 排序,fib_lookup 取最优)

3. LC-Trie 压缩前缀树(fib_trie)

3.1 算法背景

Linux 的 IPv4 FIB 使用 LC-Trie(Level-Compressed Trie,水平压缩字典树),源自 Stefan Nilsson 和 Gunnar Karlsson 1999 年的论文《IP-address lookup using LC-tries》。该算法通过两种压缩方式减少内存和查找步数:

  1. 路径压缩(Path Compression):跳过只有一个子节点的中间节点
  2. 水平压缩(Level Compression):将多层单分支节点合并为一个多分支节点(bits 增大)

代码文件开头(net/ipv4/fib_trie.c 第 1-37 行)记录了原始论文出处和历史贡献者,包括 Robert Olsson、Stefan Nilsson、Jens Laas 等研究者,以及 David S. Miller、Stephen Hemminger、Paul McKenney 等内核开发者。

3.2 核心数据结构

定义于 net/ipv4/fib_trie.c 第 121-141 行:

struct key_vector {
    t_key key;              // 路由键(IPv4 地址,大端序转换为主机序)
    unsigned char pos;      // 本节点在 key 中的起始位位置(低位)
    unsigned char bits;     // 本节点分叉的位数(bits==0 为叶节点)
    unsigned char slen;     // 后缀有效长度(用于前缀匹配)
    union {
        struct hlist_head leaf;   // 叶节点:挂载 fib_alias 链表
        DECLARE_FLEX_ARRAY(struct key_vector __rcu *, tnode); // 内部节点:子节点数组
    };
};

struct tnode {
    struct rcu_head         rcu;
    t_key empty_children;   // 空子节点数
    t_key full_children;    // 满(非空)子节点数
    struct key_vector __rcu *parent;
    struct key_vector       kv[1]; // 内嵌 key_vector
#define tn_bits kv[0].bits
};

节点判断宏(第 117-119 行):

#define IS_TRIE(n)  ((n)->pos >= KEYLENGTH)  // 根节点
#define IS_TNODE(n) ((n)->bits)              // 内部节点
#define IS_LEAF(n)  (!(n)->bits)             // 叶节点

KEYLENGTH 定义为 8 * sizeof(t_key),即 32(第 112 行),t_keyunsigned int

3.3 节点的位域语义

对于一个内部节点 n 及其父节点 tp(源码注释,第 231-288 行描述了详细的位域划分):

Key 的 32 位被划分为以下区域(从高到低):

  31        tp->pos+tp->bits  tp->pos  n->pos+n->bits  n->pos   0
  |---- i ---|------- N ------|----- S ------|------ C ------|-- u --|

  i : 已知且高于父节点处理范围的位(忽略)
  N : 父节点 tp 的索引位(用于在 tp 的子数组中定位 n)
  S : 跳过位(路径压缩,n 中所有键的这些位相同)
  C : 当前节点 n 的索引位(用于在 n 的子数组中定位子节点)
  u : 未知位(尚未处理)

3.4 trie 结构示意

                    trie (root, IS_TRIE)
                         |
              key_vector (pos=24, bits=8) TNODE
             /          |          \
     [0x0A...]      [0xC0...]   [0xFF...]
       LEAF           TNODE       LEAF
    10.0.0.0/8      192.0.0.0   ...
        |           /        \
    fib_alias   [0xC0A8]  [0xC0A9]
    fib_alias    LEAF       LEAF
               192.168.x  192.169.x

3.5 索引计算

get_cindex 宏(第 219 行)计算在当前节点的子数组索引:

#define get_cindex(key, kv) (((key) ^ (kv)->key) >> (kv)->pos)

通过 XOR 和右移,提取 key 中对应节点分叉位的值,直接作为子数组下标。

3.6 动态调整(inflate/halve)

Trie 在插入/删除时自动调整形态(第 290-293 行):

static const int halve_threshold       = 25;   // 填充率 <25% 时收缩
static const int inflate_threshold     = 50;   // 填充率 >50% 时扩展
static const int halve_threshold_root  = 15;   // 根节点收缩阈值
static const int inflate_threshold_root= 30;   // 根节点扩展阈值
  • inflate:将 bits 增加 1,子节点数组翻倍,降低树深度,加速查找
  • halve:将 bits 减少 1,合并相邻子节点,释放内存

节点内存分配(第 315-330 行):

static struct tnode *tnode_alloc(int bits)
{
    size_t size = TNODE_SIZE(1ul << bits);
    if (size <= PAGE_SIZE)
        return kzalloc(size, GFP_KERNEL);
    else
        return vzalloc(size);  // 超过一页使用虚拟内存分配
}

叶节点从专用 slab trie_leaf_kmem 分配(第 187 行),内部节点根据大小选择 kzallocvzalloc

3.7 内存同步控制

sysctl_fib_sync_mem(第 182-184 行)控制 RCU 同步的内存阈值:

unsigned int sysctl_fib_sync_mem     = 512 * 1024;
unsigned int sysctl_fib_sync_mem_min = 64  * 1024;
unsigned int sysctl_fib_sync_mem_max = 64  * 1024 * 1024;

当待释放的节点内存超过此阈值时,执行 synchronize_rcu() 而非 call_rcu(),避免大量脏内存积压。这在无抢占(PREEMPT_NONE)配置中尤为重要。


4. 路由查找流程(fib_lookup)

4.1 fib_lookup 的两个版本

根据是否开启策略路由(CONFIG_IP_MULTIPLE_TABLES),fib_lookup 有两条代码路径,定义于 include/net/ip_fib.h

无策略路由版本(第 316-334 行):

static inline int fib_lookup(struct net *net, const struct flowi4 *flp,
                              struct fib_result *res, unsigned int flags)
{
    struct fib_table *tb;
    int err = -ENETUNREACH;

    rcu_read_lock();
    tb = fib_get_table(net, RT_TABLE_MAIN);
    if (tb)
        err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
    if (err == -EAGAIN)
        err = -ENETUNREACH;
    rcu_read_unlock();
    return err;
}

策略路由版本(第 374-398 行,__fib_lookup 的快速路径):

static inline int fib_lookup(struct net *net, struct flowi4 *flp,
                              struct fib_result *res, unsigned int flags)
{
    flags |= FIB_LOOKUP_NOREF;
    if (net->ipv4.fib_has_custom_rules)
        return __fib_lookup(net, flp, res, flags); // 走规则链

    rcu_read_lock();
    res->tclassid = 0;
    tb = rcu_dereference_rtnl(net->ipv4.fib_main);
    if (tb)
        err = fib_table_lookup(tb, flp, res, flags);
    if (!err)
        goto out;
    tb = rcu_dereference_rtnl(net->ipv4.fib_default);
    if (tb)
        err = fib_table_lookup(tb, flp, res, flags);
    // ...
}

关键优化:当没有自定义策略规则时(fib_has_custom_rules == false),直接查主表和默认表,绕过规则链遍历,大幅降低正常转发的开销。

4.2 fib_table_lookup:LC-Trie 查找的三个阶段

定义于 net/ipv4/fib_trie.c 第 1420-1625 行,是整个路由查找的核心:

输入: fib_table, flowi4 (含目的地址)
输出: fib_result

阶段 1: 下行遍历 (Step 1 - Travel to longest prefix match)
阶段 2: 回溯查找最长匹配 (Step 2 - Backtracing)
阶段 3: 处理叶节点,选择路由 (Step 3 - Process the leaf)

查找入口(第 1420-1444 行):

int fib_table_lookup(struct fib_table *tb, const struct flowi4 *flp,
                     struct fib_result *res, int fib_flags)
{
    struct trie *t = (struct trie *) tb->tb_data;
    const t_key key = ntohl(flp->daddr); // 目的地址转换为主机序
    struct key_vector *n, *pn;
    // ...
    pn = t->kv;
    cindex = 0;
    n = get_child_rcu(pn, cindex);
    if (!n) {
        trace_fib_table_lookup(tb->tb_id, flp, NULL, -EAGAIN);
        return -EAGAIN;
    }
    // ...
}

阶段 1:下行遍历(第 1447-1482 行):

for (;;) {
    index = get_cindex(key, n);
    // 超出范围说明在跳过位上有不匹配
    if (index >= (1ul << n->bits))
        break;

    if (IS_LEAF(n))
        goto found;

    if (n->slen > n->pos) {
        pn = n;         // 记录最后一个有前缀的内部节点(用于回溯)
        cindex = index;
    }

    n = get_child_rcu(n, index);
    if (unlikely(!n))
        goto backtrace;
}

阶段 2:回溯(第 1484-1542 行):

当下行遍历找不到精确匹配时,回溯到父节点逐位剥离,寻找最长前缀匹配(LPM):

while (!cindex) {
    t_key pkey = pn->key;
    if (IS_TRIE(pn))
        return -EAGAIN;  // 已回溯到根,无匹配
    pn = node_parent_rcu(pn);
    cindex = get_index(pkey, pn);
}
cindex &= cindex - 1;  // 清除最低位,实现回溯
cptr = &pn->tnode[cindex];

阶段 3:叶节点处理(第 1544-1616 行):

hlist_for_each_entry_rcu(fa, &n->leaf, fa_list) {
    struct fib_info *fi = fa->fa_info;

    if (index >= (1ul << fa->fa_slen))  // 前缀长度不匹配
        continue;
    if (fa->fa_dscp && !fib_dscp_masked_match(fa->fa_dscp, flp))
        continue;
    if (READ_ONCE(fi->fib_dead))
        continue;
    if (fa->fa_info->fib_scope < flp->flowi4_scope)
        continue;

    fib_alias_accessed(fa);  // 设置 FA_S_ACCESSED 标记
    err = fib_props[fa->fa_type].error;
    // ...

    for (nhsel = 0; nhsel < fib_info_num_path(fi); nhsel++) {
        nhc = fib_info_nhc(fi, nhsel);
        if (!fib_lookup_good_nhc(nhc, fib_flags, flp))
            continue;
        // 填充 fib_result
        res->prefix    = htonl(n->key);
        res->prefixlen = KEYLENGTH - fa->fa_slen;
        res->nh_sel    = nhsel;
        res->nhc       = nhc;
        res->fi        = fi;
        res->table     = tb;
        res->fa_head   = &n->leaf;
        return err;
    }
}

4.3 查找性能分析

  • 时间复杂度:O(W) 最坏情况,W 为地址长度(32 位),实际由于路径压缩通常只需 3-5 次内存访问
  • RCU 保护:整个查找路径使用 RCU 读锁(rcu_read_lock),无需持有写锁,查找完全无锁化
  • Per-CPU 统计CONFIG_IP_FIB_TRIE_STATS 下通过 this_cpu_inc 统计 gets/backtrack 等指标(第 1424-1425、1442-1443 行)
fib_table_lookup 查找路径示意(目的地址 10.1.2.3):

  key = ntohl(0x0A010203) = 0x0A010203

  trie root
    -> pos=24, bits=8  index = key >> 24 = 0x0A
    -> [0x0A] = TNODE (pos=16, bits=8)
       index = (key >> 16) & 0xFF = 0x01
       -> [0x01] = LEAF (key=0x0A010000)
          -> fa_slen=16 (匹配 10.1.0.0/16)
          -> fa_slen=8  (匹配 10.0.0.0/8,备用)
          -> fib_result: prefix=10.1.0.0, prefixlen=16

5. 路由结果缓存:dst_entry 与 rtable

5.1 dst_entry

dst_entry 是目的缓存的通用基类,位于 include/net/dst.h,包含路由决策的缓存结果,是连接路由子系统与网络层的关键接口。

dst_ops 是其操作函数表,IPv4 版本定义于 net/ipv4/route.c 第 154-168 行:

static struct dst_ops ipv4_dst_ops = {
    .family             = AF_INET,
    .check              = ipv4_dst_check,      // 验证路由是否仍有效
    .default_advmss     = ipv4_default_advmss, // 建议 MSS
    .mtu                = ipv4_mtu,            // 获取路径 MTU
    .cow_metrics        = ipv4_cow_metrics,    // 写时复制 metrics
    .destroy            = ipv4_dst_destroy,    // 销毁回调
    .negative_advice    = ipv4_negative_advice,// 负向建议(重定向等)
    .link_failure       = ipv4_link_failure,   // 链路失败处理
    .update_pmtu        = ip_rt_update_pmtu,   // 更新 PMTU
    .redirect           = ip_do_redirect,      // ICMP 重定向处理
    .local_out          = __ip_local_out,      // 本地发出
    .neigh_lookup       = ipv4_neigh_lookup,   // 邻居查找
    .confirm_neigh      = ipv4_confirm_neigh,  // 确认邻居可达
};

5.2 rtable

rtable 是 IPv4 路由查找的具体结果结构,定义于 include/net/route.h 第 57-78 行:

struct rtable {
    struct dst_entry    dst;        // 必须是第一个成员

    int                 rt_genid;   // 路由生成号(用于失效检测)
    unsigned int        rt_flags;   // RTCF_LOCAL / RTCF_BROADCAST 等
    __u16               rt_type;    // RTN_UNICAST / RTN_LOCAL 等
    __u8                rt_is_input;// 1=入方向路由,0=出方向路由
    __u8                rt_uses_gateway;

    int                 rt_iif;     // 入接口索引

    u8                  rt_gw_family;
    union {
        __be32          rt_gw4;    // IPv4 网关地址
        struct in6_addr rt_gw6;   // IPv6 网关地址(v4-mapped 路由)
    };

    u32                 rt_mtu_locked:1,
                        rt_pmtu:31; // 路径 MTU 缓存
};

dst 必须是第一个成员,以支持零开销的 container_of 转换:

// include/net/route.h 第 284 行
BUILD_BUG_ON(offsetof(struct rtable, dst) != 0);

辅助宏和函数(第 80-106 行):

#define dst_rtable(_ptr) container_of_const(_ptr, struct rtable, dst)

static inline bool rt_is_input_route(const struct rtable *rt)
{
    return rt->rt_is_input != 0;
}

static inline __be32 rt_nexthop(const struct rtable *rt, __be32 daddr)
{
    if (rt->rt_gw_family == AF_INET)
        return rt->rt_gw4;
    return daddr;  // 直连路由,下一跳即目的地址
}

5.3 路由有效性检查

rt_genid 是路由失效的核心机制(net/ipv4/route.c 第 396-404 行):

static inline bool rt_is_expired(const struct rtable *rth)
{
    bool res;
    rcu_read_lock();
    res = rth->rt_genid != rt_genid_ipv4(dev_net_rcu(rth->dst.dev));
    rcu_read_unlock();
    return res;
}

当路由表发生变化时,rt_cache_flush() 通过递增 genid 使所有缓存的 rtable 失效:

void rt_cache_flush(struct net *net)
{
    rt_genid_bump_ipv4(net);  // 原子递增全局 genid
}

每个 rtable 在创建时记录当前 genid,下次使用时通过比较确认是否仍然有效。这种"懒失效"机制避免了遍历所有缓存条目的开销。

5.4 邻居查找与 rtable 的集成

ipv4_neigh_lookupnet/ipv4/route.c 第 412-438 行)是 dst_ops.neigh_lookup 的 IPv4 实现:

static struct neighbour *ipv4_neigh_lookup(const struct dst_entry *dst,
                                            struct sk_buff *skb,
                                            const void *daddr)
{
    const struct rtable *rt = container_of(dst, struct rtable, dst);
    struct neighbour *n;

    if (likely(rt->rt_gw_family == AF_INET)) {
        n = ip_neigh_gw4(dev, rt->rt_gw4);   // 查找网关的 ARP 条目
    } else if (rt->rt_gw_family == AF_INET6) {
        n = ip_neigh_gw6(dev, &rt->rt_gw6);  // v4-in-v6 场景
    } else {
        __be32 pkey = skb ? ip_hdr(skb)->daddr : *((__be32 *) daddr);
        n = ip_neigh_gw4(dev, pkey);          // 直连路由:查目的地址
    }
    // ...
}

6. 路由缓存的演化:从全局缓存到 per-CPU 替代方案

6.1 历史:全局路由缓存的消亡(Linux 3.6)

Linux 3.6 之前,内核维护了一个全局的路由结果哈希缓存(Route Cache),以 (src, dst, tos, oif) 为键缓存路由查找结果。随着多核处理器的普及,这个全局缓存成为严重的锁竞争热点,在高并发场景下性能急剧下降。

David S. Miller 在 2012 年彻底移除了这个缓存,改为"无全局缓存"架构:每次查找都直接遍历 LC-Trie。

net/ipv4/route.c/proc/net/rt_cache 的实现现在返回空表(第 200-232 行),只保留格式行,这是路由缓存被移除的直接证据:

static int rt_cache_seq_show(struct seq_file *seq, void *v)
{
    if (v == SEQ_START_TOKEN)
        seq_printf(seq, "%-127s\n", "Iface\tDestination\tGateway ...");
    return 0;
    // 不再输出任何实际条目
}

6.2 现代替代方案:多层 per-CPU 缓存

虽然全局缓存被移除,但内核引入了多层轻量级替代机制:

层次 1:per-CPU nhc_pcpu_rth_output(出方向缓存)

fib_nh_common.nhc_pcpu_rth_output 是最重要的缓存,定义于 include/net/ip_fib.h 第 102 行:

struct rtable __rcu * __percpu *nhc_pcpu_rth_output;

这是一个 per-CPU 的 rtable 指针数组。出方向数据包在首次建立路由后,将 rtable 存储在对应 CPU 的缓存槽中。后续相同下一跳的数据包直接读取,由于是 per-CPU 访问,完全无锁。

层次 2:nhc_rth_input(入方向缓存)

struct rtable __rcu *nhc_rth_input;

为入方向数据包缓存路由决策结果,使用 RCU 保护。

层次 3:fnhe_hash_bucket(PMTU/重定向异常缓存)

fib_nh_common.nhc_exceptions 指向哈希表,缓存 PMTU 黑洞、ICMP 重定向等异常路由:

// include/net/ip_fib.h 第 61-73 行
struct fib_nh_exception {
    struct fib_nh_exception __rcu *fnhe_next;
    int             fnhe_genid;
    __be32          fnhe_daddr;    // 异常目的地址
    u32             fnhe_pmtu;     // 缓存的 PMTU
    bool            fnhe_mtu_locked;
    __be32          fnhe_gw;       // 覆盖的网关(重定向)
    unsigned long   fnhe_expires;
    struct rtable __rcu *fnhe_rth_input;
    struct rtable __rcu *fnhe_rth_output;
    unsigned long   fnhe_stamp;
    struct rcu_head rcu;
};

#define FNHE_HASH_SHIFT  11
#define FNHE_HASH_SIZE   (1 << FNHE_HASH_SHIFT)  // 2048 个桶
#define FNHE_RECLAIM_DEPTH 5

缓存失效机制总结

路由表变更(fib_table_insert / fib_table_delete)
    |
    v
call_fib_entry_notifiers()  --> 通知 switchdev 等使用者
    |
    v
rt_cache_flush(net)
    |
    v
rt_genid_bump_ipv4(net)      // 原子递增 net->ipv4.rt_genid
    |
    v
下次使用 rtable 时:
  ipv4_dst_check() 或 rt_is_expired()
    ├── rt->rt_genid != 当前 genid --> 返回 NULL,触发重新查找
    └── genid 匹配 --> 路由有效,继续使用

7. Policy Routing(策略路由)

7.1 策略路由概述

策略路由(Policy-Based Routing)允许根据源地址、DSCP/TOS、防火墙标记、入接口等多种条件(而非仅目的地址)选择路由表。通过 ip rule 命令管理,内核由 CONFIG_IP_MULTIPLE_TABLES 控制编译。

7.2 fib4_rule 数据结构

定义于 net/ipv4/fib_rules.c 第 36-50 行:

struct fib4_rule {
    struct fib_rule     common;     // 通用规则基类(含优先级、动作等)
    u8                  dst_len;    // 目的地址匹配长度
    u8                  src_len;    // 源地址匹配长度
    dscp_t              dscp;       // DSCP 匹配值
    dscp_t              dscp_mask;
    u8                  dscp_full:1;// 使用完整 DSCP 还是 TOS 语义
    __be32              src;        // 源地址
    __be32              srcmask;
    __be32              dst;        // 目的地址
    __be32              dstmask;
#ifdef CONFIG_IP_ROUTE_CLASSID
    u32                 tclassid;   // TC 分类 ID
#endif
};

fib_rule(通用基类)包含规则优先级、动作类型(FR_ACT_TO_TBL / FR_ACT_UNREACHABLE 等)、防火墙标记、入/出接口、L3 设备(VRF)等。

7.3 __fib_lookup:规则链遍历

当存在自定义规则时,__fib_lookup 遍历规则链(net/ipv4/fib_rules.c 第 84-108 行):

int __fib_lookup(struct net *net, struct flowi4 *flp,
                  struct fib_result *res, unsigned int flags)
{
    struct fib_lookup_arg arg = {
        .result = res,
        .flags  = flags,
    };

    // 更新 l3mdev(VRF)相关的 oif/iif
    l3mdev_update_flow(net, flowi4_to_flowi(flp));

    err = fib_rules_lookup(net->ipv4.rules_ops,
                            flowi4_to_flowi(flp), 0, &arg);
    if (err == -ESRCH)
        err = -ENETUNREACH;
    return err;
}

7.4 fib4_rule_match:规则匹配(第 180-200 行)

INDIRECT_CALLABLE_SCOPE int fib4_rule_match(struct fib_rule *rule,
                                             struct flowi *fl, int flags)
{
    struct fib4_rule *r = (struct fib4_rule *) rule;
    struct flowi4 *fl4 = &fl->u.ip4;
    __be32 daddr = fl4->daddr;
    __be32 saddr = fl4->saddr;

    if (((saddr ^ r->src) & r->srcmask) ||
        ((daddr ^ r->dst) & r->dstmask))
        return 0;

    // DSCP 匹配(支持完整 DSCP 和传统 TOS 两种语义)
    if (r->dscp_full && (r->dscp ^ fl4->flowi4_dscp) & r->dscp_mask)
        return 0;
    // ...
}

7.5 fib4_rule_action:查找对应路由表(第 111-145 行)

INDIRECT_CALLABLE_SCOPE int fib4_rule_action(struct fib_rule *rule,
                                              struct flowi *flp, int flags,
                                              struct fib_lookup_arg *arg)
{
    switch (rule->action) {
    case FR_ACT_TO_TBL:      break;
    case FR_ACT_UNREACHABLE: return -ENETUNREACH;
    case FR_ACT_PROHIBIT:    return -EACCES;
    case FR_ACT_BLACKHOLE:   return -EINVAL;
    }

    rcu_read_lock();
    tb_id = fib_rule_get_table(rule, arg);
    tbl = fib_get_table(rule->fr_net, tb_id);
    if (tbl)
        err = fib_table_lookup(tbl, &flp->u.ip4,
                               (struct fib_result *)arg->result,
                               arg->flags);
    rcu_read_unlock();
    return err;
}

7.6 fib4_rule_suppress:前缀长度过滤(第 147-178 行)

suppress 机制允许策略规则拒绝前缀长度不足的路由结果,常用于防止路由环路(如 VPN 场景):

INDIRECT_CALLABLE_SCOPE bool fib4_rule_suppress(struct fib_rule *rule,
                                                  int flags,
                                                  struct fib_lookup_arg *arg)
{
    // 前缀长度不够短,拒绝此结果
    if (result->prefixlen <= rule->suppress_prefixlen)
        goto suppress_route;

    // 接口组限制
    if (rule->suppress_ifgroup != -1 && dev &&
        dev->group == rule->suppress_ifgroup)
        goto suppress_route;

    return false;
// ...
}

7.7 默认规则链

系统启动时默认的三条规则(按优先级):

优先级   动作             路由表
------   ----             ------
   0     lookup           local   (RT_TABLE_LOCAL = 255)
  32766  lookup           main    (RT_TABLE_MAIN  = 254)
  32767  lookup           default (RT_TABLE_DEFAULT = 253)

fib4_rule_default() 检查规则是否为默认规则(第 61-70 行),供快速路径优化使用:

bool fib4_rule_default(const struct fib_rule *rule)
{
    if (!fib4_rule_matchall(rule) || rule->action != FR_ACT_TO_TBL ||
        rule->l3mdev)
        return false;
    if (rule->table != RT_TABLE_LOCAL && rule->table != RT_TABLE_MAIN &&
        rule->table != RT_TABLE_DEFAULT)
        return false;
    return true;
}

7.8 策略路由架构图

数据包 (saddr=1.2.3.4, daddr=5.6.7.8, mark=100)
            |
            v
    fib_rules_lookup()   按优先级遍历规则链
            |
    +-------+-----------------------------+
    |                                     |
    | 规则 0: lookup local                |
    |   fib4_rule_match -> 不匹配         |
    |                                     |
    | 规则 100: from 1.2.3.0/24           |
    |   fib4_rule_match -> 匹配!          |
    |   fib4_rule_action:                 |
    |     fib_get_table(tb_id=100)        |
    |     fib_table_lookup()              |
    |       -> fib_result (via 10.0.0.1) |
    |                                     |
    | 规则 32766: lookup main (未执行)    |
    +-------------------------------------+

8. ECMP(等价多路径路由)

8.1 ECMP 数据结构

ECMP(Equal-Cost Multi-Path)通过在 fib_info.fib_nh[] 中存储多个下一跳实现。每个 fib_nh 通过 fib_nh_common.nhc_weight 指定权重,nhc_upper_bound 是基于权重计算的随机选路上界(atomic_t)。

8.2 ECMP 路径选择:入方向

入方向 ECMP 路径选择在 ip_mkroute_inputnet/ipv4/route.c 第 2169-2184 行):

ip_mkroute_input(struct sk_buff *skb, struct fib_result *res,
                  struct in_device *in_dev, __be32 daddr,
                  __be32 saddr, dscp_t dscp, struct flow_keys *hkeys)
{
#ifdef CONFIG_IP_ROUTE_MULTIPATH
    if (res->fi && fib_info_num_path(res->fi) > 1) {
        int h = fib_multipath_hash(res->fi->fib_net, NULL, skb, hkeys);
        fib_select_multipath(res, h, NULL);
        IPCB(skb)->flags |= IPSKB_MULTIPATH;
    }
#endif
    return __mkroute_input(skb, res, in_dev, daddr, saddr, dscp);
}

fib_multipath_hash 对数据包五元组(src IP、dst IP、src port、dst port、proto)计算哈希值,保证相同流的数据包始终选择相同路径(流一致性)。

8.3 fib_table_lookup 中的 ECMP 路径遍历

在叶节点处理阶段(第 1591-1596 行),对每条路由遍历所有下一跳:

for (nhsel = 0; nhsel < fib_info_num_path(fi); nhsel++) {
    nhc = fib_info_nhc(fi, nhsel);
    if (!fib_lookup_good_nhc(nhc, fib_flags, flp))
        continue;
    // 选中此下一跳,填充 res->nh_sel = nhsel
    goto set_result;
}

fib_lookup_good_nhc 检查下一跳是否可用(不处于 RTNH_F_DEAD 状态,接口 UP 等)。

8.4 nexthop 对象(现代 ECMP API)

Linux 5.x 引入了新式 nexthop 对象系统(include/net/nexthop.h),允许多条路由共享同一组下一跳,并支持更灵活的 ECMP 分组。在 fib_table_lookup 中通过专门路径处理(第 1578-1588 行):

if (unlikely(fi->nh)) {
    if (nexthop_is_blackhole(fi->nh)) {
        err = fib_props[RTN_BLACKHOLE].error;
        goto out_reject;
    }
    nhc = nexthop_get_nhc_lookup(fi->nh, fib_flags, flp, &nhsel);
    if (nhc)
        goto set_result;
    goto miss;
}
传统 ECMP:                    nexthop 对象 ECMP:

fib_info                      fib_info
  fib_nh[0] (via eth0)          nh -> nexthop_group
  fib_nh[1] (via eth1)               nh_grp_entry[0] -> nexthop (via eth0)
  fib_nh[2] (via eth2)               nh_grp_entry[1] -> nexthop (via eth1)
                                     nh_grp_entry[2] -> nexthop (via eth2)

                              多条路由可共享同一 nexthop_group

9. 邻居子系统(ARP/NDP)

9.1 neighbour 结构

邻居子系统(net/core/neighbour.c)是三层路由和二层硬件地址解析之间的桥梁。核心结构 struct neighbour 定义于 include/net/neighbour.h 第 140-170 行:

struct neighbour {
    struct hlist_node   hash;           // 全局哈希表链接
    struct hlist_node   dev_list;       // 设备级邻居链接
    struct neigh_table *tbl;            // 所属邻居表(arp_tbl 或 nd_tbl)
    struct neigh_parms *parms;          // 参数(超时、重传次数等)
    unsigned long       confirmed;      // 最后确认时间(jiffies)
    unsigned long       updated;        // 最后更新时间
    rwlock_t            lock;           // 保护状态和硬件地址
    refcount_t          refcnt;
    struct sk_buff_head arp_queue;      // 待解析时缓存的报文队列
    struct timer_list   timer;          // NUD 状态机定时器
    unsigned long       used;
    atomic_t            probes;         // 已发出的探测次数
    u8                  nud_state;      // NUD 状态机状态
    u8                  type;
    u8                  dead;
    u8                  protocol;
    u32                 flags;
    seqlock_t           ha_lock;        // 保护硬件地址的顺序锁
    unsigned char       ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))]; // MAC 地址
    struct hh_cache     hh;             // 硬件头缓存(加速二层头填充)
    int (*output)(struct neighbour *, struct sk_buff *); // 发送函数指针
    const struct neigh_ops *ops;        // 操作函数表
    struct list_head    gc_list;        // GC 链表(非 PERMANENT 条目)
    struct list_head    managed_list;   // 托管邻居链表
    struct rcu_head     rcu;
    struct net_device  *dev;
    u8                  primary_key[]; // IP 地址(灵活数组,4/16 字节)
} __randomize_layout;

__randomize_layout 使该结构的字段顺序在编译时随机化(KASLR 的一部分),防止基于偏移的漏洞利用。

9.2 neigh_ops:操作函数表

struct neigh_ops {
    int   family;
    void (*solicit)(struct neighbour *, struct sk_buff *);       // 发送 ARP/NS 探测
    void (*error_report)(struct neighbour *, struct sk_buff *);  // 报告错误
    int  (*output)(struct neighbour *, struct sk_buff *);        // 通用发送(慢路径)
    int  (*connected_output)(struct neighbour *, struct sk_buff *); // 快路径发送
};

当邻居状态为 NUD_CONNECTED(PERMANENT/NOARP/REACHABLE)时,neighbour->output 被设置为 neigh_ops->connected_output(快路径),直接填充以太网头发送,无需加锁;其他状态使用 neigh_ops->output(慢路径),需要检查状态、可能触发 ARP 解析。

9.3 NUD 状态机

NUD(Neighbor Unreachability Detection)是邻居子系统的核心(include/net/neighbour.h 第 38-40 行):

状态转换图:

    NONE
      |
      | neigh_lookup_create()
      v
  INCOMPLETE ----ARP响应----> REACHABLE
  (发出 ARP 请求)              (有确认的可达)
      |                           |
      | 超时/失败                  | reachable_time 超时
      v                           v
   FAILED                      STALE
  (解析失败)                   (可达性未知)
                                   |
                            首次发送数据包
                                   v
                                DELAY
                            (等待上层 TCP ACK 确认)
                                   |
                        delay_probe_time 内无确认
                                   v
                                PROBE
                            (主动发送单播 ARP)
                                   |
                 ┌─────────────────┘
                 | ARP 响应           | 超时/失败
                 v                   v
             REACHABLE            FAILED

NUD 状态码(第 38-40 行):
  NUD_IN_TIMER  = NUD_INCOMPLETE | NUD_REACHABLE | NUD_DELAY | NUD_PROBE
  NUD_VALID     = NUD_PERMANENT | NUD_NOARP | NUD_REACHABLE |
                  NUD_PROBE | NUD_STALE | NUD_DELAY
  NUD_CONNECTED = NUD_PERMANENT | NUD_NOARP | NUD_REACHABLE

GC 管理(net/core/neighbour.c 第 136-174 行):非 PERMANENT 且未被外部学习的邻居条目加入 gc_list,超过 gc_thresh3 阈值时强制回收。

9.4 neigh_table:邻居协议表

struct neigh_table 是邻居协议的操作集合,定义于 include/net/neighbour.h 第 209-247 行:

struct neigh_table {
    int             family;         // AF_INET 或 AF_INET6
    unsigned int    entry_size;     // 每个邻居项的内存大小
    unsigned int    key_len;        // 键长度(4=IPv4,16=IPv6)
    __be16          protocol;       // 协议类型
    __u32 (*hash)(const void *pkey, const struct net_device *dev,
                  __u32 *hash_rnd);
    bool  (*key_eq)(const struct neighbour *, const void *pkey);
    int   (*constructor)(struct neighbour *);
    // ...
    int gc_thresh1;  // 低于此值不触发 GC(默认 128)
    int gc_thresh2;  // 超过此值加快 GC(默认 512)
    int gc_thresh3;  // 超过此值直接拒绝新邻居(默认 1024)
    // ...
    struct neigh_statistics __percpu *stats;   // per-CPU 统计
    struct neigh_hash_table __rcu   *nht;      // 哈希表(RCU 保护)
};

设备级邻居缓存(net/core/neighbour.c 第 63-80 行)通过 dev->neighbours[] 按协议族分别存储:

static struct hlist_head *neigh_get_dev_table(struct net_device *dev, int family)
{
    switch (family) {
    case AF_INET:   i = NEIGH_ARP_TABLE; break;
    case AF_INET6:  i = NEIGH_ND_TABLE;  break;
    }
    return &dev->neighbours[i];
}

10. IPv6 路由子系统

10.1 IPv6 与 IPv4 路由的主要差异

特性 IPv4 IPv6
FIB 数据结构 LC-Trie (fib_trie) 二叉 radix tree (fib6_node)
路由信息 fib_info fib6_info
下一跳 fib_nh fib6_nh
路由表 fib_table fib6_table
路由结果缓存 rtable rt6_info
per-CPU 缓存 nhc_pcpu_rth_output rt6i_pcpu (in fib6_nh)
未缓存路由管理 uncached 列表(简单) rt6_uncached_list (per-CPU)
地址长度 32 位 128 位
路由过期 genid 机制 fn_sernum + expires

10.2 fib6_node:IPv6 基数树节点

定义于 include/net/ip6_fib.h 第 67-81 行:

struct fib6_node {
    struct fib6_node __rcu *parent;
    struct fib6_node __rcu *left;
    struct fib6_node __rcu *right;
#ifdef CONFIG_IPV6_SUBTREES
    struct fib6_node __rcu *subtree; // 源地址路由(需要 CONFIG_IPV6_SUBTREES)
#endif
    struct fib6_info __rcu *leaf;    // 指向该前缀的路由链表头

    __u16   fn_bit;     // 当前节点检查的位位置(0-127)
    __u16   fn_flags;   // RTN6_ROOT 等标志
    int     fn_sernum;  // 序列号,用于 rt6_info 缓存失效
    struct fib6_info __rcu *rr_ptr; // round-robin 指针(多路径轮询)
    struct rcu_head rcu;
};

fn_sernum 是 IPv6 路由缓存失效的关键:每次路由变更时该值递增,rt6_info.sernum 与其比较,不一致则缓存失效。

IPv6 使用传统二叉基数树而非 LC-Trie,原因:

  1. IPv6 路由表规模通常远小于 IPv4 全局表
  2. IPv6 地址分配层次化,基数树已有良好性能
  3. IPv6 支持源地址路由(subtree),二叉树结构更易于实现

10.3 fib6_info:IPv6 路由信息

定义于 include/net/ip6_fib.h 第 158-204 行(节选):

struct fib6_info {
    struct fib6_table          *fib6_table;
    struct fib6_info __rcu     *fib6_next;   // 同前缀下一条路由

    union {
        struct list_head fib6_siblings;  // ECMP 兄弟路由链表
        struct list_head nh_list;
    };
    unsigned int fib6_nsiblings;         // ECMP 路由数量

    refcount_t      fib6_ref;
    unsigned long   expires;             // RA 路由过期时间

    struct dst_metrics *fib6_metrics;
    struct rt6key   fib6_dst;            // 目的地址和前缀长度
    u32             fib6_flags;          // RTF_GATEWAY / RTF_EXPIRES 等
    struct rt6key   fib6_src;            // 源地址(源路由)
    struct rt6key   fib6_prefsrc;        // 首选源地址

    u32             fib6_metric;         // 路由 metric
    u8              fib6_protocol;       // RTPROT_KERNEL / RTPROT_RA 等
    u8              fib6_type;           // RTN_UNICAST 等

    u8 should_flush:1,
       dst_nocount:1,
       dst_nopolicy:1,
       fib6_destroying:1,
       unused:4;

    struct rcu_head rcu;
    struct nexthop *nh;                  // 新式 nexthop 对象
    struct fib6_nh  fib6_nh[];           // 内嵌下一跳(ECMP 支持多个)
};

10.4 fib6_table:IPv6 路由表

定义于 include/net/ip6_fib.h 第 391-401 行:

struct fib6_table {
    struct hlist_node   tb6_hlist;    // 哈希表链接
    u32                 tb6_id;       // 路由表 ID
    spinlock_t          tb6_lock;     // 写操作保护(读用 RCU)
    struct fib6_node    tb6_root;     // 基数树根(嵌入,非指针)
    struct inet_peer_base tb6_peers;
    unsigned int        flags;
    unsigned int        fib_seq;      // 写序列号(RTNL 保护)
    struct hlist_head   tb6_gc_hlist; // GC 候选链表
};

路由表 ID 常量(第 403-416 行):

#define RT6_TABLE_UNSPEC   RT_TABLE_UNSPEC
#define RT6_TABLE_MAIN     RT_TABLE_MAIN   // 254
#define RT6_TABLE_DFLT     RT6_TABLE_MAIN
#define RT6_TABLE_LOCAL    RT_TABLE_LOCAL  // 255(含 MULTIPLE_TABLES)

10.5 rt6_info:IPv6 路由缓存

// include/net/ip6_fib.h 第 207-220 行
struct rt6_info {
    struct dst_entry        dst;
    struct fib6_info __rcu *from;    // 指向源 fib6_info(RCU 保护)
    int                     sernum;  // 与 fib6_node.fn_sernum 比较失效

    struct rt6key   rt6i_dst;       // 目的地址/前缀
    struct rt6key   rt6i_src;       // 源地址/前缀
    struct in6_addr rt6i_gateway;   // 网关地址
    struct inet6_dev *rt6i_idev;    // 出接口的 inet6 设备
    u32             rt6i_flags;
    unsigned short  rt6i_nfheader_len;
};

10.6 IPv6 uncached_list 机制

IPv6 使用 per-CPU rt6_uncached_list 管理无法挂接到 trie 节点的临时路由对象(net/ipv6/route.c 第 131-157 行):

struct uncached_list {
    spinlock_t       lock;
    struct list_head head;
};

static DEFINE_PER_CPU_ALIGNED(struct uncached_list, rt6_uncached_list);

void rt6_uncached_list_add(struct rt6_info *rt)
{
    struct uncached_list *ul = raw_cpu_ptr(&rt6_uncached_list);
    rt->dst.rt_uncached_list = ul;
    spin_lock_bh(&ul->lock);
    list_add_tail(&rt->dst.rt_uncached, &ul->head);
    spin_unlock_bh(&ul->lock);
}

当设备下线时(rt6_uncached_list_flush_dev),遍历所有 CPU 的 uncached_list,将引用了该设备的 rt6_infoidevdev 替换为 blackhole_netdev,防止悬空指针访问(第 160-194 行)。

10.7 IPv6 路由有效性检查

ip6_dst_checknet/ipv6/route.c 中实现)通过比较 rt6_info.sernumfib6_node.fn_sernum 确认路由是否失效,与 IPv4 的 rt_genid 机制相对应但更精细(精确到 trie 节点级别)。


11. 路由通知机制(fib_notifier)

11.1 事件类型

定义于 include/net/fib_notifier.h 第 15-26 行:

enum fib_event_type {
    FIB_EVENT_ENTRY_REPLACE,  // 路由被替换(ip route change)
    FIB_EVENT_ENTRY_APPEND,   // 路由被追加(ECMP 新路径)
    FIB_EVENT_ENTRY_ADD,      // 路由被添加(ip route add)
    FIB_EVENT_ENTRY_DEL,      // 路由被删除(ip route del)
    FIB_EVENT_RULE_ADD,       // 策略规则被添加(ip rule add)
    FIB_EVENT_RULE_DEL,       // 策略规则被删除(ip rule del)
    FIB_EVENT_NH_ADD,         // nexthop 对象被添加
    FIB_EVENT_NH_DEL,         // nexthop 对象被删除
    FIB_EVENT_VIF_ADD,        // 多播接口被添加
    FIB_EVENT_VIF_DEL,        // 多播接口被删除
};

11.2 通知基础设施

fib_notifier.h 第 28-50 行定义了 notifier 的注册和分发接口:

struct fib_notifier_ops {
    int family;
    struct list_head list;
    unsigned int (*fib_seq_read)(const struct net *net);
    int (*fib_dump)(struct net *net, struct notifier_block *nb,
                    struct netlink_ext_ack *extack);
    struct module *owner;
    struct rcu_head rcu;
};

int register_fib_notifier(struct net *net, struct notifier_block *nb,
                           void (*cb)(struct notifier_block *nb),
                           struct netlink_ext_ack *extack);
int unregister_fib_notifier(struct net *net, struct notifier_block *nb);

cb 回调在注册时被立即调用,用于做初始状态同步(fib_dump),保证注册者获得完整的路由表快照。

11.3 通知触发点

net/ipv4/fib_trie.c 第 76-108 行,每次 FIB 条目变更时触发通知:

static int call_fib_entry_notifiers(struct net *net,
                                     enum fib_event_type event_type,
                                     u32 dst, int dst_len,
                                     struct fib_alias *fa,
                                     struct netlink_ext_ack *extack)
{
    struct fib_entry_notifier_info info = {
        .info.extack = extack,
        .dst         = dst,
        .dst_len     = dst_len,
        .fi          = fa->fa_info,
        .dscp        = fa->fa_dscp,
        .type        = fa->fa_type,
        .tb_id       = fa->tb_id,
    };
    return call_fib4_notifiers(net, event_type, &info.info);
}

11.4 主要使用者

使用者 功能
net/switchdev 将路由同步到硬件交换芯片(offload)
drivers/net/mlxsw Mellanox 交换芯片路由卸载
drivers/net/netdevsim 内核测试用虚拟设备
net/ipv4/fib_notifier.c IPv4 FIB 通知分发
net/ipv6/ip6_fib.c IPv6 FIB 通知分发
BPF/TC 子系统 路由变更感知,更新 BPF map

通知机制使硬件卸载(Hardware Offload)成为可能:当路由添加时,switchdev 驱动收到通知,将路由写入 ASIC 流表,后续匹配的数据包直接在硬件转发,完全绕过内核协议栈。


12. Netlink/rtnetlink 与用户态交互

12.1 整体通信架构

用户空间 (iproute2: ip route / ip rule / ip neigh)
    |
    |  socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)
    |  sendmsg: RTM_NEWROUTE / RTM_DELROUTE / RTM_GETROUTE
    |
    v
内核 rtnetlink 处理器
  net/core/rtnetlink.c: rtnetlink_rcv_msg()
    |
    +-- RTM_NEWROUTE --> inet_rtm_newroute()
    |                       (net/ipv4/fib_frontend.c)
    |                           |
    |                           v
    |                    rtm_to_fib_config()   // 解析 NLA 属性
    |                           |
    |                           v
    |                    fib_table_insert()    // 插入路由
    |                           |
    |                           v
    |                    call_fib_entry_notifiers()
    |
    +-- RTM_NEWRULE  --> fib4_nl_newrule()
    |                       (net/ipv4/fib_rules.c)
    |
    +-- RTM_GETROUTE --> inet_rtm_getroute()   // 路由查询
    |                       执行完整 fib_lookup 并返回结果
    |
    +-- RTM_NEWNEIGH --> neigh_add()
                            (net/core/neighbour.c)

12.2 fib_config 结构(路由配置参数)

fib_config 是 Netlink 属性解析后的内核内部表示,定义于 include/net/ip_fib.h 第 28-56 行:

struct fib_config {
    u8          fc_dst_len;     // 前缀长度(/24 等)
    dscp_t      fc_dscp;        // DSCP/TOS 值
    u8          fc_protocol;    // RTPROT_STATIC / RTPROT_KERNEL 等
    u8          fc_scope;       // RT_SCOPE_UNIVERSE / RT_SCOPE_LINK 等
    u8          fc_type;        // RTN_UNICAST / RTN_BLACKHOLE 等
    u8          fc_gw_family;   // 网关地址族
    u32         fc_table;       // 目标路由表 ID
    __be32      fc_dst;         // 目的地址
    union { __be32 fc_gw4; struct in6_addr fc_gw6; }; // 网关地址
    int         fc_oif;         // 出接口索引
    u32         fc_flags;       // RTF_UP / RTF_GATEWAY 等
    u32         fc_priority;    // metric(metric 越小优先级越高)
    __be32      fc_prefsrc;     // 首选源地址
    u32         fc_nh_id;       // nexthop 对象 ID(5.x+)
    struct nlattr *fc_mx;       // 路径指标(MTU、RTT、CWND 等)
    struct rtnexthop *fc_mp;    // 多路径下一跳数组
    int         fc_mx_len;
    int         fc_mp_len;
    u32         fc_flow;        // fwmark(用于策略路由)
    u32         fc_nlflags;     // NLM_F_CREATE / NLM_F_REPLACE 等
    struct nl_info fc_nlinfo;   // Netlink 源信息
    struct nlattr *fc_encap;    // 封装参数(VXLAN/MPLS 等)
    u16         fc_encap_type;
};

12.3 主要 Netlink 消息类型

消息类型 方向 操作 内核处理函数
RTM_NEWROUTE 用户→内核 添加/修改路由 inet_rtm_newroute()
RTM_DELROUTE 用户→内核 删除路由 inet_rtm_delroute()
RTM_GETROUTE 用户→内核 查询路由 inet_rtm_getroute()
RTM_NEWRULE 用户→内核 添加策略规则 fib4_nl_newrule()
RTM_DELRULE 用户→内核 删除策略规则 fib4_nl_delrule()
RTM_NEWNEIGH 用户→内核 添加 ARP 条目 neigh_add()
RTM_GETNEIGH 用户→内核 查询邻居表 neigh_get()
RTM_NEWROUTE 内核→用户 路由变更通知 rtmsg_fib()

12.4 ip route get 的内部实现

ip route get 10.1.2.3 发送 RTM_GETROUTE 消息,内核调用 inet_rtm_getroute(),执行一次完整的出方向路由查找(ip_route_output_key_hash_rcu),将结果以 RTM_NEWROUTE 格式回复,包含:

  • 精确匹配的目的前缀和长度
  • 经策略路由选择后的出接口
  • 实际使用的网关
  • 路由来源协议
  • 路径 MTU 等 metrics
  • RTA_PREFSRC:内核选择的源地址
$ ip route get 8.8.8.8
8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.100
    cache
         ↑
         内核填充了 RTA_CACHEINFO,包含过期时间、使用次数等

12.5 路由变更通知到用户空间

rtmsg_fib() 在路由变更时主动推送通知(net/ipv4/fib_lookup.h 第 46 行声明),监听 RTMGRP_IPV4_ROUTE 多播组的用户空间进程(如 Quagga/FRR、NetworkManager)收到通知后可更新自身状态。


13. 完整数据流图

13.1 入方向数据包路由流程

网卡驱动 (RX DMA)
    |
    v
__netif_receive_skb()          // net/core/dev.c
    |
    v
ip_rcv()                       // net/ipv4/ip_input.c
    |
    +- IP 头校验、版本检查、TTL 检查
    |
    v
ip_rcv_finish() -> ip_route_input_noref()
    |
    +- 构建 flowi4 (daddr, saddr, iif, dscp)
    |
    +- [检查 nhc_rth_input per-CPU 缓存]
    |    命中 -> 直接 skb_dst_set(),跳过查找
    |    未命中 ↓
    |
    v
ip_route_input_slow()          // net/ipv4/route.c
    |
    +- fib_lookup(net, &fl4, &res, 0)
    |      |
    |      +- [fib_has_custom_rules == false]
    |      |    直接 fib_table_lookup(fib_main)
    |      |
    |      +- [fib_has_custom_rules == true]
    |           __fib_lookup() -> fib_rules_lookup()
    |                -> fib4_rule_match()
    |                -> fib4_rule_action()
    |                     -> fib_table_lookup()
    |                          -> LC-Trie 三阶段查找
    |                          -> fib_result
    |
    +- ip_mkroute_input()
    |      |
    |      +- [ECMP] fib_multipath_hash() -> fib_select_multipath()
    |      |
    |      +- __mkroute_input()
    |              |
    |              +- rt_dst_alloc() 分配 rtable
    |              +- 设置 rt_is_input=1, rt_gw4, rt_flags
    |              +- 存入 nhc_rth_input 缓存
    |
    v
skb_dst_set(skb, &rt->dst)
    |
    +---[RTN_LOCAL]---> ip_local_deliver() -> 上层协议(TCP/UDP)
    |
    +---[RTN_UNICAST]-> ip_forward()
    |                       |
    |                       +- TTL 递减,Netfilter FORWARD hook
    |                       +- dst_output(skb) -> ip_output()
    |                            -> ip_finish_output2()
    |                                 -> ipv4_neigh_lookup()
    |                                 -> neigh->output()
    |                                      -> dev_queue_xmit()
    |
    +---[RTN_BLACKHOLE/UNREACHABLE] -> 丢弃/发送 ICMP

13.2 出方向数据包路由流程

应用程序 sendmsg() / write()
    |
    v
tcp_sendmsg() / udp_sendmsg()
    |
    v
ip_make_skb() 或 ip_push_pending_frames()
    |
    v
ip_route_output_ports()        // include/net/route.h(内联)
    |
    v
ip_route_output_flow()
    |
    v
ip_route_output_key_hash()     // net/ipv4/route.c:2691
    |
    +- 初始化 fl4 (daddr, saddr, oif, proto, sport, dport, mark)
    +- 设置 fl4->flowi4_iif = LOOPBACK_IFINDEX
    |
    v
ip_route_output_key_hash_rcu() // :2712
    |
    +- saddr 验证(非组播/广播)
    +- fib_lookup(net, fl4, &res, 0)
    |
    +- [ECMP 出方向] fib_select_multipath()
    |
    +- __mkroute_output()
    |      |
    |      +- rt_dst_alloc() 分配 rtable
    |      +- 设置 rt_is_input=0, rt_gw_family, rt_gw4
    |      +- 存入 nhc_pcpu_rth_output[smp_processor_id()]
    |
    v
返回 rtable *
    |
    v
ip_local_out()
    |
    +- Netfilter OUTPUT hook
    +- ip_output()
    +- Netfilter POSTROUTING hook
    |
    v
ip_finish_output()
    |
    +- [需要分片] ip_fragment() -> ip_do_fragment()
    |
    +- ip_finish_output2()
    |      |
    |      +- ipv4_neigh_lookup() 查找/创建 ARP 邻居条目
    |      |      |
    |      |      +- [NUD_CONNECTED] 直接使用缓存的硬件地址
    |      |      +- [NUD_STALE]     设置 DELAY,发送单播 ARP 探测
    |      |      +- [NUD_NONE]      发送广播 ARP 请求,缓存 skb
    |      |
    |      +- neigh_hh_output() 或 neigh->output()
    |           -> 填充以太网帧头
    |           -> dev_queue_xmit()
    |                -> 网卡驱动 TX

13.3 路由子系统模块关系总图

+--------------------------------+   +----------------------------------+
|  net/ipv4/fib_rules.c          |   |  net/ipv4/fib_trie.c             |
|  策略路由规则管理               |-->|  LC-Trie 路由表查找核心          |
|  struct fib4_rule               |   |  struct key_vector / tnode        |
|  __fib_lookup()                 |   |  fib_table_lookup()               |
|  fib4_rule_match/action()       |   |  inflate() / halve()              |
+----------------+----------------+   +---------------+------------------+
                 ^                                    |
                 |      fib_lookup()                  |
        +--------+--------+                           v
        |                 |              +---------------------------+
        |  include/net/ip_fib.h          |  net/ipv4/fib_lookup.h   |
        |  struct fib_table  |           |  struct fib_alias         |
        |  struct fib_info   |           |  fib_alias_accessed()     |
        |  struct fib_nh     |           |  struct fib_prop          |
        |  struct fib_result |           +---------------------------+
        |  fib_nh_common     |
        +--------+-----------+
                 |
                 v
+--------------------------------+   +----------------------------------+
|  net/ipv4/route.c              |   |  include/net/route.h             |
|  路由决策与缓存管理             |   |  struct rtable                   |
|  ip_route_input_*()            |   |  struct dst_ops ipv4_dst_ops     |
|  ip_route_output_key_hash_*()  |   |  rt_nexthop()                    |
|  rt_is_expired()               |   |  ip_route_output_* 内联函数      |
|  nhc_pcpu_rth_output 缓存      |   +----------------------------------+
+---------------+----------------+
                |
                v
+--------------------------------+   +----------------------------------+
|  net/core/neighbour.c          |   |  include/net/neighbour.h         |
|  ARP/NDP 邻居解析              |   |  struct neighbour                |
|  neigh_lookup()                |   |  struct neigh_table              |
|  neigh_update()                |   |  struct neigh_ops                |
|  neigh_timer_handler()         |   |  NUD 状态位定义                  |
|  GC 管理(gc_thresh1/2/3)     |   +----------------------------------+
+----------------+---------------+
                 |
                 v
+--------------------------------+   +----------------------------------+
|  include/net/fib_notifier.h    |   |  net/ipv6/route.c                |
|  enum fib_event_type           |   |  IPv6 路由决策                   |
|  register_fib_notifier()       |   |  struct rt6_info                 |
|  call_fib_notifiers()          |   |  rt6_uncached_list               |
|  fib_notifier_ops              |   |  ip6_dst_check()                 |
+--------------------------------+   +----------------------------------+
                                      include/net/ip6_fib.h
                                      struct fib6_node(二叉基数树)
                                      struct fib6_info
                                      struct fib6_table

附录:关键源码文件索引

文件路径 行号区间 主要内容
include/net/ip_fib.h 28-56 fib_config(路由配置)
include/net/ip_fib.h 61-73 fib_nh_exception(PMTU 异常)
include/net/ip_fib.h 83-105 fib_nh_common(下一跳公共基类)
include/net/ip_fib.h 107-128 fib_nh(IPv4 下一跳)
include/net/ip_fib.h 136-163 fib_info(路由共享属性)
include/net/ip_fib.h 173-185 fib_result(查找结果)
include/net/ip_fib.h 257-264 fib_table(路由表)
include/net/ip_fib.h 292-398 fib_lookup 两个版本(有/无策略路由)
net/ipv4/fib_lookup.h 11-24 fib_alias(前缀别名)
net/ipv4/fib_trie.c 1-37 文件头:LC-Trie 算法引用
net/ipv4/fib_trie.c 110-141 key_vector/tnode 定义
net/ipv4/fib_trie.c 182-184 sysctl_fib_sync_mem 内存阈值
net/ipv4/fib_trie.c 231-293 位域语义注释和调整阈值
net/ipv4/fib_trie.c 1420-1625 fib_table_lookup()(三阶段查找)
net/ipv4/fib_rules.c 36-50 fib4_rule(策略路由规则)
net/ipv4/fib_rules.c 84-108 __fib_lookup()(规则链遍历)
net/ipv4/fib_rules.c 111-145 fib4_rule_action()
net/ipv4/fib_rules.c 147-178 fib4_rule_suppress()
net/ipv4/fib_rules.c 180-200 fib4_rule_match()
net/ipv4/route.c 154-168 ipv4_dst_ops
net/ipv4/route.c 192-196 rt_cache_stat per-CPU 统计
net/ipv4/route.c 396-410 rt_is_expired() / rt_cache_flush()
net/ipv4/route.c 412-438 ipv4_neigh_lookup()
net/ipv4/route.c 2169-2184 ip_mkroute_input() ECMP
net/ipv4/route.c 2691-2769 ip_route_output_key_hash_*()
include/net/route.h 57-78 struct rtable
include/net/route.h 115-124 struct rt_cache_stat
net/core/neighbour.c 63-80 neigh_get_dev_table()
net/core/neighbour.c 136-174 邻居 GC 链表管理
include/net/neighbour.h 38-40 NUD 状态位
include/net/neighbour.h 72-90 struct neigh_parms
include/net/neighbour.h 140-170 struct neighbour
include/net/neighbour.h 209-247 struct neigh_table
include/net/ip6_fib.h 67-81 struct fib6_node
include/net/ip6_fib.h 147-156 struct fib6_nh
include/net/ip6_fib.h 158-204 struct fib6_info
include/net/ip6_fib.h 207-220 struct rt6_info
include/net/ip6_fib.h 391-401 struct fib6_table
net/ipv6/route.c 78-83 enum rt6_nud_state
net/ipv6/route.c 131-194 rt6_uncached_list 机制
include/net/fib_notifier.h 15-26 enum fib_event_type
include/net/fib_notifier.h 28-50 fib_notifier_ops 和注册接口
include/uapi/linux/rtnetlink.h 361-362 RT_TABLE_MAIN / RT_TABLE_LOCAL

由 Claude Code 分析生成


14. VRF(虚拟路由转发)实现

14.1 VRF 概述

VRF(Virtual Routing and Forwarding)允许在同一台 Linux 主机上运行多个相互隔离的路由域。 每个 VRF 实例拥有独立的路由表,不同 VRF 之间的路由默认不互通,实现类似于 物理路由器 VRF/VPN 的网络隔离。

Linux 内核 VRF 由 drivers/net/vrf.c 实现(自 4.3 内核引入), 核心是一个虚拟网络设备 vrf,与 L3mdev(Layer 3 Master Device)框架结合。

14.2 VRF 与路由表的关系

主机路由结构(VRF 场景):

  VRF 设备 vrf-red (table 100)    VRF 设备 vrf-blue (table 200)
  +--------------------+           +--------------------+
  |  eth0.100          |           |  eth0.200          |
  |  绑定到 vrf-red    |           |  绑定到 vrf-blue   |
  +--------------------+           +--------------------+
           |                                |
     路由表 100                       路由表 200
  10.0.0.0/8 via 10.0.0.1         172.16.0.0/12 via 172.16.0.1
           |                                |
     策略路由规则:                   策略路由规则:
     iif vrf-red lookup 100          iif vrf-blue lookup 200

14.3 L3mdev 框架

L3mdev(include/net/l3mdev.h)是 VRF 的抽象层,为上层协议提供统一接口:

/* include/net/l3mdev.h */
struct l3mdev_ops {
    u32 (*l3mdev_fib_table)(const struct net_device *dev);
    /* 获取 VRF 绑定的路由表 ID */

    struct sk_buff * (*l3mdev_l3_rcv)(struct net_device *dev,
                                       struct sk_buff *skb, u16 proto);
    /* 入方向:将数据包关联到正确的 VRF */

    struct sk_buff * (*l3mdev_l3_out)(struct net_device *dev,
                                       struct sock *sk,
                                       struct sk_buff *skb, u16 proto);
    /* 出方向:选择正确的出接口 */

    struct dst_entry * (*l3mdev_link_scope_lookup)(const struct net_device *dev,
                                                    struct flowi6 *fl6);
};

14.4 VRF 路由查找流程

/* net/ipv4/fib_frontend.c */
/* 入方向:sk_buff 进入协议栈时 */
static int ip_route_input_slow(struct sk_buff *skb, ...)
{
    /* 检查 skb 是否属于某个 L3mdev */
    if (skb->dev->l3mdev_ops) {
        /* 使用 L3mdev 绑定的路由表 */
        tb_id = l3mdev_fib_table_rcu(skb->dev);
    } else {
        tb_id = RT_TABLE_MAIN;
    }
    /* 在对应路由表中查找 */
    fib_lookup_with_table(net, fl4, tb_id, ...);
}

14.5 VRF 配置示例

# 创建 VRF 设备(绑定到路由表 100)
ip link add vrf-red type vrf table 100
ip link set vrf-red up

# 将物理接口绑定到 VRF
ip link set eth0 master vrf-red

# 在 VRF 中添加路由(自动写入表 100)
ip route add 10.0.0.0/8 via 192.168.1.1 dev eth0 vrf vrf-red

# 查看 VRF 路由表
ip route show vrf vrf-red
# 等价于:ip route show table 100

# 在特定 VRF 中执行命令(socket 绑定)
ip vrf exec vrf-red ping 10.0.0.1

14.6 VRF 的 socket 绑定

进程可以通过 SO_BINDTODEVICE 将 socket 绑定到 VRF 设备:

int sock = socket(AF_INET, SOCK_STREAM, 0);
/* 绑定到 VRF 设备,后续路由查找在该 VRF 的路由表中进行 */
setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, "vrf-red", strlen("vrf-red"));

内核中 sk->sk_bound_dev_if 记录绑定的设备索引,路由查找时 通过 l3mdev_master_ifindex_by_index() 获取对应的 L3mdev。


15. 路由 Metric 与 Scope

15.1 路由 Metric

Metric(度量值)用于在多条到达同一目的地的路由中选择优先级最高的路由, 值越小优先级越高(与距离向量协议中的 hop count 概念相同)。

内核中 metric 存储在 fib_info.fib_priorityinclude/net/ip_fib.h):

struct fib_info {
    /* ... */
    int         fib_priority;     /* 路由度量值(metric),默认 0 */
    /* ... */
};

iproute2 中通过 metric 关键字指定:

# 添加两条到同一目标的路由,metric 不同
ip route add 192.168.1.0/24 via 10.0.0.1 metric 100
ip route add 192.168.1.0/24 via 10.0.0.2 metric 200
# 内核优先使用 metric=100 的路由

多条到同一前缀的路由(metric 不同)在 LC-Trie 叶节点以 fib_alias 链表 形式存储,按 fib_priority 排序,查找时返回 priority 最小的可用路由。

15.2 Route Scope(路由作用域)

Scope 描述路由所代表的网络距离,定义于 include/uapi/linux/rtnetlink.h

enum rt_scope_t {
    RT_SCOPE_UNIVERSE = 0,   /* 全局路由(默认,跨网络)*/
    RT_SCOPE_SITE     = 200, /* IPv6 站点本地(已废弃)*/
    RT_SCOPE_LINK     = 253, /* 直连路由(同一链路)*/
    RT_SCOPE_HOST     = 254, /* 本机路由(loopback)*/
    RT_SCOPE_NOWHERE  = 255, /* 不可达 */
};

路由查找时 scope 的作用(fib_table_lookup() 中):

/* 检查下一跳接口的 scope 是否满足要求 */
if (nhc->nhc_scope > res->scope) {
    /* 下一跳 scope 过窄,跳过此路由 */
    continue;
}

典型 scope 应用:

# 直连路由自动为 LINK scope
ip route add 192.168.1.0/24 dev eth0
# 等价于:... scope link

# 默认路由为 UNIVERSE scope
ip route add default via 192.168.1.1
# 等价于:... scope global

# 本机接口地址路由为 HOST scope(由系统自动添加到 local 表)
# 127.0.0.1 local 127.0.0.1 dev lo table local scope host

15.3 Route Type

路由类型(RTN_*)定义路由的处理动作:

/* include/uapi/linux/rtnetlink.h */
enum {
    RTN_UNSPEC,
    RTN_UNICAST,    /* 正常单播路由(最常用)*/
    RTN_LOCAL,      /* 本地地址路由(到 lo 的流量)*/
    RTN_BROADCAST,  /* 广播路由 */
    RTN_ANYCAST,    /* 任播路由 */
    RTN_MULTICAST,  /* 组播路由 */
    RTN_BLACKHOLE,  /* 黑洞路由(丢包,无 ICMP)*/
    RTN_UNREACHABLE,/* 不可达路由(丢包,发 ICMP Unreachable)*/
    RTN_PROHIBIT,   /* 禁止路由(丢包,发 ICMP Admin Prohibited)*/
    RTN_THROW,      /* 抛出路由(继续查找下一个规则)*/
    RTN_NAT,        /* NAT 路由(已废弃)*/
    RTN_XRESOLVE,   /* 外部解析(已废弃)*/
};

黑洞路由示例(用于防止路由泄露):

ip route add blackhole 10.0.0.0/8
# 来自 10.0.0.0/8 的流量被静默丢弃

ip route add unreachable 172.16.0.0/12
# 发送 ICMP 不可达消息

16. BPF FIB Lookup

16.1 bpf_fib_lookup() Helper

BPF 程序可以通过 bpf_fib_lookup() helper 在内核中执行 FIB 查找, 用于 XDP/TC BPF 程序实现高性能路由转发决策。

接口定义(include/uapi/linux/bpf.h):

/* BPF FIB 查找参数 */
struct bpf_fib_lookup {
    __u8    family;          /* AF_INET 或 AF_INET6 */
    __u8    l4_protocol;     /* IPPROTO_TCP/UDP 等 */
    __be16  sport;           /* 源端口 */
    __be16  dport;           /* 目的端口 */
    union {
        __u32 ipv4_dst;
        __u32 ipv6_dst[4];   /* 目的 IP(网络字节序)*/
    };
    union {
        __u32 ipv4_src;
        __u32 ipv6_src[4];   /* 源 IP */
    };
    /* 输出字段(查找成功后填充)*/
    __u8    smac[6];         /* 出接口 MAC 地址(源)*/
    __u8    dmac[6];         /* 下一跳 MAC 地址 */
    __u16   h_vlan_proto;    /* VLAN 协议 */
    __u16   h_vlan_TCI;      /* VLAN TCI */
    __u32   ifindex;         /* 出接口索引 */
    /* ... */
};

16.2 BPF FIB Lookup 内核实现

bpf_fib_lookup() 内核侧实现于 net/core/filter.c, 函数 bpf_ipv4_fib_lookup()

/* net/core/filter.c (简化) */
static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
                                 u32 flags, bool check_mtu)
{
    struct fib_result res;
    struct flowi4 fl4 = {
        .daddr = params->ipv4_dst,
        .saddr = params->ipv4_src,
        .flowi4_oif  = params->ifindex,  /* 可选:限制出接口 */
        .flowi4_mark = 0,
    };

    /* 执行 FIB 查找(支持策略路由) */
    if (flags & BPF_FIB_LOOKUP_SKIP_NEIGH)
        err = fib_lookup(net, &fl4, &res, FIB_LOOKUP_NOREF);
    else
        err = fib_lookup(net, &fl4, &res, 0);

    if (err)
        return BPF_FIB_LKUP_RET_NOT_FWDED;

    /* 填充出接口信息 */
    params->ifindex = FIB_RES_OIF(res);
    /* 解析邻居(ARP)*/
    neigh = __ipv4_neigh_lookup_noref(dev, params->ipv4_dst);
    if (neigh && (neigh->nud_state & NUD_VALID)) {
        memcpy(params->dmac, neigh->ha, ETH_ALEN);
        memcpy(params->smac, dev->dev_addr, ETH_ALEN);
        return BPF_FIB_LKUP_RET_SUCCESS;
    }
    return BPF_FIB_LKUP_RET_NO_NEIGH;
}

16.3 XDP 路由转发示例

/* XDP 程序示例:使用 bpf_fib_lookup 实现 L3 转发 */
SEC("xdp")
int xdp_router(struct xdp_md *ctx)
{
    struct bpf_fib_lookup fib_params = {};
    struct ethhdr *eth = data;
    struct iphdr  *iph = data + sizeof(*eth);

    fib_params.family  = AF_INET;
    fib_params.ipv4_src = iph->saddr;
    fib_params.ipv4_dst = iph->daddr;
    fib_params.ifindex = ctx->ingress_ifindex;

    int rc = bpf_fib_lookup(ctx, &fib_params, sizeof(fib_params),
                             BPF_FIB_LOOKUP_DIRECT);

    if (rc == BPF_FIB_LKUP_RET_SUCCESS) {
        /* 更新 MAC 地址,直接转发 */
        memcpy(eth->h_dest,   fib_params.dmac, ETH_ALEN);
        memcpy(eth->h_source, fib_params.smac, ETH_ALEN);
        /* 减少 TTL */
        iph->ttl--;
        /* 重算校验和 */
        bpf_l3_csum_replace(ctx, IP_CSUM_OFFSET, 0, 0, BPF_F_PSEUDO_HDR);
        return bpf_redirect(fib_params.ifindex, 0);
    }
    return XDP_PASS;
}

16.4 BPF FIB Lookup 返回码

/* include/uapi/linux/bpf.h */
enum {
    BPF_FIB_LKUP_RET_SUCCESS,       /* 查找成功,可直接转发 */
    BPF_FIB_LKUP_RET_BLACKHOLE,     /* 命中黑洞路由 */
    BPF_FIB_LKUP_RET_UNREACHABLE,   /* 不可达 */
    BPF_FIB_LKUP_RET_PROHIBIT,      /* 被禁止 */
    BPF_FIB_LKUP_RET_NOT_FWDED,     /* 未找到路由 */
    BPF_FIB_LKUP_RET_FWD_MFW,       /* 转发到 MFW(未使用)*/
    BPF_FIB_LKUP_RET_NO_NEIGH,      /* 路由找到但无 ARP/ND 条目 */
    BPF_FIB_LKUP_RET_FRAG_NEEDED,   /* 需要分片但 DF 位已置 */
};

17. 路由调试:ip route 与内核交互

17.1 ip route show 的 Netlink 流程

ip route show 命令通过 Netlink 套接字与内核通信:

用户空间                      内核空间
-----------                   ---------
ip route show
  |
  | socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)
  | sendmsg: RTM_GETROUTE + NLM_F_DUMP
  |                            |
  |                            | rtnl_dump_all()
  |                            |   -> fib_dump_info_fnhe() 遍历所有路由表
  |                            |   -> fib_trie_dump() 遍历 LC-Trie 每个叶节点
  |                            |   -> 每个 fib_alias 生成一条 RTM_NEWROUTE 消息
  |                            |
  | <-- RTM_NEWROUTE 消息流 ---|
  |
  解析并打印路由信息

17.2 ip route get 的精确查找

ip route get 触发实际的路由查找,模拟内核处理数据包的路径:

ip route get 8.8.8.8 from 192.168.1.100

对应 Netlink 请求:RTM_GETROUTE(不带 NLM_F_DUMP),内核调用 inet_rtm_getroute() (net/ipv4/route.c):

static int inet_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,
                              struct netlink_ext_ack *extack)
{
    /* 从 Netlink 消息解析 flowi4 */
    fl4.daddr = dst;
    fl4.saddr = src;
    fl4.flowi4_oif = oif;
    fl4.flowi4_iif = iif;

    if (iif) {
        /* 入方向查找 */
        err = ip_route_input_rcu(skb, dst, src, dscp, dev, &res);
        rt = skb_rtable(skb);
    } else {
        /* 出方向查找(更常用)*/
        rt = ip_route_output_key(net, &fl4);
    }

    /* 将 rtable 信息序列化为 RTM_NEWROUTE 回复 */
    return rt_fill_info(net, dst, src, rt, table_id, &fl4, skb, ...);
}

输出解析示例:

$ ip route get 8.8.8.8
8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.100 uid 0
    cache

cache 字样说明结果已缓存在 nhc_pcpu_rth_output 中。

17.3 路由统计:/proc/net/rt_cache

虽然全局路由缓存已在 3.6 版本废弃,/proc/net/rt_cache 仍然存在但通常为空:

cat /proc/net/rt_cache
# 现代内核(3.6+)此文件通常只有头部,无条目
# 实际缓存在 per-CPU dst_entry 中,不通过此接口暴露

路由统计通过 /proc/net/stat/rt_cache 查看:

cat /proc/net/stat/rt_cache
# entries  in_hit in_slow_tot in_slow_mc in_no_route in_brd ...
# 000003f0 000000 000001ab    00000000   00000000    00000000 ...

17.4 nstat 与路由相关计数器

nstat 工具查看内核路由相关统计:

nstat -a | grep -i route
# IpExtInNoRoutes     0                  0.0
# 含义:收到但无法路由的 IP 包(路由表中无匹配项)

nstat -a | grep -i forward
# IpForwDatagrams     12345              0.0
# 含义:被转发的 IP 数据包总数(路由器模式)

内核 SNMP 计数器位于 net/ipv4/proc.csnmp4_ipstats_list[] 数组。

17.5 ftrace 追踪路由查找

# 追踪 fib_lookup 调用链
cd /sys/kernel/tracing
echo 'fib_lookup fib_table_lookup fib_check_nexthop' > set_ftrace_filter
echo function_graph > current_tracer
echo 1 > tracing_on

# 产生一次路由查找(ping 触发)
ping -c 1 8.8.8.8 &

# 查看 trace
cat trace | head -50
# 0) + 1.234 us | fib_lookup() {
# 0)   0.567 us |   fib_table_lookup() {
# 0)   0.123 us |     fib_check_nexthop();
# 0)             |   }
# 0)             | }

18. 路由子系统完整参考源码索引(补充)

18.1 VRF 相关

文件路径 关键内容
drivers/net/vrf.c VRF 虚拟设备实现、L3mdev 回调
include/net/l3mdev.h L3mdev 操作接口定义
net/ipv4/l3mdev.c L3mdev IPv4 查找辅助函数

18.2 BPF 路由相关

文件路径 关键内容
net/core/filter.c bpf_fib_lookup() 实现(bpf_ipv4_fib_lookupbpf_ipv6_fib_lookup
include/uapi/linux/bpf.h struct bpf_fib_lookup 定义、返回码枚举

18.3 路由 Metric/Scope 相关

文件路径 行号 关键内容
include/uapi/linux/rtnetlink.h 103-122 enum rt_scope_t 路由作用域
include/uapi/linux/rtnetlink.h 33-61 enum rtattr_type_t(RTA_*)路由属性
net/ipv4/fib_semantics.c 全文 fib_info 分配、RTA 属性解析、metric 处理
net/ipv4/fib_trie.c 1420-1625 fib_table_lookup 中 scope/metric 过滤逻辑

18.4 ECMP Nexthop 对象(新式接口)

Linux 5.1 引入了独立的 nexthop 对象(net/ipv4/nexthop.c), 将 ECMP 配置从路由条目中分离:

# 创建 nexthop 组(ECMP)
ip nexthop add id 10 via 10.0.0.1 dev eth0
ip nexthop add id 11 via 10.0.0.2 dev eth1
ip nexthop add id 20 group 10/11  # 等权重 ECMP

# 使用 nexthop 组的路由
ip route add 192.168.2.0/24 nhid 20

# 查看 nexthop 对象
ip nexthop show
# id 10  via 10.0.0.1 dev eth0 scope link
# id 11  via 10.0.0.2 dev eth1 scope link
# id 20  group 10/11

相关源码:

文件路径 关键内容
net/ipv4/nexthop.c Nexthop 对象 CRUD、RTM_NEWNEXTHOP/DELNEXTHOP/GETNEXTHOP
include/net/nexthop.h struct nexthopstruct nh_groupstruct nh_grp_entry
net/ipv4/fib_semantics.c fib_info 与 nexthop 对象的关联

18.5 路由通知链(FIB Notifier)补充

FIB notifier 链允许内核模块监听路由变化事件,驱动 offload 同步到硬件:

/* 驱动注册 FIB notifier */
struct notifier_block nb = {
    .notifier_call = my_fib_event_handler,
};
register_fib_notifier(net, &nb, fib_rules_event_type, extack);

/* 事件处理 */
static int my_fib_event_handler(struct notifier_block *nb,
                                  unsigned long event, void *ptr)
{
    struct fib_notifier_info *info = ptr;

    switch (event) {
    case FIB_EVENT_ENTRY_ADD:
        /* 将路由同步到硬件转发表 */
        offload_add_route((struct fib4_entry_notifier_info *)info);
        break;
    case FIB_EVENT_ENTRY_DEL:
        offload_del_route(...);
        break;
    case FIB_EVENT_NH_ADD:
        /* 下一跳变化(ECMP 权重调整等)*/
        break;
    }
    return NOTIFY_DONE;
}

使用 FIB notifier 的内核模块示例:

  • drivers/net/ethernet/mellanox/mlxsw/spectrum_router.c(Mellanox 交换机)
  • drivers/net/ethernet/netronome/nfp/bpf/offload.c(Netronome NFP)
  • net/bridge/br_fib_notifier.c(网桥 FIB 同步)

由 Claude Code 分析生成