4. eBPF程序类型详解

eBPF 技术的核心价值在于其程序类型(Program Type)与挂载点(Attachment Point)的多样化设计。不同的程序类型决定了 eBPF 虚拟机在内核中的执行上下文、可访问的数据结构以及触发时机。本章将深入剖析五种最关键的 eBPF 程序类型:XDP、TC、Socket Filter、kprobe 与 tracepoint,并对比其在内核数据路径中的位置与适用场景。

4.1 程序类型与挂载点概览

eBPF 程序不能独立运行,必须挂载到内核预定义的钩子(Hook)上。下表列出了本章重点讨论的五种程序类型及其核心特征:

程序类型 挂载点层级 触发时机 主要用途 可访问数据
XDP 网卡驱动层 数据包到达网卡,未分配 sk_buff 高速包过滤、DDoS 防护、负载均衡 原始报文(xdp_md)
TC 内核协议栈层 数据包进入/离开协议栈(已分配 sk_buff) 流量整形、QoS、隧道封装 sk_buff 结构
Socket Filter Socket 层 数据包到达 Socket 接收队列 应用层过滤、协议分析 Socket 缓冲区数据
kprobe 内核指令级 内核函数入口/出口/指定偏移 动态追踪、性能分析、内核调试 函数参数、寄存器、栈
tracepoint 内核静态插桩 内核预定义事件(如系统调用、调度) 稳定监控、可观测性 结构化事件上下文
关键概念:XDP 与 TC 均工作在数据路径上,但 XDP 在 sk_buff 分配之前执行,因此延迟更低、吞吐更高;而 TC 拥有完整的协议栈上下文,适合需要修改 L3/L4 头的复杂操作。

4.2 XDP(eXpress Data Path)

XDP 是 eBPF 在网络数据路径中最早、最快的挂载点。它在网卡驱动层直接注入 eBPF 程序,在数据包尚未被内核协议栈处理之前即做出决策。

4.2.1 执行位置与数据流

下图展示了 XDP 程序在数据包接收路径中的位置:

XDP 数据包处理路径 网卡硬件 XDP 程序 sk_buff 分配 协议栈处理 XDP 返回码 XDP_DROP:丢弃 XDP_PASS:放行 XDP_TX:回送 丢弃/回送 XDP 优势:零拷贝、无 sk_buff 开销、支持 AF_XDP 用户态零拷贝 典型场景:Cloudflare DDoS 防护、Cilium 负载均衡、Facebook Katran

4.2.2 XDP 返回码与动作

XDP 程序通过返回码决定数据包的命运,这是其高性能的关键:

  • XDP_DROP:在驱动层直接丢弃数据包,适用于 DDoS 清洗。
  • XDP_PASS:允许数据包继续进入内核协议栈,分配 sk_buff。
  • XDP_TX:将数据包从接收网卡直接转发出去,实现快速转发。
  • XDP_REDIRECT:将数据包重定向到其他网卡或 AF_XDP Socket。
  • XDP_ABORTED:程序执行出错,数据包被丢弃并记录异常。

4.3 TC(Traffic Control)

TC 是 Linux 内核流量控制子系统的 eBPF 扩展。与 XDP 不同,TC 程序挂载在协议栈内部,拥有完整的 sk_buff 结构,可以访问 L3/L4 头部信息。

4.3.1 TC 的挂载点:ingress 与 egress

TC 支持两个方向的挂载:

  • TC ingress:数据包从协议栈进入 Socket 层之前执行。
  • TC egress:数据包从 Socket 层发出、离开协议栈之前执行。
TC 程序挂载点:ingress 与 egress 内核协议栈 Socket 层 TC ingress TC egress TC 与 XDP 的关键区别 TC 拥有 sk_buff,可修改 L3/L4 头;支持 BPF 直连(BPF_PROG_TYPE_SCHED_CLS) 典型场景:Cilium 网络策略、Istio sidecar 流量拦截、容器网络隧道封装

4.3.2 TC 程序示例

以下是一个简单的 TC egress 程序,用于丢弃特定端口的出站数据包:

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

SEC("classifier")
int tc_egress_drop(struct __sk_buff *skb) {
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;

    struct ethhdr *eth = data;
    if (data + sizeof(*eth) > data_end)
        return TC_ACT_OK;

    // 检查 IP 协议
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return TC_ACT_OK;

    struct iphdr *ip = data + sizeof(*eth);
    if (data + sizeof(*eth) + sizeof(*ip) > data_end)
        return TC_ACT_OK;

    // 丢弃目标端口 8080 的 TCP 数据包
    if (ip->protocol == IPPROTO_TCP) {
        struct tcphdr *tcp = (void *)ip + sizeof(*ip);
        if (data + sizeof(*eth) + sizeof(*ip) + sizeof(*tcp) > data_end)
            return TC_ACT_OK;
        if (tcp->dest == __constant_htons(8080))
            return TC_ACT_SHOT;  // 丢弃
    }

    return TC_ACT_OK;
}

4.4 Socket Filter

Socket Filter 是 eBPF 最早的应用之一,起源于经典的 BPF(cBPF)包过滤机制。它挂载在 Socket 层,允许用户空间程序对接收或发送的数据包进行过滤。

4.4.1 挂载方式

Socket Filter 通过 setsockopt() 系统调用挂载到指定的 Socket 上:

// 用户态挂载 Socket Filter
int prog_fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, prog, insns_cnt, "GPL");
if (prog_fd < 0) {
    perror("bpf_prog_load");
    exit(1);
}

// 挂载到 Socket
setsockopt(sock_fd, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));

4.4.2 典型应用

  • tcpdump:底层使用 BPF 指令进行包过滤。
  • 应用层防火墙:在用户态 Socket 层面实现白名单/黑名单。
  • 协议嗅探:只接收特定协议的数据包,减少用户态唤醒次数。
性能对比:Socket Filter 工作在 Socket 层,性能优于用户态过滤,但低于 XDP 和 TC。它适合应用级过滤,不适合高吞吐网络数据路径。

4.5 kprobe(动态探针)

kprobe 允许 eBPF 程序动态挂载到内核函数的任意位置,包括函数入口(kprobe)、函数返回(kretprobe)以及函数内部的指定偏移。这是内核动态追踪的核心机制。

4.5.1 挂载点类型

探针类型 触发时机 访问能力 稳定性
kprobe 函数入口或指定指令偏移 函数参数、寄存器、全局变量 依赖内核版本,函数可能被内联
kretprobe 函数返回前 返回值、函数执行时间 依赖内核版本,需处理并发
fentry/fexit 函数入口/出口(BPF 原生) 更高效的参数/返回值访问 需要 BTF 支持,更稳定

4.5.2 使用场景

  • 性能分析:追踪 tcp_sendmsg、tcp_recvmsg 等网络函数延迟。
  • 内核调试:在不重新编译内核的情况下,动态注入调试逻辑。
  • 安全监控:监控敏感系统调用或文件操作。
kprobe 挂载点示意图 tcp_v4_connect() kprobe 入口 kretprobe 出口 kprobe 可挂载到任意内核函数,但需注意函数内联和版本兼容性;推荐使用 fentry/fexit 替代

4.6 tracepoint(静态探针)

tracepoint 是内核预定义的静态插桩点,由内核开发者在内核源代码中显式声明。与 kprobe 相比,tracepoint 具有更好的稳定性和安全性,因为其参数格式是固定的。

4.6.1 常见网络相关 tracepoint

tracepoint 名称 触发事件 可访问参数
net:netif_receive_skb 数据包到达协议栈 skb 地址、协议、设备
net:net_dev_queue 数据包进入设备队列 skb 地址、队列号
syscalls:sys_enter_connect connect 系统调用入口 fd、sockaddr、addrlen
syscalls:sys_exit_connect connect 系统调用返回 返回值
tcp:tcp_rcv_established TCP 接收已建立连接的数据 sk、skb、tcp 状态

4.6.2 tracepoint 程序示例

// tracepoint_net.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

SEC("tracepoint/net/netif_receive_skb")
int trace_netif_receive_skb(struct trace_event_raw_net_dev_template *ctx) {
    // 记录数据包接收事件
    bpf_printk("Packet received on device: %s\n", ctx->name);
    return 0;
}
稳定性提示:tracepoint 的 API 由内核维护,不会随版本随意变更。对于生产环境中的可观测性需求,优先使用 tracepoint,仅在 tracepoint 无法覆盖的场景下使用 kprobe。

4.7 程序类型选择决策树

在实际项目中,选择合适的 eBPF 程序类型至关重要。以下决策树可帮助快速判断:

eBPF 程序类型选择决策树 eBPF 程序类型选择 需要网络数据包处理? 需要内核函数追踪? 驱动层处理? 协议栈层处理? 静态插桩可用? XDP TC tracepoint kprobe

4.8 总结与最佳实践

核心要点:
  • XDP:追求极致性能,适合 DDoS 防护、负载均衡、高速转发。
  • TC:需要完整协议栈上下文,适合网络策略、隧道、QoS。
  • Socket Filter:应用层过滤,适合 tcpdump 类工具。
  • kprobe:动态追踪,适合调试和临时监控。
  • tracepoint:稳定可观测,适合生产环境监控。

在实际部署中,建议优先使用 tracepoint 和 XDP/TC 等稳定接口,仅在必要时使用 kprobe。同时,注意 eBPF 程序的兼容性:XDP 需要网卡驱动支持,TC 需要内核开启 BPF 直连支持,tracepoint 依赖内核版本。