3. eBPF网络数据路径:XDP与TC钩子,sk_buff结构解析与包处理流程

在eBPF网络观测与租户隔离方案中,数据包在内核中的处理路径决定了观测点与策略注入的位置。本章深入解析两个核心eBPF钩子——XDP(eXpress Data Path)与TC(Traffic Control),以及内核网络栈的核心数据结构sk_buff,帮助您理解数据包从网卡到协议栈的完整流转过程。

3.1 XDP与TC:内核网络数据路径的两大钩子

XDP与TC分别位于网络数据路径的早期与后期阶段,提供了不同粒度的包处理能力。下表对比了二者的核心差异:

特性 XDP(eXpress Data Path) TC(Traffic Control)
挂载点 网卡驱动层,在分配sk_buff之前 内核协议栈的dev_queue_xmit(出)与__netif_receive_skb_core(入)
数据包形态 原始帧(xdp_buff),无sk_buff开销 已封装为sk_buff,可访问协议头
处理能力 丢弃、重定向、透传、修改包内容 过滤、整形、镜像、修改、流量控制
性能 极高(DPDK级别,无协议栈开销) 较高(经过部分协议栈处理)
适用场景 DDoS防御、负载均衡、高速包过滤 QoS、流量镜像、容器网络策略
关键认知:XDP在驱动层直接操作原始帧,适合高性能场景;TC在协议栈内操作sk_buff,适合需要协议解析的复杂策略。租户隔离方案中,通常组合使用:XDP做快速过滤,TC做精细化观测与标记。

3.2 XDP数据路径:从网卡到eBPF程序

XDP程序挂载在网卡驱动接收路径的最前端。当数据包到达网卡时,驱动通过ixgbe_xdp_run_prog或类似函数调用eBPF程序。程序接收一个xdp_buff结构,该结构包含指向原始帧数据的指针、长度以及上下文信息。

XDP程序可以返回以下动作:

  • XDP_DROP:直接丢弃数据包,不分配sk_buff,性能最高。
  • XDP_PASS:将数据包传递给内核协议栈,后续会分配sk_buff。
  • XDP_TX:从接收网卡直接发送出去(通常用于负载均衡)。
  • XDP_REDIRECT:将数据包重定向到其他网卡或AF_XDP套接字。
  • XDP_ABORTED:程序异常,效果等同于DROP。
性能提示:XDP_DROP路径完全不经过协议栈,单核处理能力可达数百万PPS。在租户隔离场景中,可用XDP快速丢弃恶意租户的流量,避免影响其他租户。
XDP与TC数据路径对比 网卡驱动 XDP钩子 XDP_DROP / TX XDP_PASS 内核协议栈 TC Ingress XDP在sk_buff分配前处理原始帧,TC在协议栈内操作sk_buff。两者组合可实现从驱动到应用的全路径观测。

3.3 TC数据路径:协议栈内的流量控制

TC(Traffic Control)是Linux内核中成熟的流量控制框架。eBPF程序可以挂载到TC的ingress(入向)和egress(出向)钩子上,分别处理接收和发送的数据包。

TC eBPF程序接收一个__sk_buff结构,该结构是内核sk_buff的eBPF友好版本,提供了对协议头字段的访问能力。TC程序可以:

  • 过滤:根据五元组、租户标签等条件决定放行或丢弃。
  • 镜像:通过bpf_skb_output或bpf_clone_redirect将包复制到观测通道。
  • 标记:修改sk_buff的mark字段或添加隧道封装,用于租户隔离。
  • 整形:结合TC的qdisc实现带宽限制。
注意:TC eBPF程序运行在进程上下文,可以调用辅助函数(如bpf_map_lookup_elem),但需要注意锁竞争和性能开销。在高速场景下,建议将频繁查询的映射表设置为BPF_MAP_TYPE_PERCPU_HASH。

3.4 sk_buff结构解析:内核网络栈的核心

sk_buff(套接字缓冲区)是Linux内核网络栈中最重要的数据结构之一。它贯穿整个网络处理路径,承载数据包的所有元数据。对于eBPF开发者而言,理解sk_buff的关键字段至关重要。

字段 类型 说明 eBPF访问方式
len __u32 数据包总长度(包括协议头) skb->len
data unsigned char * 指向数据包数据的起始位置 skb->data
head unsigned char * 数据缓冲区起始地址 skb->head
mark __u32 防火墙标记,常用于策略路由和租户标识 skb->mark
protocol __be16 L3协议类型(如ETH_P_IP、ETH_P_IPV6) skb->protocol
vlan_proto __be16 VLAN协议类型 skb->vlan_proto
vlan_tci __u16 VLAN标签(含PCP、DEI、VID) skb->vlan_tci
cb[] char[48] 控制块,用于在协议层间传递私有数据 skb->cb
租户隔离关键字段:skb->mark和skb->cb[]是eBPF程序标记租户身份的常用位置。在TC ingress程序中设置skb->mark,后续的iptables、路由策略或XDP程序均可读取该标记,实现跨层级的租户隔离。

3.5 包处理流程:从网卡到eBPF再到协议栈

一个数据包从网卡到达用户空间应用,经过的eBPF可观测路径如下:

  1. 网卡接收:DMA将数据包写入内存环形缓冲区,触发硬中断。
  2. XDP钩子:驱动调用XDP eBPF程序,程序返回动作(DROP/PASS/TX/REDIRECT)。
  3. sk_buff分配:若XDP返回PASS,内核分配sk_buff结构,将数据包元数据填充到字段中。
  4. TC Ingress钩子:在__netif_receive_skb_core中调用TC eBPF程序,可修改sk_buff字段。
  5. 协议栈处理:IP层、TCP/UDP层依次处理,最终将数据交付给socket。
  6. TC Egress钩子:在数据包发送时,dev_queue_xmit中调用TC eBPF程序。
  7. XDP TX/REDIRECT:在发送路径上,XDP程序可再次介入(如负载均衡场景)。
sk_buff结构及包处理流程 sk_buff 结构 head → [数据缓冲区起始] data → [当前协议头位置] mark / cb[] / protocol / len ① 网卡接收 ② XDP钩子 ③ sk_buff分配 ④ TC Ingress ⑤ 协议栈处理 ⑥ TC Egress ⑦ XDP TX sk_buff分配后

3.6 实战:在TC eBPF中读取sk_buff标记

以下代码片段展示了如何在TC ingress程序中读取并设置sk_buff的mark字段,用于租户隔离:

// tc_tenant_mark.c
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <bpf/bpf_helpers.h>

// 租户ID映射表:根据源IP映射到租户ID
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, __u32);   // 源IP
    __type(value, __u32); // 租户ID
} tenant_map SEC(".maps");

SEC("tc")
int tc_ingress_mark(struct __sk_buff *skb) {
    // 读取源IP(假设为IPv4,偏移量在IP头中)
    void *data_end = (void *)(long)skb->data_end;
    void *data = (void *)(long)skb->data;
    
    // 简单校验:确保至少包含以太网头 + IP头
    if (data + 34 > data_end)
        return TC_ACT_OK;
    
    // 读取IP头中的源地址(偏移14字节以太网头 + 12字节IP头)
    __u32 src_ip = *(__u32 *)(data + 26);
    
    // 查询租户映射
    __u32 *tenant_id = bpf_map_lookup_elem(&tenant_map, &src_ip);
    if (tenant_id) {
        // 设置skb->mark为租户ID
        skb->mark = *tenant_id;
    }
    
    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";
编译与挂载:使用clang -O2 -target bpf -c tc_tenant_mark.c -o tc_tenant_mark.o编译,然后通过tc filter add dev eth0 ingress bpf da obj tc_tenant_mark.o sec tc挂载到网卡。

3.7 本章小结