先问需要在哪个时刻看到什么对象,再选择 BPF 类型。context、helper/kfunc 和返回值各有契约,只修改 ELF section 名称不能完成类型转换。
下表是常见类型导航,完整对应关系见 libbpf 程序类型表。
| 查阅目标 | 阅读位置 |
| 类型全景与 XDP 实验 | 第 1~2 节 |
| 网络层与函数事件追踪 | 第 3~4 节 |
| LSM、struct_ops 与接口限制 | 第 5~6 节 |
1. 常见类型速查
| 类型/家族 | 执行位置与上下文 | 主要目的 |
| SOCKET_FILTER | socket 接收过滤,__sk_buff | 决定交给 socket 的数据长度 |
| XDP | 支持的 XDP 数据路径,xdp_md | 早期报文处理与重定向 |
| SCHED_CLS / SCHED_ACT | TC,__sk_buff | 分类、动作、报文修改 |
| KPROBE | kprobe/kretprobe、uprobe/uretprobe | 动态函数探测,依架构寄存器上下文 |
| TRACEPOINT / RAW_TRACEPOINT | 预定义事件或原始事件参数 | 事件观测 |
| TRACING | fentry/fexit、tp_btf 等 | BTF 类型化的函数/事件观测 |
| PERF_EVENT | 软件/硬件性能事件 | 周期或事件驱动采样 |
| CGROUP_SKB / SOCK_ADDR 等 | cgroup 相关网络操作 | socket/地址/流量策略 |
| SOCK_OPS / SK_MSG / SK_SKB | socket 回调或消息路径 | 连接处理、消息重定向等 |
| SK_LOOKUP | socket 查找 | 在受支持查找路径选择目标 socket |
| LSM | 受支持 LSM hook | 审计与访问控制扩展 |
| STRUCT_OPS | 特定内核操作表 | 实现受支持回调,如拥塞控制、调度扩展 |
| SYSCALL | 用户态通过相应 BPF 运行接口调用 | 特定 BPF 操作/加载逻辑,不是 seccomp |
2. XDP:模式决定“有多早”
native/driver XDP 常在驱动接收路径、构造常规 skb 之前运行。generic/SKB 模式则使用更晚的 skb 路径;硬件 offload 又受设备支持限制。因此不能把所有 XDP 都写成“NAPI 中零 skb 分配且无函数调用”。
| 动作 | 后续含义 |
| XDP_ABORTED / DROP | 异常终止 / 丢弃的相应处理 |
| XDP_PASS | 继续后续栈路径 |
| XDP_TX | 从接收设备发回 |
| XDP_REDIRECT | 按准备好的目标重定向 |
REDIRECT 通常需先通过 helper 等设置设备、CPU 或 AF_XDP 等目标。仅返回常数,不能自动生成目标或补齐转发协议。
最小边界检查示例
下面只检查当前可直接访问的数据至少包含一个以太网头。它不解析 VLAN、IP 或多缓冲区完整报文,也不是可部署的完整过滤策略。
#include <linux/bpf.h>
#include <linux/if_ether.h>
__attribute__((section("xdp"), used))
int check_ethernet_header(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > end)
return XDP_DROP;
return XDP_PASS;
}
char bpf_license[] __attribute__((section("license"), used)) = "GPL";
保存为 header.bpf.c,在有 Linux UAPI 头文件与 BPF 后端的 Clang 环境可编译:
clang -O2 -g -target bpf -Wall -Wextra -Werror -c header.bpf.c -o header.bpf.o
生成 ELF 只验证编译过程;真正加载还要通过 verifier,挂接还需权限和设备支持。某些发行版需补充对应架构 UAPI include 路径。调整 packet head 等 helper 可能改变数据指针有效性,后续访问应重新取得指针并重新证明边界。
下面只取源和目的端口这四个字节,逐项计算它们需要的边界。IHL 改变后,之前针对最小 IPv4 头成立的检查,未必覆盖新的端口位置。
正在呈现知识画面
固定检查 20 字节,为何不足以找到端口?IPv4 头长是 4×IHL,端口位置不能始终写死为 Ethernet+20。读取端口前,需要合法 IHL 和相应 data_end 边界证明。
{"id":"xdp-variable-header-bounds","title":"固定检查 20 字节,为何不足以找到端口?","summary":"IPv4 头长是 4×IHL,端口位置不能始终写死为 Ethernet+20。读取端口前,需要合法 IHL 和相应 data_end 边界证明。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"length\" data-i18n=\"length\"></label><output for=\"length\" id=\"length-value\"></output><input id=\"length\" type=\"range\" min=\"0\" max=\"100\" value=\"54\" step=\"1\"></div><div class=\"control\"><label for=\"ihl\" data-i18n=\"ihl\"></label><output for=\"ihl\" id=\"ihl-value\"></output><input id=\"ihl\" type=\"range\" min=\"4\" max=\"15\" value=\"5\" step=\"1\"></div><p class=\"caption\" data-i18n=\"layout\"></p><div class=\"header-map\"><div class=\"header-row\"><span data-i18n=\"eth\"></span><div><i id=\"range-eth\"></i><b class=\"end\"></b></div></div><div class=\"header-row\"><span data-i18n=\"ip\"></span><div><i id=\"range-ip\"></i><b class=\"end\"></b></div></div><div class=\"header-row\"><span data-i18n=\"ports\"></span><div><i id=\"range-ports\"></i><b class=\"end\"></b></div></div></div><div class=\"axis\"><span>0</span><span>50</span><span>100 B</span></div><div class=\"formula\" id=\"proof\"></div><div class=\"card\"><header data-i18n=\"result\"></header><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"shortEth\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"shortIP\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"badIHL\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"shortPorts\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"safe\"></p></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.header-row{margin:14px 0}.header-row span{font:12px var(--viz-font-mono)}.header-row>div{height:28px;position:relative;background:var(--surface);margin-top:7px}.header-row i{display:block;position:absolute;top:5px;bottom:5px;background:var(--accent);transition:left .2s,width .2s}.header-row b{position:absolute;top:-4px;bottom:-4px;width:2px;background:var(--second);transition:left .2s}.formula{min-height:6em;white-space:pre-line}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const len=+$('#length').value,ihl=+$('#ihl').value,port=14+ihl*4,end=port+4;set('length-value',len+' B');set('ihl-value',ihl+' → '+ihl*4+' B');for(const[k,start,size]of [['eth',0,14],['ip',14,ihl*4],['ports',port,4]]){$('#range-'+k).style.left=start+'%';$('#range-'+k).style.width=size+'%';}document.querySelectorAll('.end').forEach(e=>e.style.left=len+'%');set('proof','14 ≤ '+len+'; 34 ≤ '+len+'; IHL ≥ 5\\nports end = 14 + 4 × '+ihl+' + 4 = '+end+'\\n'+end+' ≤ '+len+' ?');set('explanation',t(len<14?'shortEth':len<34?'shortIP':ihl<5?'badIHL':len<end?'shortPorts':'safe'));}$('#length').oninput=$('#ihl').oninput=draw;draw();","audio":false,"strings":{"badIHL":"IHL 小于 5,不是合法 IPv4 头长。","eth":"Ethernet [0,14)","heading":"固定检查 20 字节,为何不足以找到端口?","ihl":"IPv4 IHL(32 位字数)","intro":"改变以太网帧可访问长度和 IPv4 IHL。比较实际数据末尾、最小头部检查和按 IHL 计算的端口结束位置。","ip":"IPv4 [14,14+4×IHL)","layout":"需要读取的范围;金线为 data_end","length":"可直接访问的数据长度 / B","ports":"Ports [14+4×IHL,18+4×IHL)","reset":"重置","result":"当前能够证明什么","safe":"在这些前提下,四个端口字节的边界已证明;这不是完整报文有效性的证明。","scope":"Ethernet II 无 VLAN,假定 EtherType=IPv4、protocol=TCP、非分片且其他协议字段有效。只证明读取源/目的端口四字节所需边界,不证明完整 TCP 头、IPv4 tot_len、校验和或整个报文有效。IHL=4 特意作为非法输入。","scopeLabel":"模型范围与约定","shortEth":"连以太网头都不完整,不能读取其字段。","shortIP":"最小 IPv4 头不完整,不能继续按完整头解析。","shortPorts":"IHL 指向的头部或端口区域超出 data_end,不能读取端口。"}}
3. TC 与 cgroup 不是同一层的网络 hook
TC 处于 skb 路径,可利用更丰富的网络上下文。SCHED_CLS 的返回解释取决于挂接模式,direct-action 使用 TC_ACT_* 语义,传统 classifier 的分类结果不能直接套用同一解释。更新 attachment 还应考虑目标系统使用传统 TC 还是支持的较新 link/TCX 等接口。
cgroup 网络程序围绕 cgroup 与 socket 关联提供检查,例如创建、bind/connect 地址、发送接收流量等。socket 的关联生命周期和继承规则会影响它属于哪个 cgroup;移动进程并不等于所有已存在 socket 立即按直觉重新归属。
CGROUP_SKB 不是挂在 cgroup v1 的 net_cls 控制器上,也不能概括为“容器所有包必经唯一 hook”。实际网络产品往往组合 TC、XDP、cgroup 和 socket 层程序,不能只凭产品名断言其全部策略由一种类型实现。
4. 函数与事件追踪:稳定性与开销要分开看
kprobe 使用架构相关寄存器状态;BPF 的 R0–R10 虚拟寄存器不是被探测 C 函数的原始参数寄存器。应通过相应 tracing 宏/上下文访问参数,而不是写“R0–R5 就是函数入参”。
kretprobe 观测返回,可能需要与入口状态关联;函数内联、禁止探测位置、尾调用和目标实现变化都会影响覆盖。uprobe/uretprobe 对用户态代码进行类似探测,适合与否取决于触发频率和测得开销,并非一律不能用于生产。
tracepoint 提供显式命名的事件及字段,比依赖任意内部函数通常更容易维护;但 trace event 格式不等于所有版本永不改变的稳定 UAPI。实际字段可查 tracefs 的 events/.../format。raw tracepoint、常规 tracepoint 和 tp_btf 的上下文表示也不同。
类型化追踪:有 BTF 还要有可挂接目标
fentry/fexit 利用 BPF trampoline 与目标 BTF,通常便于按类型读取函数参数和返回信息,但需要相应内核、目标函数和 BTF 支持。不能把“有 BTF”理解成所有函数都可挂接。见追踪与调试。
5. LSM、struct_ops 与 SYSCALL
LSM 程序按具体 hook 参数定义上下文,并无一份可以覆盖所有 hook 字段的通用 bpf_lsm_ctx 结构。系统范围 MAC 与 cgroup LSM 的挂接和返回语义应分别查阅,不能把 0/1/-errno 的含义混用。见 LSM BPF。
struct_ops 只实现内核明确支持的操作表及回调,不允许任意替换任何内核函数指针。回调是否允许睡眠、如何验证对象、谁管理注册寿命,均由目标接口规定。
BPF_PROG_TYPE_SYSCALL 的名字容易误导:它不是在每个系统调用入口过滤用户进程,而是由用户态通过相应运行接口执行、具备特定 helper/kfunc 能力的程序类别。源码可从 kernel/bpf/syscall.c 的 bpf_syscall_prog_ops 与 test-run 路径阅读。系统调用限制应查 seccomp;系统调用观测可选 tracepoint 等。
6. Helper/kfunc 可用性不能只看名字
同一个 helper 是否允许取决于程序类型、attach 类型、上下文、权限和内核能力;kfunc 还可能有版本更强的内部接口约束。sleepable BPF 也只在支持的程序与调用点中合法。
归因边界:当前任务不一定是报文的业务所有者
网络软中断里取得的 current PID/comm,不一定是产生该报文的业务进程。即使 helper 调用合法,观测语义也可能不符合预期。先明确对象归属,再决定统计 key,才能避免做出看似精确但归因错误的图表。