Files
bpf-developer-tutorial/src/41-xdp-tcpdump/README.zh.md
云微 c054586a39 Fix verified eBPF tutorial examples and instructions (#211)
* docs: fix XDP stub compile command

* docs: link hardirqs example headers

* fix: capture the actual TCP header length

* docs: refresh generated tutorial index

* Revert "docs: refresh generated tutorial index"

This reverts commit 6a2418a277.

* fix: print TCP header length as unsigned

* docs: sync TCP header debug format

* fix: address final tutorial portability review

* docs: auto-generate documentation

* Revert "docs: auto-generate documentation"

This reverts commit be93763602.

---------

Co-authored-by: LinuxDev9002 <i@littlefisher.me>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-14 14:14:09 -07:00

549 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# eBPF 示例教程:使用 XDP 捕获 TCP 信息
扩展伯克利包过滤器(eBPF)是 Linux 内核中的一项革命性技术,允许开发者在内核空间内运行沙箱程序。它提供了强大的网络、安全和跟踪能力,无需修改内核源代码或加载内核模块。本教程重点介绍如何使用 eBPF 结合 Express Data Path(XDP),在数据包进入时的最早阶段直接捕获 TCP 头信息。
## 使用 XDP 捕获 TCP 头信息
捕获网络数据包对于监控、调试和保护网络通信至关重要。传统工具如 `tcpdump` 在用户空间运行,可能会带来显著的开销。通过利用 eBPF 和 XDP,我们可以在内核中直接捕获 TCP 头信息,最小化开销并提高性能。
在本教程中,我们将开发一个 XDP 程序,该程序拦截传入的 TCP 数据包并提取其头信息。我们将这些数据存储在一个环形缓冲区中,用户空间的程序将读取并以可读的格式显示这些信息。
### 为什么使用 XDP 进行数据包捕获?
XDP 是 Linux 内核中一个高性能的数据路径,允许在网络栈的最低层进行可编程的数据包处理。通过将 eBPF 程序附加到 XDP,我们可以在数据包到达时立即处理它们,减少延迟并提高效率。
## 内核 eBPF 代码分析
让我们深入了解捕获 TCP 头信息的内核空间 eBPF 代码。
内核程序与用户空间程序通过 `xdp-tcpdump.h` 共享事件定义。TCP 头的长度可以是 20 到 60 字节,`header_len` 保存根据数据偏移字段计算出的准确长度。
```c
#define MAX_TCP_HEADER_BYTES 60
struct tcp_event {
unsigned int header_len;
unsigned char header[MAX_TCP_HEADER_BYTES];
};
```
### 完整的内核代码
```c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include "xdp-tcpdump.h"
#define ETH_P_IP 0x0800
// 定义环形缓冲区映射
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16 MB 缓冲区
} rb SEC(".maps");
// 检查数据包是否为 TCP 的辅助函数
static bool is_tcp(struct ethhdr *eth, void *data_end)
{
// 确保以太网头在边界内
if ((void *)(eth + 1) > data_end)
return false;
// 仅处理 IPv4 数据包
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
return false;
struct iphdr *ip = (struct iphdr *)(eth + 1);
// 确保 IP 头在边界内
if ((void *)(ip + 1) > data_end)
return false;
// 检查协议是否为 TCP
if (ip->protocol != IPPROTO_TCP)
return false;
return true;
}
SEC("xdp")
int xdp_pass(struct xdp_md *ctx)
{
// 数据包数据指针
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
// 解析以太网头
struct ethhdr *eth = data;
// 检查数据包是否为 TCP 数据包
if (!is_tcp(eth, data_end)) {
return XDP_PASS;
}
// 转换为 IP 头
struct iphdr *ip = (struct iphdr *)(eth + 1);
// 计算 IP 头长度
int ip_hdr_len = ip->ihl * 4;
if (ip_hdr_len < sizeof(struct iphdr)) {
return XDP_PASS;
}
// 确保 IP 头在数据包边界内
if ((void *)ip + ip_hdr_len > data_end) {
return XDP_PASS;
}
// 解析 TCP 头
struct tcphdr *tcp = (struct tcphdr *)((unsigned char *)ip + ip_hdr_len);
// 确保 TCP 头在数据包边界内
if ((void *)(tcp + 1) > data_end) {
return XDP_PASS;
}
// 根据以 32 位字为单位的数据偏移字段 doff 计算 TCP 头长度
__u32 tcp_header_bytes = tcp->doff * 4;
if (tcp_header_bytes < sizeof(*tcp) || tcp_header_bytes > MAX_TCP_HEADER_BYTES) {
return XDP_PASS;
}
// bpf_ringbuf_reserve 要求大小为常量,因此预留固定大小的事件
struct tcp_event *event = bpf_ringbuf_reserve(&rb, sizeof(*event), 0);
if (!event) {
return XDP_PASS; // 如果预留失败,跳过处理
}
event->header_len = tcp_header_bytes;
__builtin_memset(event->header, 0, sizeof(event->header));
// 将 TCP 头字节复制到环形缓冲区
// 使用循环以确保符合 eBPF 验证器要求
for (int i = 0; i < MAX_TCP_HEADER_BYTES; i++) {
if (i >= tcp_header_bytes)
break;
if ((void *)tcp + i + 1 > data_end) {
bpf_ringbuf_discard(event, 0);
return XDP_PASS;
}
unsigned char byte = *((unsigned char *)tcp + i);
event->header[i] = byte;
}
// 将数据提交到环形缓冲区
bpf_ringbuf_submit(event, 0);
// 可选:打印调试信息
bpf_printk("Captured TCP header (%u bytes)", tcp_header_bytes);
return XDP_PASS;
}
char __license[] SEC("license") = "GPL";
```
### 代码解释
#### 定义环形缓冲区映射
我们定义了一个名为 `rb` 的环形缓冲区映射,用于高效地将数据从内核传递到用户空间。
```c
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16 MB 缓冲区
} rb SEC(".maps");
```
#### 数据包解析与验证
`is_tcp` 辅助函数通过验证以太网和 IP 头,检查传入的数据包是否为 TCP 数据包。
```c
static bool is_tcp(struct ethhdr *eth, void *data_end)
{
// ...(检查内容略)
}
```
#### 捕获 TCP 头信息
在 `xdp_pass` 函数中,我们:
1. 解析以太网、IP 和 TCP 头。
2. 读取并校验 TCP 数据偏移字段,得到实际的头长度。
3. 由于辅助函数要求使用编译期常量大小,在环形缓冲区中预留固定大小的事件。
4. 逐次检查数据包边界,只把经过校验的头字节复制到事件中。
5. 提交数据到环形缓冲区,供用户空间使用。
```c
__u32 tcp_header_bytes = tcp->doff * 4;
if (tcp_header_bytes < sizeof(*tcp) || tcp_header_bytes > MAX_TCP_HEADER_BYTES) {
return XDP_PASS;
}
struct tcp_event *event = bpf_ringbuf_reserve(&rb, sizeof(*event), 0);
if (!event) {
return XDP_PASS;
}
event->header_len = tcp_header_bytes;
__builtin_memset(event->header, 0, sizeof(event->header));
for (int i = 0; i < MAX_TCP_HEADER_BYTES; i++) {
if (i >= tcp_header_bytes)
break;
if ((void *)tcp + i + 1 > data_end) {
bpf_ringbuf_discard(event, 0);
return XDP_PASS;
}
unsigned char byte = *((unsigned char *)tcp + i);
event->header[i] = byte;
}
bpf_ringbuf_submit(event, 0);
```
#### 使用 bpf_printk 进行调试
`bpf_printk` 函数将消息记录到内核的跟踪管道,对于调试非常有用。
```c
bpf_printk("Captured TCP header (%u bytes)", tcp_header_bytes);
```
## 用户空间代码分析
让我们查看用户空间程序,该程序从环形缓冲区中读取捕获的 TCP 头信息并显示。
### 完整的用户空间代码
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "xdp-tcpdump.skel.h" // 生成的骨架头文件
#include "xdp-tcpdump.h"
// 处理环形缓冲区事件的回调函数
static int handle_event(void *ctx, void *data, size_t data_sz)
{
if (data_sz < sizeof(struct tcp_event)) {
fprintf(stderr, "Received incomplete TCP event\n");
return 0;
}
struct tcp_event *event = data;
if (event->header_len < 20 || event->header_len > MAX_TCP_HEADER_BYTES) {
fprintf(stderr, "Invalid TCP header length: %u\n", event->header_len);
return 0;
}
// 解析原始 TCP 头字节
struct tcphdr {
uint16_t source;
uint16_t dest;
uint32_t seq;
uint32_t ack_seq;
uint16_t res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
uint16_t window;
uint16_t check;
uint16_t urg_ptr;
// 可能还有选项和填充
} __attribute__((packed));
struct tcphdr *tcp = (struct tcphdr *)event->header;
// 将字段从网络字节序转换为主机字节序
uint16_t source_port = ntohs(tcp->source);
uint16_t dest_port = ntohs(tcp->dest);
uint32_t seq = ntohl(tcp->seq);
uint32_t ack_seq = ntohl(tcp->ack_seq);
uint16_t window = ntohs(tcp->window);
// 提取标志位
uint8_t flags = 0;
flags |= (tcp->fin) ? 0x01 : 0x00;
flags |= (tcp->syn) ? 0x02 : 0x00;
flags |= (tcp->rst) ? 0x04 : 0x00;
flags |= (tcp->psh) ? 0x08 : 0x00;
flags |= (tcp->ack) ? 0x10 : 0x00;
flags |= (tcp->urg) ? 0x20 : 0x00;
flags |= (tcp->ece) ? 0x40 : 0x00;
flags |= (tcp->cwr) ? 0x80 : 0x00;
printf("Captured TCP Header:\n");
printf(" 源端口: %u\n", source_port);
printf(" 目的端口: %u\n", dest_port);
printf(" 序列号: %u\n", seq);
printf(" 确认号: %u\n", ack_seq);
printf(" 数据偏移: %u\n", tcp->doff);
printf(" 标志位: 0x%02x\n", flags);
printf(" 窗口大小: %u\n", window);
printf("\n");
return 0;
}
int main(int argc, char **argv)
{
struct xdp_tcpdump_bpf *skel;
struct ring_buffer *rb = NULL;
int ifindex;
int err;
if (argc != 2)
{
fprintf(stderr, "Usage: %s <ifname>\n", argv[0]);
return 1;
}
const char *ifname = argv[1];
ifindex = if_nametoindex(ifname);
if (ifindex == 0)
{
fprintf(stderr, "Invalid interface name %s\n", ifname);
return 1;
}
/* 打开并加载 BPF 应用 */
skel = xdp_tcpdump_bpf__open();
if (!skel)
{
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
/* 加载并验证 BPF 程序 */
err = xdp_tcpdump_bpf__load(skel);
if (err)
{
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
goto cleanup;
}
/* 附加 XDP 程序 */
err = xdp_tcpdump_bpf__attach(skel);
if (err)
{
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
goto cleanup;
}
/* 将 XDP 程序附加到指定的接口 */
skel->links.xdp_pass = bpf_program__attach_xdp(skel->progs.xdp_pass, ifindex);
if (!skel->links.xdp_pass)
{
err = -errno;
fprintf(stderr, "Failed to attach XDP program: %s\n", strerror(errno));
goto cleanup;
}
printf("成功将 XDP 程序附加到接口 %s\n", ifname);
/* 设置环形缓冲区轮询 */
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb)
{
fprintf(stderr, "Failed to create ring buffer\n");
err = -1;
goto cleanup;
}
printf("开始轮询环形缓冲区\n");
/* 轮询环形缓冲区 */
while (1)
{
err = ring_buffer__poll(rb, -1);
if (err == -EINTR)
continue;
if (err < 0)
{
fprintf(stderr, "Error polling ring buffer: %d\n", err);
break;
}
}
cleanup:
ring_buffer__free(rb);
xdp_tcpdump_bpf__destroy(skel);
return -err;
}
```
### 代码解释
#### 处理环形缓冲区事件
`handle_event` 函数处理从环形缓冲区接收到的 TCP 头数据。
```c
static int handle_event(void *ctx, void *data, size_t data_sz)
{
if (data_sz < sizeof(struct tcp_event)) {
fprintf(stderr, "Received incomplete TCP event\n");
return 0;
}
struct tcp_event *event = data;
if (event->header_len < 20 || event->header_len > MAX_TCP_HEADER_BYTES) {
fprintf(stderr, "Invalid TCP header length: %u\n", event->header_len);
return 0;
}
// ...(解析代码)
}
```
#### 解析 TCP 头
我们定义了一个本地的 `tcphdr` 结构来解释原始字节。
```c
struct tcphdr {
uint16_t source;
uint16_t dest;
uint32_t seq;
uint32_t ack_seq;
// ...(其他字段)
} __attribute__((packed));
```
#### 显示捕获的信息
解析后,我们以可读的格式打印 TCP 头字段。
```c
printf("Captured TCP Header:\n");
printf(" 源端口: %u\n", source_port);
printf(" 目的端口: %u\n", dest_port);
// ...(其他字段)
```
#### 设置 eBPF 骨架
我们使用生成的骨架 `xdp-tcpdump.skel.h` 来加载和附加 eBPF 程序。
```c
/* 打开并加载 BPF 应用 */
skel = xdp_tcpdump_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
/* 加载并验证 BPF 程序 */
err = xdp_tcpdump_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
goto cleanup;
}
```
#### 附加到网络接口
我们通过接口名称将 XDP 程序附加到指定的网络接口。
```c
/* 将 XDP 程序附加到指定的接口 */
skel->links.xdp_pass = bpf_program__attach_xdp(skel->progs.xdp_pass, ifindex);
if (!skel->links.xdp_pass) {
err = -errno;
fprintf(stderr, "Failed to attach XDP program: %s\n", strerror(errno));
goto cleanup;
}
```
## 编译和执行说明
### 前提条件
- 支持 eBPF 和 XDP 的 Linux 系统内核。
- 安装了 libbpf 库。
- 具有 eBPF 支持的编译器(如 clang)。
### 构建程序
假设您已从 [GitHub](https://github.com/eunomia-bpf/bpf-developer-tutorial) 克隆了仓库,请导航到 `bpf-developer-tutorial/src/41-xdp-tcpdump` 目录。
```bash
cd bpf-developer-tutorial/src/41-xdp-tcpdump
make
```
此命令将编译内核 eBPF 代码和用户空间应用程序。
### 运行程序
首先,识别您的网络接口:
```bash
ifconfig
```
示例输出:
```
wlp0s20f3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.10 netmask 255.255.255.0 broadcast 192.168.1.255
ether 00:1a:2b:3c:4d:5e txqueuelen 1000 (Ethernet)
```
使用所需的网络接口运行用户空间程序:
```bash
sudo ./xdp-tcpdump wlp0s20f3
```
示例输出:
```
成功将 XDP 程序附加到接口 wlp0s20f3
开始轮询环形缓冲区
Captured TCP Header:
源端口: 443
目的端口: 53500
序列号: 572012449
确认号: 380198588
数据偏移: 8
标志位: 0x10
窗口大小: 16380
```
### 完整的源代码和资源
- **源代码仓库:** [GitHub - bpf-developer-tutorial](https://github.com/eunomia-bpf/bpf-developer-tutorial)
- **教程网站:** [eunomia.dev Tutorials](https://eunomia.dev/tutorials/)
## 总结与结论
在本教程中,我们探讨了如何使用 eBPF 和 XDP 在 Linux 内核中直接捕获 TCP 头信息。通过分析内核 eBPF 代码和用户空间应用程序,我们学习了如何拦截数据包、提取关键的 TCP 字段,并使用环形缓冲区高效地将这些数据传递到用户空间。
这种方法为传统的数据包捕获方法提供了一种高性能的替代方案,对系统资源的影响最小。它是网络监控、安全分析和调试的强大技术。
如果您想了解更多关于 eBPF 的内容,请访问我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或我们的网站 <https://eunomia.dev/tutorials/>
编程愉快!