eBPF 中的 `__sk_buff`
eBPF 中的__sk_buff
在 Linux 内核网络处理中,struct sk_buff(通常称为 skb)是数据包的核心数据结构,包含了从链路层到应用层的所有信息以及内核处理过程中的元数据。然而,当我们在 eBPF 中编写网络程序(如 TC、XDP、cgroup skb 等)时,无法直接访问内核的sk_buff,因为直接暴露如此庞大的结构会带来安全风险,且 eBPF 验证器(verifier)难以跟踪。为此,内核提供了一个精简、安全的视图——__sk_buff,定义在<linux/bpf.h>中。
本文旨在全面解析__sk_buff的各个字段,说明它们的含义、可读写性以及典型使用场景,并辅以简单的 TC 程序示例来展示如何访问和修改这些字段。无论你是编写 TC 分类器、防火墙、负载均衡器还是其他网络程序,理解__sk_buff都是关键的一步。
一、__sk_buff的由来与定位
__sk_buff是 BPF 程序与内核sk_buff之间的桥梁。eBPF 程序通过__sk_buff读取或写入元数据,并通过辅助函数(如bpf_skb_load_bytes()、bpf_skb_store_bytes()等)操作数据包内容。这种设计既保证了安全性(verifier 可以严格检查访问范围),又提供了足够的灵活性。
不同的 BPF 程序类型支持__sk_buff的不同字段,例如:
- TC(Traffic Control)程序:可以访问绝大多数字段,支持
mark、priority、tc_classid等。 - cgroup skb 程序:额外提供
remote_ip4、local_port等直接的四层信息。 - XDP 程序(在 skb 模式下):支持
data、data_end等,但通常 XDP 使用原生模式(直接访问xdp_md)。
本文以 TC 程序为上下文,但大部分字段的解释也适用于其他类型。
二、__sk_buff字段详解
下面按功能类别对字段逐一说明。字段类型均为__u32,除非特别注明。
2.1 数据包内容访问
| 字段 | 说明 | 可读写 | 使用要点 |
|---|---|---|---|
data | 数据负载起始位置的偏移量(相对于 skb 起始)。 | 只读 | 在程序中通常转换为指针:void *data = (void *)(long)skb->data;,然后与data_end配合进行边界检查。 |
data_end | 数据负载结束位置的偏移量。 | 只读 | 用于检查是否越界:if (data + size > data_end) return; |
len | 数据包的总长度(包括所有头部)。 | 只读 | 可用于快速判断包大小,但注意可能包含分片。 |
wire_len | 原始包长度(GRO 合并前的长度)。 | 只读 | 需要较高版本内核(5.1+),用于 GSO 场景下的原始长度获取。 |
示例:
void*data=(void*)(long)skb->data;void*data_end=(void*)(long)skb->data_end;if(data+sizeof(structiphdr)>data_end)returnTC_ACT_OK;structiphdr*ip=data;2.2 元数据标记与优先级
| 字段 | 说明 | 可读写 | 使用场景 |
|---|---|---|---|
mark | skb 的防火墙标记,32 位值。 | 可读写 | 可在入口设置 mark,出口根据 mark 进行策略路由、过滤或连接跟踪。 |
priority | 调度优先级(skb->priority),值越小优先级越高。 | 可读写 | 影响 Qdisc 调度顺序,可用于实现 QoS。 |
tc_classid | TC 分类 ID,格式为major:minor,每个 16 位。 | 可读写 | 在分类器中设置,使数据包进入指定的 classful qdisc 队列。 |
cb[5] | 控制块(control block),5 个 32 位值的数组。 | 可读写 | 可在同一钩子的多个 eBPF 程序间传递数据(例如 ingress 到 egress)。 |
示例:
// 设置 mark 和 classidskb->mark=0x12345678;skb->tc_classid=bpf_htons(1<<16|10);// major=1, minor=10// 使用 cb 传递临时数据skb->cb[0]=0xdeadbeef;2.3 设备与协议信息
| 字段 | 说明 | 可读写 | 使用场景 |
|---|---|---|---|
ifindex | 设备索引。对于 ingress,是接收接口;对于 egress,是发送接口。 | 只读 | 判断流量来自/去向哪个接口,或用于重定向。 |
ingress_ifindex | 同ifindex,仅用于 ingress 场景。 | 只读 | 同上。 |
protocol | 链路层协议类型(如ETH_P_IP或ETH_P_ARP)。 | 只读 | 快速判断上层协议,通常需要bpf_htons()转换。 |
pkt_type | 包类型,如PACKET_HOST(发给本机)、PACKET_BROADCAST(广播)等。 | 只读 | 过滤非本机流量。 |
示例:
if(skb->protocol==bpf_htons(ETH_P_IP)){// 处理 IPv4 包}if(skb->pkt_type==PACKET_HOST){// 只处理发往本机的包}2.4 VLAN 相关
| 字段 | 说明 | 可读写 | 使用场景 |
|---|---|---|---|
vlan_present | 布尔值,表示是否存在 VLAN 标签。 | 只读 | 条件判断。 |
vlan_tci | VLAN 标签控制信息(Tag Control Information),包含 VID(12位)、PCP(3位)、DEI(1位)。 | 只读 | 读取 VLAN ID 或优先级。 |
vlan_proto | VLAN 协议类型,如ETH_P_8021Q(0x8100)或ETH_P_8021AD(0x88A8)。 | 只读 | 区分单层 VLAN 或 QinQ。 |
示例:
if(skb->vlan_present){__u16 vid=skb->vlan_tci&0xFFF;// 根据 VID 处理}2.5 GSO/TSO 相关(高版本内核)
| 字段 | 说明 | 可读写 | 使用场景 |
|---|---|---|---|
gso_size | 如果数据包是 GSO 分段,表示每个分段的大小(MSS)。 | 只读 | 用于优化处理,修改头部时需谨慎,因为会影响所有分段。 |
gso_segs | GSO 分段数。 | 只读 | 可用于统计或控制。 |
csum_level | 校验和层级(用于隧道封装)。 | 只读 | 处理隧道包的校验和时参考。 |
2.6 时间戳与哈希
| 字段 | 说明 | 可读写 | 使用场景 |
|---|---|---|---|
tstamp | 时间戳(通常为 ns 单位),需要内核配置CONFIG_NET_CLS_ACT和CONFIG_BPF等。 | 可读写(需内核支持) | 用于延迟测量、重放攻击检测等。 |
hash | 包的哈希值,由硬件或软件计算。 | 可读写 | 可重写哈希以影响 RSS 或负载均衡决策。 |
2.7 cgroup skb 专用字段
以下字段仅在 cgroup skb 程序中可用(即挂载在 cgroup 上的 BPF 程序,用于对进程发出的包进行过滤或修改)。它们提供直接的四层信息,无需解析数据包,性能更高。
| 字段 | 说明 | 可读写 |
|---|---|---|
remote_ip4 | 远程 IPv4 地址。 | 只读 |
local_ip4 | 本地 IPv4 地址。 | 只读 |
remote_ip6[4] | 远程 IPv6 地址(16 字节,分成 4 个 32 位)。 | 只读 |
local_ip6[4] | 本地 IPv6 地址。 | 只读 |
remote_port | 远程端口。 | 只读 |
local_port | 本地端口。 | 只读 |
family | 地址族(AF_INET 或 AF_INET6)。 | 只读 |
这些字段在实现 cgroup 级别的网络策略时非常方便。
三、通过 TC 程序使用__sk_buff字段
下面通过一个简单的 TC 程序示例,展示如何读取和修改部分字段。该程序在 ingress 方向检查数据包,根据protocol和mark决定是否丢弃,并在 egress 方向根据cb传递的标记修改priority。
3.1 程序代码
ingress 部分(用于 tun0 入口):
// tc_ingress.c#include<linux/bpf.h>#include<linux/if_ether.h>#include<linux/pkt_cls.h>#include<bpf/bpf_helpers.h>SEC("classifier")inttc_ingress(struct__sk_buff*skb){// 只处理 IPv4 包if(skb->protocol==bpf_htons(ETH_P_IP)){// 若 mark 为 0x1234,则丢弃if(skb->mark==0x1234)returnTC_ACT_SHOT;// 设置 cb[0] 标记,供 egress 使用skb->cb[0]=0xabcd;}returnTC_ACT_OK;}char_license[]SEC("license")="GPL";egress 部分(用于 tun0 出口):
// tc_egress.c#include<linux/bpf.h>#include<linux/pkt_cls.h>#include<bpf/bpf_helpers.h>SEC("classifier")inttc_egress(struct__sk_buff*skb){// 如果 ingress 设置了 cb[0] 标记,则提高优先级(降低数值)if(skb->cb[0]==0xabcd){// 原优先级 3,改为 1(更高优先级)skb->priority=1;// 清除标记(可选)skb->cb[0]=0;}returnTC_ACT_OK;}char_license[]SEC("license")="GPL";3.2 编译与挂载
# 编译clang-targetbpf-O2-g-Wall-ctc_ingress.c-otc_ingress.o clang-targetbpf-O2-g-Wall-ctc_egress.c-otc_egress.o# 创建 clsact qdiscsudotc qdiscadddev tun0 clsact# 挂载 ingress 和 egress 程序sudotc filteradddev tun0 ingress bpf obj tc_ingress.o sec classifiersudotc filteradddev tun0 egress bpf obj tc_egress.o sec classifier3.3 验证
- 通过
tc filter show dev tun0 ingress和egress查看挂载状态。 - 使用
bpftool prog show查看加载的程序。 - 发送测试流量,观察
skb->mark和skb->priority的变化(可通过其他工具如nstat或自定义程序验证)。
四、字段访问的注意事项
- 边界检查不可省略:当通过
data/data_end访问数据包内容时,必须进行严格的边界检查,否则 verifier 会拒绝加载。 - 字段的可写性:某些字段(如
len、protocol)是只读的,尝试写入会导致验证失败。请查阅内核文档或linux/bpf.h中的注释。 - 大小端转换:
protocol、vlan_proto等字段通常使用网络字节序,与bpf_htons()配合。 - 内核版本差异:部分字段(如
wire_len、gso_size)需要较新的内核(5.x 以上),在低版本上可能不存在或行为不同。 cb数组的范围:只有 5 个元素,不要越界访问。
五、总结
__sk_buff是 eBPF 网络程序访问数据包元数据的标准接口。通过它,我们可以读取或修改数据包的各种属性,包括:
- 数据包内容(通过
data/data_end配合辅助函数) - 标记和优先级(
mark、priority) - 流量分类(
tc_classid) - 临时状态传递(
cb) - 设备信息(
ifindex) - 协议类型(
protocol) - VLAN 信息(
vlan_tci等) - GSO 元数据(
gso_size等)
理解这些字段的作用和限制,是编写高效、安全网络程序的基础。希望本文能帮助你在 eBPF 网络开发中更加得心应手。
参考资料:
- Linux 内核源码
include/uapi/linux/bpf.h man 2 bpf- 内核文档
Documentation/bpf/ bpftool工具的使用手册
