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