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 场景下的原始长度获取。 |
示例:
c
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
if (data + sizeof(struct iphdr) > data_end)
return TC_ACT_OK;
struct iphdr *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)。 |
示例:
c
// 设置 mark 和 classid
skb->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(广播)等。 |
只读 | 过滤非本机流量。 |
示例:
c
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。 |
示例:
c
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 入口):
c
// tc_ingress.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/pkt_cls.h>
#include <bpf/bpf_helpers.h>
SEC("classifier")
int tc_ingress(struct __sk_buff *skb) {
// 只处理 IPv4 包
if (skb->protocol == bpf_htons(ETH_P_IP)) {
// 若 mark 为 0x1234,则丢弃
if (skb->mark == 0x1234)
return TC_ACT_SHOT;
// 设置 cb[0] 标记,供 egress 使用
skb->cb[0] = 0xabcd;
}
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";
egress 部分(用于 tun0 出口):
c
// tc_egress.c
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <bpf/bpf_helpers.h>
SEC("classifier")
int tc_egress(struct __sk_buff *skb) {
// 如果 ingress 设置了 cb[0] 标记,则提高优先级(降低数值)
if (skb->cb[0] == 0xabcd) {
// 原优先级 3,改为 1(更高优先级)
skb->priority = 1;
// 清除标记(可选)
skb->cb[0] = 0;
}
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";
3.2 编译与挂载
bash
# 编译
clang -target bpf -O2 -g -Wall -c tc_ingress.c -o tc_ingress.o
clang -target bpf -O2 -g -Wall -c tc_egress.c -o tc_egress.o
# 创建 clsact qdisc
sudo tc qdisc add dev tun0 clsact
# 挂载 ingress 和 egress 程序
sudo tc filter add dev tun0 ingress bpf obj tc_ingress.o sec classifier
sudo tc filter add dev tun0 egress bpf obj tc_egress.o sec classifier
3.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工具的使用手册