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快速丢弃恶意租户的流量,避免影响其他租户。
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可观测路径如下:
- 网卡接收:DMA将数据包写入内存环形缓冲区,触发硬中断。
- XDP钩子:驱动调用XDP eBPF程序,程序返回动作(DROP/PASS/TX/REDIRECT)。
- sk_buff分配:若XDP返回PASS,内核分配
sk_buff结构,将数据包元数据填充到字段中。 - TC Ingress钩子:在
__netif_receive_skb_core中调用TC eBPF程序,可修改sk_buff字段。 - 协议栈处理:IP层、TCP/UDP层依次处理,最终将数据交付给socket。
- TC Egress钩子:在数据包发送时,
dev_queue_xmit中调用TC eBPF程序。 - XDP TX/REDIRECT:在发送路径上,XDP程序可再次介入(如负载均衡场景)。
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挂载到网卡。