4、eBPF网络观测核心原理:网络数据包在内核中的路径、eBPF挂载点(XDP/TC)、sk_buff结构解析、包过滤与捕获机制

4.1 网络数据包在内核中的路径

理解eBPF网络观测的前提,是掌握数据包从网卡到达用户态应用程序所经过的内核路径。这条路径涉及多个内核子系统,每个阶段都是eBPF程序的潜在挂载点。

核心路径概览: 网卡(NIC)→ 驱动层 → XDP(eXpress Data Path)→ GRO(Generic Receive Offload)→ 协议栈(IP/TCP/UDP)→ 套接字(Socket)→ 用户态。发送路径则反向。

数据包接收路径的关键阶段如下:

阶段 描述 eBPF挂载点
1. 网卡接收 数据帧到达网卡硬件,通过DMA写入内存Ring Buffer。 —
2. XDP处理 驱动层调用XDP程序,在协议栈处理前执行。可做最早期的包过滤、转发或丢弃。 XDP
3. GRO/LRO 合并多个小包为大包,减少协议栈处理次数。 —
4. 协议栈处理 IP层路由查找、分片重组;TCP/UDP层连接跟踪、校验和验证。 TC(Traffic Control)
5. 套接字投递 数据包根据五元组匹配到对应Socket,放入接收队列。 kprobe/tracepoint
6. 用户态读取 应用程序通过recvfrom/read等系统调用获取数据。 tracepoint
数据包接收路径与eBPF挂载点 数据包接收路径与eBPF挂载点 网卡 NIC XDP 挂载点 GRO 合并 TC 挂载点 协议栈 Socket 说明:XDP在驱动层最早介入,性能最高;TC在协议栈入口/出口介入,可修改包内容;kprobe/tracepoint用于观测Socket层。 eBPF网络观测制品库主要挂载XDP和TC两个点,兼顾性能与功能。

4.2 eBPF挂载点:XDP与TC

在eBPF网络观测中,XDP和TC是最核心的两个挂载点。它们分别位于数据路径的不同位置,具有不同的能力和性能特征。

4.2.1 XDP(eXpress Data Path)

  • 位置:网卡驱动层,协议栈处理之前。
  • 触发时机:数据帧从网卡DMA到内存后,立即执行eBPF程序。
  • 返回动作:XDP_PASS(放行)、XDP_DROP(丢弃)、XDP_TX(从原网卡转发)、XDP_REDIRECT(重定向到其他网卡/CPU)。
  • 优势:极低延迟,适合DDoS防御、负载均衡、早期包过滤。
  • 限制:只能访问包头部,不能修改包内容(除非使用bpf_xdp_adjust_head),不支持隧道封装等复杂操作。

4.2.2 TC(Traffic Control)

  • 位置:内核协议栈内部,分为ingress(入口)和egress(出口)两个方向。
  • 触发时机:数据包经过协议栈处理前后。
  • 返回动作:TC_ACT_OK(放行)、TC_ACT_SHOT(丢弃)、TC_ACT_REDIRECT(重定向)。
  • 优势:可以访问完整的sk_buff结构,支持修改包内容、封装/解封装、流量整形。
  • 限制:延迟略高于XDP,因为经过协议栈部分处理。
对比维度 XDP TC
挂载层级 驱动层(NIC驱动) 协议栈层(netfilter/traffic control)
数据包访问 原始帧(xdp_md) sk_buff结构
修改能力 有限(头部调整) 完整(可修改头部、数据、元数据)
性能 极高(无协议栈开销) 高(有协议栈部分开销)
典型场景 DDoS防护、包过滤、负载均衡 流量监控、QoS、隧道封装、网络策略
选型建议: 在eBPF网络观测制品库中,如果只需要统计包数量、大小、协议类型等轻量指标,优先使用XDP以获得最佳性能;如果需要解析应用层协议、修改包内容或关联连接跟踪状态,则必须使用TC。

4.3 sk_buff结构解析

sk_buff(Socket Buffer)是Linux内核网络子系统中最重要的数据结构,它承载了数据包在网络栈中传递的所有信息。eBPF程序在TC挂载点可以访问sk_buff的各个字段,从而提取观测指标。

4.3.1 sk_buff核心字段

字段 类型 说明 eBPF访问方式
len __u32 数据包总长度(包括头部) skb->len
data unsigned char * 指向数据包数据的起始位置 skb->data
head unsigned char * 指向数据缓冲区头部 skb->head
protocol __be16 L3协议类型(如ETH_P_IP、ETH_P_IPV6) skb->protocol
pkt_type __u8 包类型(单播、多播、广播) skb->pkt_type
mark __u32 防火墙标记,可用于策略路由 skb->mark
tstamp ktime_t 时间戳(接收或发送时间) skb->tstamp
dev struct net_device * 关联的网络设备 skb->dev

4.3.2 在eBPF中解析sk_buff

以下示例展示如何在TC eBPF程序中读取sk_buff的关键字段,并提取网络指标:

// tc_skb_metrics.c - 从sk_buff提取观测指标
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <bpf/bpf_helpers.h>

struct packet_metrics {
    __u64 total_bytes;
    __u64 total_packets;
    __u32 max_size;
    __u32 min_size;
};

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, struct packet_metrics);
} metrics_map SEC(".maps");

SEC("tc")
int tc_ingress_metrics(struct __sk_buff *skb) {
    __u32 key = 0;
    struct packet_metrics *metrics = bpf_map_lookup_elem(&metrics_map, &key);
    if (!metrics)
        return TC_ACT_OK;

    __u32 pkt_len = skb->len;
    __u16 protocol = skb->protocol;

    // 更新统计指标
    metrics->total_packets++;
    metrics->total_bytes += pkt_len;
    if (pkt_len > metrics->max_size)
        metrics->max_size = pkt_len;
    if (pkt_len < metrics->min_size || metrics->min_size == 0)
        metrics->min_size = pkt_len;

    // 根据协议类型记录(示例:IPv4)
    if (protocol == __constant_htons(ETH_P_IP)) {
        // 进一步解析IP头部...
    }

    return TC_ACT_OK;
}
注意: 在eBPF中访问sk_buff字段时,必须使用__sk_buff结构体(eBPF上下文),而非内核原生的sk_buff。eBPF验证器会检查所有内存访问的合法性,确保不会越界。

4.4 包过滤与捕获机制

eBPF网络观测的核心能力之一是在内核中高效地过滤和捕获数据包,避免将大量无关数据拷贝到用户态。包过滤机制基于BPF表达式和eBPF程序逻辑实现。

4.4.1 包过滤策略

  • 协议过滤: 检查sk_buff的protocol字段,只处理IPv4、IPv6、ARP等特定协议。
  • 端口过滤: 解析TCP/UDP头部,匹配源端口或目的端口。
  • IP地址过滤: 解析IP头部,匹配源IP或目的IP(支持CIDR)。
  • 包大小过滤: 根据skb->len过滤小包或超大包。
  • 连接状态过滤: 结合连接跟踪(conntrack)信息,只捕获新连接或已建立连接的数据包。

4.4.2 捕获机制实现

捕获机制通常使用Perf Buffer或Ring Buffer将过滤后的数据包元数据(而非完整包)从内核传递到用户态。这样可以大幅降低开销。

// 定义传递到用户态的包元数据结构
struct packet_event {
    __u32 src_ip;
    __u32 dst_ip;
    __u16 src_port;
    __u16 dst_port;
    __u8  protocol;
    __u32 pkt_len;
    __u64 timestamp_ns;
};

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(__u32));
} packet_events SEC(".maps");

SEC("xdp")
int xdp_capture(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    // 解析以太网头部
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    // 只捕获IPv4包
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;

    // 解析IP头部
    struct iphdr *ip = data + sizeof(struct ethhdr);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    // 填充事件结构
    struct packet_event event = {
        .src_ip = ip->saddr,
        .dst_ip = ip->daddr,
        .protocol = ip->protocol,
        .pkt_len = ctx->data_end - ctx->data,
        .timestamp_ns = bpf_ktime_get_ns()
    };

    // 如果是TCP/UDP,解析端口
    if (ip->protocol == IPPROTO_TCP || ip->protocol == IPPROTO_UDP) {
        struct tcphdr *tcp = (void *)ip + sizeof(*ip);
        if ((void *)(tcp + 1) <= data_end) {
            event.src_port = tcp->source;
            event.dst_port = tcp->dest;
        }
    }

    // 发送到用户态
    bpf_perf_event_output(ctx, &packet_events, BPF_F_CURRENT_CPU, &event, sizeof(event));

    return XDP_PASS;  // 放行数据包,不影响正常网络
}
eBPF包过滤与捕获流程 eBPF包过滤与捕获流程 原始数据包 协议过滤 IP/端口过滤 捕获元数据 Perf Buffer 丢弃(不匹配) 说明:过滤链在eBPF程序内顺序执行,不匹配的包直接返回XDP_PASS/TC_ACT_OK,不产生捕获事件。 只有完全匹配过滤条件的包才会被捕获并发送到Perf Buffer,用户态程序异步读取。

4.5 核心原理总结

本章涵盖了eBPF网络观测的四个核心原理:

  1. 数据包路径: 从网卡到套接字的完整内核路径,每个阶段都有不同的eBPF挂载机会。
  2. XDP与TC: 两个主要挂载点的位置、能力、性能差异和选型策略。
  3. sk_buff结构: 内核网络数据包的核心数据结构,eBPF程序通过__sk_buff上下文访问关键字段。
  4. 包过滤与捕获: 基于协议、IP、端口、大小等条件的过滤机制,以及通过Perf Buffer将元数据高效传递到用户态的实现方式。
关键要点: 在构建eBPF网络观测制品库时,XDP适用于轻量级、高性能的指标采集(如包计数、字节数);TC适用于需要深度包解析和修改的场景。理解sk_buff结构是编写高效TC程序的基础,而合理的过滤策略可以大幅降低观测开销,避免对生产网络造成影响。