WEBKT

深度解析 eBPF 辅助函数 bpf_fib_lookup:如何在 XDP 层免去内存查表直接复用内核路由表?

4 0 0 0

在构建高性能的网络数据面(如 L3 转发、负载均衡器、网关)时,XDP (eXpress Data Path) 凭借其在网卡驱动层(sk_buff 分配之前)处理数据包的能力,成为了无可争议的利器。

然而,一旦涉及 L3 路由转发,开发者通常面临一个两难抉择:

  1. 在用户态与 BPF Map 中维护一套独立的路由表:性能极高,但需要极其复杂的同步逻辑(监听 RTNETLINK 变动,处理黑洞、多路径、ARP/邻居表变化等),极易出现数据不一致。
  2. 将数据包上送至内核协议栈(XDP_PASS)处理:省去了路由表维护工作,但数据包需要经历昂贵的 skb 构建及完整的协议栈流程,XDP 的性能优势荡然无存。

为了解决这一痛点,Linux 内核引入了 bpf_fib_lookup 辅助函数。它允许我们在 XDP(或 TC)层直接调用内核的 FIB(Forwarding Information Base,转发信息库)。这意味着我们既能享受 XDP 接近线速(wirespeed)的转发性能,又无需在用户态重复造路由表的“轮子”。


一、 什么是 bpf_fib_lookup?

bpf_fib_lookup 是一个 eBPF 辅助函数,定义在 bpf.h 中。它的核心作用是:传入当前数据包的元数据(源 IP、目的 IP、协议类型等),直接查询 Linux 内核的路由表和邻居表(ARP/NDP Cache)。

如果路由查询成功,该函数会直接返回:

  1. 下一跳的 MAC 地址(用于修改数据包的 Eth Header)。
  2. 源 MAC 地址(输出网卡的 MAC)。
  3. 输出网卡的网口索引(ifindex)

有了这三个信息,XDP 程序可以直接修改数据包的以太网首部,并通过 bpf_redirectXDP_TX 将其直接发送出去,完全绕过了内核网络栈的后续处理。


二、 核心数据结构:struct bpf_fib_lookup

在调用该函数前,我们需要构造并填充一个 struct bpf_fib_lookup 结构体。其关键字段如下:

struct bpf_fib_lookup {
    /* 输入参数 */
    __u8    family;          // AF_INET (IPv4) 或 AF_INET6 (IPv6)
    __u8    l4_protocol;     // 传输层协议 (IPPROTO_TCP, IPPROTO_UDP 等)
    __u16   sport;           // 源端口 (L4 查找,主要用于多路径路由 ECMP)
    __u16   dport;           // 目的端口
    __u16   tot_len;         // IP 包总长度

    union {
        /* 输入/输出参数 */
        __u16   ifindex;     // 输入:接收网卡索引;输出:发送网卡索引
    };

    union {
        /* 输入:目的 IP;输出:如果是网关路由,则为下一跳 IP */
        __be32  ipv4_dst;
        __u32   ipv6_dst[4];
    };

    union {
        /* 输入:源 IP */
        __be32  ipv4_src;
        __u32   ipv6_src[4];
    };

    /* 输出参数 */
    __u8    smac[6];         // 输出网卡的 MAC 地址 (Source MAC)
    __u8    dmac[6];         // 下一跳或目的主机的 MAC 地址 (Destination MAC)
};

三、 内核内部工作原理

当在 XDP 中调用 bpf_fib_lookup(ctx, fib_params, sizeof(*fib_params), flags) 时,内核在后台执行了以下优化路径:

  1. 快速路径路由查找:内核跳过了复杂的协议栈上下文切换,直接调用 ip_route_input_noref(IPv4)或 ip6_route_input(IPv6)进行快速路由查找。
  2. 邻居表(ARP/NDP)检索
    • 找到出口设备后,内核会根据下一跳 IP 地址在邻居表中查找 MAC 地址。
    • 如果邻居表中有缓存(NUD_VALID 状态),内核会将目标 MAC 和源 MAC 直接写入 bpf_fib_lookup 结构体的 dmacsmac 字段。
    • 如果邻居表无缓存(未解析),函数会返回特定错误码(如 BPF_FIB_LKUP_RET_NO_NEIGH),此时需要将包上送协议栈以触发 ARP 请求。

四、 极简实战:XDP 极速路由转发代码实现

下面是一个完整的 XDP 路由转发 C 代码示例。该程序拦截 IPv4 数据包,调用 bpf_fib_lookup 查询内核路由,如果查找成功,则直接修改 MAC 地址并从指定网口重定向发出。

#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>

SEC("xdp")
int xdp_router_func(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    // 1. 解析以太网首部
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    // 仅处理 IPv4 流量
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;

    // 2. 解析 IP 首部
    struct iphdr *iph = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    // 3. 构造 FIB 查找结构体
    struct bpf_fib_lookup fib_params;
    __builtin_memset(&fib_params, 0, sizeof(fib_params));

    fib_params.family = AF_INET;
    fib_params.l4_protocol = iph->protocol;
    fib_params.sport = 0; // 若不需要 ECMP,可设为 0
    fib_params.dport = 0;
    fib_params.tot_len = __constant_ntohs(iph->tot_len);
    fib_params.ipv4_src = iph->saddr;
    fib_params.ipv4_dst = iph->daddr;
    fib_params.ifindex = ctx->ingress_ifindex; // 传入入口网卡索引

    // 4. 调用内核 FIB 查找
    // BPF_FIB_LOOKUP_DIRECT 标志表示跳过基于策略路由的某些复杂检查,提升性能
    int rc = bpf_fib_lookup(ctx, &fib_params, sizeof(fib_params), 0);

    if (rc == BPF_FIB_LKUP_RET_SUCCESS) {
        // 路由及邻居查找成功!
        
        // 5. 递减 TTL (L3 转发规范)
        iph->ttl--;
        if (iph->ttl <= 0) {
            return XDP_PASS; // 让内核协议栈去发送 ICMP Time Exceeded
        }
        
        // 重新计算 IP 校验和 (简单增量更新)
        __u32 csum = iph->check;
        csum += __constant_htons(0x0100);
        iph->check = (csum & 0xffff) + (csum >> 16);

        // 6. 替换以太网首部的 MAC 地址
        __builtin_memcpy(eth->h_dest, fib_params.dmac, ETH_ALEN);
        __builtin_memcpy(eth->h_source, fib_params.smac, ETH_ALEN);

        // 7. 将数据包重定向到找到的出口网卡
        return bpf_redirect(fib_params.ifindex, 0);
    }

    // 对于无法直接转发的情况(如无路由、邻居未解析、多播等),上送给内核协议栈处理
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

五、 返回值(Return Code)深度剖析与边界处理

bpf_fib_lookup 的返回值是判断数据包后续走向的关键。不能简单地对非 SUCCESS 返回值一律丢弃(XDP_DROP),合理地回退到 XDP_PASS 是保障网络高可用性的核心。

返回值常量 含义 推荐应对策略
BPF_FIB_LKUP_RET_SUCCESS 查找成功。获取到了下一跳 MAC 和出口 ifindex 修改 MAC,执行 bpf_redirect 快速转发。
BPF_FIB_LKUP_RET_BLACKHOLE 命中黑洞路由(Blackhole route)。 直接返回 XDP_DROP,就地丢弃。
BPF_FIB_LKUP_RET_UNREACHABLE 路由不可达(Unreachable)。 返回 XDP_PASS,由内核回复 ICMP Destination Unreachable。
BPF_FIB_LKUP_RET_NO_NEIGH 路由表有记录,但 邻居表(ARP/NDP)无缓存或已过期 必须返回 XDP_PASS!这会让包进入内核协议栈,触发 ARP 请求。一旦 ARP 解析成功,后续的包就能命中快速路径。
BPF_FIB_LKUP_RET_FRAG_NEEDED 报文长度大于出口 MTU,需要分片。 返回 XDP_PASS,让内核栈处理分片逻辑。

六、 性能优化与工程落地实践

在生产环境落地基于 bpf_fib_lookup 的 XDP 路由器时,需特别注意以下几点:

1. 邻居表热拔插温流(Warm-up)

如上表所示,如果新目的 IP 首次出现,bpf_fib_lookup 会返回 NO_NEIGH 并降级为 XDP_PASS

  • 优化方案:在高性能场景下,可以运行一个用户态守护进程,定期 ping 局域网内的核心网关,或者主动向内核邻居表写入静态/半静态 ARP 记录,保证 XDP 路径的“热态”,避免高并发下大量包回退到协议栈引发 CPU 软中断突高。

2. 注意 MTU 限制

struct bpf_fib_lookup 内置了 MTU 检查。如果入口数据包长度(tot_len)大于出口网卡的 MTU,查找会失败。确保在组装 fib_params 时,准确填写 tot_len

3. 与 Linux ip rule(策略路由)的协同

默认情况下,bpf_fib_lookup 仅支持主路由表。若系统配置了复杂的策略路由(Policy Routing),需要传入 BPF_FIB_LOOKUP_DIRECT 以外的 flags,或者确保路由逻辑可由基础路由表覆盖。


七、 总结

通过 bpf_fib_lookup,eBPF 技术将 “极速的数据面(XDP)”“成熟的控制面(Linux 路由协议栈,如 FRR/Bird 等)” 完美地结合在了一起。

  • 开发效率:零,你不需要编写一行用户态路由同步代码。
  • 内存开销:极小,直接复用系统的路由内存结构,无需额外的 BPF Map。
  • 转发性能:几乎等同于手写 BPF Map 查表,将原本微秒级的路由延迟降到极限。

对于正在构建 K8s CNI 网络插件、L3/L4 边缘网关、高并发 NAT 盒子的开发者而言,bpf_fib_lookup 绝对是目前兼具开发效率与极致性能的最优解。

KernelDevLeo eBPFXDPLinux路由表

评论点评