mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-05-08 14:52:40 +08:00
rename README to chinese documents
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
# eBPF 示例教程:使用 XDP 捕获 TCP 信息
|
||||
# eBPF Tutorial by Example: Capturing TCP Information with XDP
|
||||
|
||||
扩展伯克利包过滤器(eBPF)是 Linux 内核中的一项革命性技术,允许开发者在内核空间内运行沙箱程序。它提供了强大的网络、安全和跟踪能力,无需修改内核源代码或加载内核模块。本教程重点介绍如何使用 eBPF 结合 Express Data Path(XDP),在数据包进入时的最早阶段直接捕获 TCP 头信息。
|
||||
Extended Berkeley Packet Filter (eBPF) is a revolutionary technology in the Linux kernel that allows developers to run sandboxed programs within the kernel space. It enables powerful networking, security, and tracing capabilities without the need to modify the kernel source code or load kernel modules. This tutorial focuses on using eBPF with the Express Data Path (XDP) to capture TCP header information directly from network packets at the earliest point of ingress.
|
||||
|
||||
## 使用 XDP 捕获 TCP 头信息
|
||||
## Capturing TCP Headers with XDP
|
||||
|
||||
捕获网络数据包对于监控、调试和保护网络通信至关重要。传统工具如 `tcpdump` 在用户空间运行,可能会带来显著的开销。通过利用 eBPF 和 XDP,我们可以在内核中直接捕获 TCP 头信息,最小化开销并提高性能。
|
||||
Capturing network packets is essential for monitoring, debugging, and securing network communications. Traditional tools like `tcpdump` operate in user space and can incur significant overhead. By leveraging eBPF and XDP, we can capture TCP header information directly within the kernel, minimizing overhead and improving performance.
|
||||
|
||||
在本教程中,我们将开发一个 XDP 程序,该程序拦截传入的 TCP 数据包并提取其头信息。我们将这些数据存储在一个环形缓冲区中,用户空间的程序将读取并以可读的格式显示这些信息。
|
||||
In this tutorial, we'll develop an XDP program that intercepts incoming TCP packets and extracts their header information. We'll store this data in a ring buffer, which a user-space program will read and display in a human-readable format.
|
||||
|
||||
### 为什么使用 XDP 进行数据包捕获?
|
||||
### Why Use XDP for Packet Capturing?
|
||||
|
||||
XDP 是 Linux 内核中一个高性能的数据路径,允许在网络栈的最低层进行可编程的数据包处理。通过将 eBPF 程序附加到 XDP,我们可以在数据包到达时立即处理它们,减少延迟并提高效率。
|
||||
XDP is a high-performance data path within the Linux kernel that allows for programmable packet processing at the lowest level of the network stack. By attaching an eBPF program to XDP, we can process packets immediately as they arrive, reducing latency and improving efficiency.
|
||||
|
||||
## 内核 eBPF 代码分析
|
||||
## Kernel eBPF Code Analysis
|
||||
|
||||
让我们深入了解捕获 TCP 头信息的内核空间 eBPF 代码。
|
||||
Let's dive into the kernel-space eBPF code that captures TCP header information.
|
||||
|
||||
### 完整的内核代码
|
||||
### Full Kernel Code
|
||||
|
||||
```c
|
||||
#include "vmlinux.h"
|
||||
@@ -25,30 +25,30 @@ XDP 是 Linux 内核中一个高性能的数据路径,允许在网络栈的最
|
||||
|
||||
#define ETH_P_IP 0x0800
|
||||
|
||||
// 定义环形缓冲区映射
|
||||
// Define the ring buffer map
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 1 << 24); // 16 MB 缓冲区
|
||||
__uint(max_entries, 1 << 24); // 16 MB buffer
|
||||
} rb SEC(".maps");
|
||||
|
||||
// 检查数据包是否为 TCP 的辅助函数
|
||||
// Helper function to check if the packet is TCP
|
||||
static bool is_tcp(struct ethhdr *eth, void *data_end)
|
||||
{
|
||||
// 确保以太网头在边界内
|
||||
// Ensure Ethernet header is within bounds
|
||||
if ((void *)(eth + 1) > data_end)
|
||||
return false;
|
||||
|
||||
// 仅处理 IPv4 数据包
|
||||
// Only handle IPv4 packets
|
||||
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
|
||||
return false;
|
||||
|
||||
struct iphdr *ip = (struct iphdr *)(eth + 1);
|
||||
|
||||
// 确保 IP 头在边界内
|
||||
// Ensure IP header is within bounds
|
||||
if ((void *)(ip + 1) > data_end)
|
||||
return false;
|
||||
|
||||
// 检查协议是否为 TCP
|
||||
// Check if the protocol is TCP
|
||||
if (ip->protocol != IPPROTO_TCP)
|
||||
return false;
|
||||
|
||||
@@ -58,65 +58,65 @@ static bool is_tcp(struct ethhdr *eth, void *data_end)
|
||||
SEC("xdp")
|
||||
int xdp_pass(struct xdp_md *ctx)
|
||||
{
|
||||
// 数据包数据指针
|
||||
// Pointers to packet data
|
||||
void *data = (void *)(long)ctx->data;
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
|
||||
// 解析以太网头
|
||||
// Parse Ethernet header
|
||||
struct ethhdr *eth = data;
|
||||
|
||||
// 检查数据包是否为 TCP 数据包
|
||||
// Check if the packet is a TCP packet
|
||||
if (!is_tcp(eth, data_end)) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// 转换为 IP 头
|
||||
// Cast to IP header
|
||||
struct iphdr *ip = (struct iphdr *)(eth + 1);
|
||||
|
||||
// 计算 IP 头长度
|
||||
// Calculate IP header length
|
||||
int ip_hdr_len = ip->ihl * 4;
|
||||
if (ip_hdr_len < sizeof(struct iphdr)) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// 确保 IP 头在数据包边界内
|
||||
// Ensure IP header is within packet bounds
|
||||
if ((void *)ip + ip_hdr_len > data_end) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// 解析 TCP 头
|
||||
// Parse TCP header
|
||||
struct tcphdr *tcp = (struct tcphdr *)((unsigned char *)ip + ip_hdr_len);
|
||||
|
||||
// 确保 TCP 头在数据包边界内
|
||||
// Ensure TCP header is within packet bounds
|
||||
if ((void *)(tcp + 1) > data_end) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// 定义要捕获的 TCP 头字节数
|
||||
// Define the number of bytes you want to capture from the TCP header
|
||||
const int tcp_header_bytes = 32;
|
||||
|
||||
// 确保所需字节数不超过数据包边界
|
||||
// Ensure that the desired number of bytes does not exceed packet bounds
|
||||
if ((void *)tcp + tcp_header_bytes > data_end) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// 在环形缓冲区中预留空间
|
||||
// Reserve space in the ring buffer
|
||||
void *ringbuf_space = bpf_ringbuf_reserve(&rb, tcp_header_bytes, 0);
|
||||
if (!ringbuf_space) {
|
||||
return XDP_PASS; // 如果预留失败,跳过处理
|
||||
return XDP_PASS; // If reservation fails, skip processing
|
||||
}
|
||||
|
||||
// 将 TCP 头字节复制到环形缓冲区
|
||||
// 使用循环以确保符合 eBPF 验证器要求
|
||||
// Copy the TCP header bytes into the ring buffer
|
||||
// Using a loop to ensure compliance with eBPF verifier
|
||||
for (int i = 0; i < tcp_header_bytes; i++) {
|
||||
unsigned char byte = *((unsigned char *)tcp + i);
|
||||
((unsigned char *)ringbuf_space)[i] = byte;
|
||||
}
|
||||
|
||||
// 将数据提交到环形缓冲区
|
||||
// Submit the data to the ring buffer
|
||||
bpf_ringbuf_submit(ringbuf_space, 0);
|
||||
|
||||
// 可选:打印调试信息
|
||||
// Optional: Print a debug message
|
||||
bpf_printk("Captured TCP header (%d bytes)", tcp_header_bytes);
|
||||
|
||||
return XDP_PASS;
|
||||
@@ -125,70 +125,70 @@ int xdp_pass(struct xdp_md *ctx)
|
||||
char __license[] SEC("license") = "GPL";
|
||||
```
|
||||
|
||||
### 代码解释
|
||||
### Code Explanation
|
||||
|
||||
#### 定义环形缓冲区映射
|
||||
#### Defining the Ring Buffer Map
|
||||
|
||||
我们定义了一个名为 `rb` 的环形缓冲区映射,用于高效地将数据从内核传递到用户空间。
|
||||
We define a ring buffer map named `rb` to pass data from the kernel to user space efficiently.
|
||||
|
||||
```c
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 1 << 24); // 16 MB 缓冲区
|
||||
__uint(max_entries, 1 << 24); // 16 MB buffer
|
||||
} rb SEC(".maps");
|
||||
```
|
||||
|
||||
#### 数据包解析与验证
|
||||
#### Packet Parsing and Validation
|
||||
|
||||
`is_tcp` 辅助函数通过验证以太网和 IP 头,检查传入的数据包是否为 TCP 数据包。
|
||||
The `is_tcp` helper function checks whether the incoming packet is a TCP packet by verifying the Ethernet and IP headers.
|
||||
|
||||
```c
|
||||
static bool is_tcp(struct ethhdr *eth, void *data_end)
|
||||
{
|
||||
// ...(检查内容略)
|
||||
// ... (checks omitted for brevity)
|
||||
}
|
||||
```
|
||||
|
||||
#### 捕获 TCP 头信息
|
||||
#### Capturing TCP Header Information
|
||||
|
||||
在 `xdp_pass` 函数中,我们:
|
||||
In the `xdp_pass` function, we:
|
||||
|
||||
1. 解析以太网、IP 和 TCP 头。
|
||||
2. 确保所有头信息在数据包边界内,以防止无效内存访问。
|
||||
3. 在环形缓冲区中预留空间以存储 TCP 头。
|
||||
4. 将 TCP 头字节复制到环形缓冲区。
|
||||
5. 提交数据到环形缓冲区,供用户空间使用。
|
||||
1. Parse the Ethernet, IP, and TCP headers.
|
||||
2. Ensure all headers are within the packet bounds to prevent invalid memory access.
|
||||
3. Reserve space in the ring buffer to store the TCP header.
|
||||
4. Copy the TCP header bytes into the ring buffer.
|
||||
5. Submit the data to the ring buffer for user-space consumption.
|
||||
|
||||
```c
|
||||
// 在环形缓冲区中预留空间
|
||||
// Reserve space in the ring buffer
|
||||
void *ringbuf_space = bpf_ringbuf_reserve(&rb, tcp_header_bytes, 0);
|
||||
if (!ringbuf_space) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// 复制 TCP 头字节
|
||||
// Copy the TCP header bytes
|
||||
for (int i = 0; i < tcp_header_bytes; i++) {
|
||||
unsigned char byte = *((unsigned char *)tcp + i);
|
||||
((unsigned char *)ringbuf_space)[i] = byte;
|
||||
}
|
||||
|
||||
// 提交到环形缓冲区
|
||||
// Submit to ring buffer
|
||||
bpf_ringbuf_submit(ringbuf_space, 0);
|
||||
```
|
||||
|
||||
#### 使用 bpf_printk 进行调试
|
||||
#### Using bpf_printk for Debugging
|
||||
|
||||
`bpf_printk` 函数将消息记录到内核的跟踪管道,对于调试非常有用。
|
||||
The `bpf_printk` function logs messages to the kernel's trace pipe, which can be invaluable for debugging.
|
||||
|
||||
```c
|
||||
bpf_printk("Captured TCP header (%d bytes)", tcp_header_bytes);
|
||||
```
|
||||
|
||||
## 用户空间代码分析
|
||||
## User-Space Code Analysis
|
||||
|
||||
让我们查看用户空间程序,该程序从环形缓冲区中读取捕获的 TCP 头信息并显示。
|
||||
Let's examine the user-space program that reads the captured TCP headers from the ring buffer and displays them.
|
||||
|
||||
### 完整的用户空间代码
|
||||
### Full User-Space Code
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
@@ -201,17 +201,17 @@ bpf_printk("Captured TCP header (%d bytes)", tcp_header_bytes);
|
||||
#include <bpf/libbpf.h>
|
||||
#include <bpf/bpf.h>
|
||||
|
||||
#include "xdp-tcpdump.skel.h" // 生成的骨架头文件
|
||||
#include "xdp-tcpdump.skel.h" // Generated skeleton header
|
||||
|
||||
// 处理环形缓冲区事件的回调函数
|
||||
// Callback function to handle events from the ring buffer
|
||||
static int handle_event(void *ctx, void *data, size_t data_sz)
|
||||
{
|
||||
if (data_sz < 20) { // 最小 TCP 头大小
|
||||
if (data_sz < 20) { // Minimum TCP header size
|
||||
fprintf(stderr, "Received incomplete TCP header\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 解析原始 TCP 头字节
|
||||
// Parse the raw TCP header bytes
|
||||
struct tcphdr {
|
||||
uint16_t source;
|
||||
uint16_t dest;
|
||||
@@ -230,7 +230,7 @@ static int handle_event(void *ctx, void *data, size_t data_sz)
|
||||
uint16_t window;
|
||||
uint16_t check;
|
||||
uint16_t urg_ptr;
|
||||
// 可能还有选项和填充
|
||||
// Options and padding may follow
|
||||
} __attribute__((packed));
|
||||
|
||||
if (data_sz < sizeof(struct tcphdr)) {
|
||||
@@ -240,14 +240,14 @@ static int handle_event(void *ctx, void *data, size_t data_sz)
|
||||
|
||||
struct tcphdr *tcp = (struct tcphdr *)data;
|
||||
|
||||
// 将字段从网络字节序转换为主机字节序
|
||||
// Convert fields from network byte order to host byte order
|
||||
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);
|
||||
|
||||
// 提取标志位
|
||||
// Extract flags
|
||||
uint8_t flags = 0;
|
||||
flags |= (tcp->fin) ? 0x01 : 0x00;
|
||||
flags |= (tcp->syn) ? 0x02 : 0x00;
|
||||
@@ -259,13 +259,13 @@ static int handle_event(void *ctx, void *data, size_t data_sz)
|
||||
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(" Source Port: %u\n", source_port);
|
||||
printf(" Destination Port: %u\n", dest_port);
|
||||
printf(" Sequence Number: %u\n", seq);
|
||||
printf(" Acknowledgment Number: %u\n", ack_seq);
|
||||
printf(" Data Offset: %u\n", tcp->doff);
|
||||
printf(" Flags: 0x%02x\n", flags);
|
||||
printf(" Window Size: %u\n", window);
|
||||
printf("\n");
|
||||
|
||||
return 0;
|
||||
@@ -292,7 +292,7 @@ int main(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 打开并加载 BPF 应用 */
|
||||
/* Open and load BPF application */
|
||||
skel = xdp_tcpdump_bpf__open();
|
||||
if (!skel)
|
||||
{
|
||||
@@ -300,7 +300,7 @@ int main(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 加载并验证 BPF 程序 */
|
||||
/* Load & verify BPF programs */
|
||||
err = xdp_tcpdump_bpf__load(skel);
|
||||
if (err)
|
||||
{
|
||||
@@ -308,7 +308,7 @@ int main(int argc, char **argv)
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* 附加 XDP 程序 */
|
||||
/* Attach XDP program */
|
||||
err = xdp_tcpdump_bpf__attach(skel);
|
||||
if (err)
|
||||
{
|
||||
@@ -316,7 +316,7 @@ int main(int argc, char **argv)
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* 将 XDP 程序附加到指定的接口 */
|
||||
/* Attach the XDP program to the specified interface */
|
||||
skel->links.xdp_pass = bpf_program__attach_xdp(skel->progs.xdp_pass, ifindex);
|
||||
if (!skel->links.xdp_pass)
|
||||
{
|
||||
@@ -325,9 +325,9 @@ int main(int argc, char **argv)
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("成功将 XDP 程序附加到接口 %s\n", ifname);
|
||||
printf("Successfully attached XDP program to interface %s\n", ifname);
|
||||
|
||||
/* 设置环形缓冲区轮询 */
|
||||
/* Set up ring buffer polling */
|
||||
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
|
||||
if (!rb)
|
||||
{
|
||||
@@ -336,9 +336,9 @@ int main(int argc, char **argv)
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
printf("开始轮询环形缓冲区\n");
|
||||
printf("Start polling ring buffer\n");
|
||||
|
||||
/* 轮询环形缓冲区 */
|
||||
/* Poll the ring buffer */
|
||||
while (1)
|
||||
{
|
||||
err = ring_buffer__poll(rb, -1);
|
||||
@@ -358,29 +358,29 @@ cleanup:
|
||||
}
|
||||
```
|
||||
|
||||
### 代码解释
|
||||
### Code Explanation
|
||||
|
||||
#### 处理环形缓冲区事件
|
||||
#### Handling Ring Buffer Events
|
||||
|
||||
`handle_event` 函数处理从环形缓冲区接收到的 TCP 头数据。
|
||||
The `handle_event` function processes TCP header data received from the ring buffer.
|
||||
|
||||
```c
|
||||
static int handle_event(void *ctx, void *data, size_t data_sz)
|
||||
{
|
||||
// 验证数据大小
|
||||
// Validate data size
|
||||
if (data_sz < 20) {
|
||||
fprintf(stderr, "Received incomplete TCP header\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 解析 TCP 头
|
||||
// ...(解析代码)
|
||||
// Parse the TCP header
|
||||
// ... (parsing code)
|
||||
}
|
||||
```
|
||||
|
||||
#### 解析 TCP 头
|
||||
#### Parsing the TCP Header
|
||||
|
||||
我们定义了一个本地的 `tcphdr` 结构来解释原始字节。
|
||||
We define a local `tcphdr` structure to interpret the raw bytes.
|
||||
|
||||
```c
|
||||
struct tcphdr {
|
||||
@@ -388,34 +388,34 @@ struct tcphdr {
|
||||
uint16_t dest;
|
||||
uint32_t seq;
|
||||
uint32_t ack_seq;
|
||||
// ...(其他字段)
|
||||
// ... (other fields)
|
||||
} __attribute__((packed));
|
||||
```
|
||||
|
||||
#### 显示捕获的信息
|
||||
#### Displaying Captured Information
|
||||
|
||||
解析后,我们以可读的格式打印 TCP 头字段。
|
||||
After parsing, we print the TCP header fields in a readable format.
|
||||
|
||||
```c
|
||||
printf("Captured TCP Header:\n");
|
||||
printf(" 源端口: %u\n", source_port);
|
||||
printf(" 目的端口: %u\n", dest_port);
|
||||
// ...(其他字段)
|
||||
printf(" Source Port: %u\n", source_port);
|
||||
printf(" Destination Port: %u\n", dest_port);
|
||||
// ... (other fields)
|
||||
```
|
||||
|
||||
#### 设置 eBPF 骨架
|
||||
#### Setting Up the eBPF Skeleton
|
||||
|
||||
我们使用生成的骨架 `xdp-tcpdump.skel.h` 来加载和附加 eBPF 程序。
|
||||
We use the generated skeleton `xdp-tcpdump.skel.h` to load and attach the eBPF program.
|
||||
|
||||
```c
|
||||
/* 打开并加载 BPF 应用 */
|
||||
/* Open and load BPF application */
|
||||
skel = xdp_tcpdump_bpf__open();
|
||||
if (!skel) {
|
||||
fprintf(stderr, "Failed to open BPF skeleton\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 加载并验证 BPF 程序 */
|
||||
/* Load & verify BPF programs */
|
||||
err = xdp_tcpdump_bpf__load(skel);
|
||||
if (err) {
|
||||
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
|
||||
@@ -423,12 +423,11 @@ if (err) {
|
||||
}
|
||||
```
|
||||
|
||||
#### 附加到网络接口
|
||||
#### Attaching to the Network Interface
|
||||
|
||||
我们通过接口名称将 XDP 程序附加到指定的网络接口。
|
||||
We attach the XDP program to the specified network interface by name.
|
||||
|
||||
```c
|
||||
/* 将 XDP 程序附加到指定的接口 */
|
||||
skel->links.xdp_pass = bpf_program__attach_xdp(skel->progs.xdp_pass, ifindex);
|
||||
if (!skel->links.xdp_pass) {
|
||||
err = -errno;
|
||||
@@ -437,34 +436,34 @@ if (!skel->links.xdp_pass) {
|
||||
}
|
||||
```
|
||||
|
||||
## 编译和执行说明
|
||||
## Compilation and Execution Instructions
|
||||
|
||||
### 前提条件
|
||||
### Prerequisites
|
||||
|
||||
- 支持 eBPF 和 XDP 的 Linux 系统内核。
|
||||
- 安装了 libbpf 库。
|
||||
- 具有 eBPF 支持的编译器(如 clang)。
|
||||
- A Linux system with a kernel version that supports eBPF and XDP.
|
||||
- libbpf library installed.
|
||||
- Compiler with eBPF support (clang).
|
||||
|
||||
### 构建程序
|
||||
### Building the Program
|
||||
|
||||
假设您已从 [GitHub](https://github.com/eunomia-bpf/bpf-developer-tutorial) 克隆了仓库,请导航到 `bpf-developer-tutorial/src/41-xdp-tcpdump` 目录。
|
||||
Assuming you have cloned the repository from [GitHub](https://github.com/eunomia-bpf/bpf-developer-tutorial), navigate to the `bpf-developer-tutorial/src/41-xdp-tcpdump` directory.
|
||||
|
||||
```bash
|
||||
cd bpf-developer-tutorial/src/41-xdp-tcpdump
|
||||
make
|
||||
```
|
||||
|
||||
此命令将编译内核 eBPF 代码和用户空间应用程序。
|
||||
This command compiles both the kernel eBPF code and the user-space application.
|
||||
|
||||
### 运行程序
|
||||
### Running the Program
|
||||
|
||||
首先,识别您的网络接口:
|
||||
First, identify your network interfaces:
|
||||
|
||||
```bash
|
||||
ifconfig
|
||||
```
|
||||
|
||||
示例输出:
|
||||
Sample output:
|
||||
|
||||
```
|
||||
wlp0s20f3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
|
||||
@@ -472,38 +471,38 @@ wlp0s20f3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
|
||||
ether 00:1a:2b:3c:4d:5e txqueuelen 1000 (Ethernet)
|
||||
```
|
||||
|
||||
使用所需的网络接口运行用户空间程序:
|
||||
Run the user-space program with the desired network interface:
|
||||
|
||||
```bash
|
||||
sudo ./xdp-tcpdump wlp0s20f3
|
||||
```
|
||||
|
||||
示例输出:
|
||||
Sample output:
|
||||
|
||||
```
|
||||
成功将 XDP 程序附加到接口 wlp0s20f3
|
||||
开始轮询环形缓冲区
|
||||
Successfully attached XDP program to interface wlp0s20f3
|
||||
Start polling ring buffer
|
||||
Captured TCP Header:
|
||||
源端口: 443
|
||||
目的端口: 53500
|
||||
序列号: 572012449
|
||||
确认号: 380198588
|
||||
数据偏移: 8
|
||||
标志位: 0x10
|
||||
窗口大小: 16380
|
||||
Source Port: 443
|
||||
Destination Port: 53500
|
||||
Sequence Number: 572012449
|
||||
Acknowledgment Number: 380198588
|
||||
Data Offset: 8
|
||||
Flags: 0x10
|
||||
Window Size: 16380
|
||||
```
|
||||
|
||||
### 完整的源代码和资源
|
||||
### Complete Source Code and Resources
|
||||
|
||||
- **源代码仓库:** [GitHub - bpf-developer-tutorial](https://github.com/eunomia-bpf/bpf-developer-tutorial)
|
||||
- **教程网站:** [eunomia.dev Tutorials](https://eunomia.dev/tutorials/)
|
||||
- **Source Code Repository:** [GitHub - bpf-developer-tutorial](https://github.com/eunomia-bpf/bpf-developer-tutorial)
|
||||
- **Tutorial Website:** [eunomia.dev Tutorials](https://eunomia.dev/tutorials/)
|
||||
|
||||
## 总结与结论
|
||||
## Summary and Conclusion
|
||||
|
||||
在本教程中,我们探讨了如何使用 eBPF 和 XDP 在 Linux 内核中直接捕获 TCP 头信息。通过分析内核 eBPF 代码和用户空间应用程序,我们学习了如何拦截数据包、提取关键的 TCP 字段,并使用环形缓冲区高效地将这些数据传递到用户空间。
|
||||
In this tutorial, we explored how to use eBPF and XDP to capture TCP header information directly within the Linux kernel. By analyzing both the kernel eBPF code and the user-space application, we learned how to intercept packets, extract essential TCP fields, and communicate this data to user space efficiently using a ring buffer.
|
||||
|
||||
这种方法为传统的数据包捕获方法提供了一种高性能的替代方案,对系统资源的影响最小。它是网络监控、安全分析和调试的强大技术。
|
||||
This approach offers a high-performance alternative to traditional packet capturing methods, with minimal impact on system resources. It's a powerful technique for network monitoring, security analysis, and debugging.
|
||||
|
||||
如果您想了解更多关于 eBPF 的内容,请访问我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 或我们的网站 <https://eunomia.dev/tutorials/>。
|
||||
If you would like to learn more about eBPF, visit our tutorial code repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or our website at <https://eunomia.dev/tutorials/>.
|
||||
|
||||
编程愉快!
|
||||
Happy coding!
|
||||
|
||||
509
src/41-xdp-tcpdump/README.zh.md
Normal file
509
src/41-xdp-tcpdump/README.zh.md
Normal file
@@ -0,0 +1,509 @@
|
||||
# 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 代码。
|
||||
|
||||
### 完整的内核代码
|
||||
|
||||
```c
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.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;
|
||||
}
|
||||
|
||||
// 定义要捕获的 TCP 头字节数
|
||||
const int tcp_header_bytes = 32;
|
||||
|
||||
// 确保所需字节数不超过数据包边界
|
||||
if ((void *)tcp + tcp_header_bytes > data_end) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// 在环形缓冲区中预留空间
|
||||
void *ringbuf_space = bpf_ringbuf_reserve(&rb, tcp_header_bytes, 0);
|
||||
if (!ringbuf_space) {
|
||||
return XDP_PASS; // 如果预留失败,跳过处理
|
||||
}
|
||||
|
||||
// 将 TCP 头字节复制到环形缓冲区
|
||||
// 使用循环以确保符合 eBPF 验证器要求
|
||||
for (int i = 0; i < tcp_header_bytes; i++) {
|
||||
unsigned char byte = *((unsigned char *)tcp + i);
|
||||
((unsigned char *)ringbuf_space)[i] = byte;
|
||||
}
|
||||
|
||||
// 将数据提交到环形缓冲区
|
||||
bpf_ringbuf_submit(ringbuf_space, 0);
|
||||
|
||||
// 可选:打印调试信息
|
||||
bpf_printk("Captured TCP header (%d 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. 确保所有头信息在数据包边界内,以防止无效内存访问。
|
||||
3. 在环形缓冲区中预留空间以存储 TCP 头。
|
||||
4. 将 TCP 头字节复制到环形缓冲区。
|
||||
5. 提交数据到环形缓冲区,供用户空间使用。
|
||||
|
||||
```c
|
||||
// 在环形缓冲区中预留空间
|
||||
void *ringbuf_space = bpf_ringbuf_reserve(&rb, tcp_header_bytes, 0);
|
||||
if (!ringbuf_space) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// 复制 TCP 头字节
|
||||
for (int i = 0; i < tcp_header_bytes; i++) {
|
||||
unsigned char byte = *((unsigned char *)tcp + i);
|
||||
((unsigned char *)ringbuf_space)[i] = byte;
|
||||
}
|
||||
|
||||
// 提交到环形缓冲区
|
||||
bpf_ringbuf_submit(ringbuf_space, 0);
|
||||
```
|
||||
|
||||
#### 使用 bpf_printk 进行调试
|
||||
|
||||
`bpf_printk` 函数将消息记录到内核的跟踪管道,对于调试非常有用。
|
||||
|
||||
```c
|
||||
bpf_printk("Captured TCP header (%d 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 <bpf/libbpf.h>
|
||||
#include <bpf/bpf.h>
|
||||
|
||||
#include "xdp-tcpdump.skel.h" // 生成的骨架头文件
|
||||
|
||||
// 处理环形缓冲区事件的回调函数
|
||||
static int handle_event(void *ctx, void *data, size_t data_sz)
|
||||
{
|
||||
if (data_sz < 20) { // 最小 TCP 头大小
|
||||
fprintf(stderr, "Received incomplete TCP header\n");
|
||||
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));
|
||||
|
||||
if (data_sz < sizeof(struct tcphdr)) {
|
||||
fprintf(stderr, "Data size (%zu) less than TCP header size\n", data_sz);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct tcphdr *tcp = (struct tcphdr *)data;
|
||||
|
||||
// 将字段从网络字节序转换为主机字节序
|
||||
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 < 20) {
|
||||
fprintf(stderr, "Received incomplete TCP header\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 解析 TCP 头
|
||||
// ...(解析代码)
|
||||
}
|
||||
```
|
||||
|
||||
#### 解析 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/>。
|
||||
|
||||
编程愉快!
|
||||
@@ -1,508 +0,0 @@
|
||||
# eBPF Tutorial by Example: Capturing TCP Information with XDP
|
||||
|
||||
Extended Berkeley Packet Filter (eBPF) is a revolutionary technology in the Linux kernel that allows developers to run sandboxed programs within the kernel space. It enables powerful networking, security, and tracing capabilities without the need to modify the kernel source code or load kernel modules. This tutorial focuses on using eBPF with the Express Data Path (XDP) to capture TCP header information directly from network packets at the earliest point of ingress.
|
||||
|
||||
## Capturing TCP Headers with XDP
|
||||
|
||||
Capturing network packets is essential for monitoring, debugging, and securing network communications. Traditional tools like `tcpdump` operate in user space and can incur significant overhead. By leveraging eBPF and XDP, we can capture TCP header information directly within the kernel, minimizing overhead and improving performance.
|
||||
|
||||
In this tutorial, we'll develop an XDP program that intercepts incoming TCP packets and extracts their header information. We'll store this data in a ring buffer, which a user-space program will read and display in a human-readable format.
|
||||
|
||||
### Why Use XDP for Packet Capturing?
|
||||
|
||||
XDP is a high-performance data path within the Linux kernel that allows for programmable packet processing at the lowest level of the network stack. By attaching an eBPF program to XDP, we can process packets immediately as they arrive, reducing latency and improving efficiency.
|
||||
|
||||
## Kernel eBPF Code Analysis
|
||||
|
||||
Let's dive into the kernel-space eBPF code that captures TCP header information.
|
||||
|
||||
### Full Kernel Code
|
||||
|
||||
```c
|
||||
#include "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
|
||||
#define ETH_P_IP 0x0800
|
||||
|
||||
// Define the ring buffer map
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 1 << 24); // 16 MB buffer
|
||||
} rb SEC(".maps");
|
||||
|
||||
// Helper function to check if the packet is TCP
|
||||
static bool is_tcp(struct ethhdr *eth, void *data_end)
|
||||
{
|
||||
// Ensure Ethernet header is within bounds
|
||||
if ((void *)(eth + 1) > data_end)
|
||||
return false;
|
||||
|
||||
// Only handle IPv4 packets
|
||||
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
|
||||
return false;
|
||||
|
||||
struct iphdr *ip = (struct iphdr *)(eth + 1);
|
||||
|
||||
// Ensure IP header is within bounds
|
||||
if ((void *)(ip + 1) > data_end)
|
||||
return false;
|
||||
|
||||
// Check if the protocol is TCP
|
||||
if (ip->protocol != IPPROTO_TCP)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SEC("xdp")
|
||||
int xdp_pass(struct xdp_md *ctx)
|
||||
{
|
||||
// Pointers to packet data
|
||||
void *data = (void *)(long)ctx->data;
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
|
||||
// Parse Ethernet header
|
||||
struct ethhdr *eth = data;
|
||||
|
||||
// Check if the packet is a TCP packet
|
||||
if (!is_tcp(eth, data_end)) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Cast to IP header
|
||||
struct iphdr *ip = (struct iphdr *)(eth + 1);
|
||||
|
||||
// Calculate IP header length
|
||||
int ip_hdr_len = ip->ihl * 4;
|
||||
if (ip_hdr_len < sizeof(struct iphdr)) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Ensure IP header is within packet bounds
|
||||
if ((void *)ip + ip_hdr_len > data_end) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Parse TCP header
|
||||
struct tcphdr *tcp = (struct tcphdr *)((unsigned char *)ip + ip_hdr_len);
|
||||
|
||||
// Ensure TCP header is within packet bounds
|
||||
if ((void *)(tcp + 1) > data_end) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Define the number of bytes you want to capture from the TCP header
|
||||
const int tcp_header_bytes = 32;
|
||||
|
||||
// Ensure that the desired number of bytes does not exceed packet bounds
|
||||
if ((void *)tcp + tcp_header_bytes > data_end) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Reserve space in the ring buffer
|
||||
void *ringbuf_space = bpf_ringbuf_reserve(&rb, tcp_header_bytes, 0);
|
||||
if (!ringbuf_space) {
|
||||
return XDP_PASS; // If reservation fails, skip processing
|
||||
}
|
||||
|
||||
// Copy the TCP header bytes into the ring buffer
|
||||
// Using a loop to ensure compliance with eBPF verifier
|
||||
for (int i = 0; i < tcp_header_bytes; i++) {
|
||||
unsigned char byte = *((unsigned char *)tcp + i);
|
||||
((unsigned char *)ringbuf_space)[i] = byte;
|
||||
}
|
||||
|
||||
// Submit the data to the ring buffer
|
||||
bpf_ringbuf_submit(ringbuf_space, 0);
|
||||
|
||||
// Optional: Print a debug message
|
||||
bpf_printk("Captured TCP header (%d bytes)", tcp_header_bytes);
|
||||
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
char __license[] SEC("license") = "GPL";
|
||||
```
|
||||
|
||||
### Code Explanation
|
||||
|
||||
#### Defining the Ring Buffer Map
|
||||
|
||||
We define a ring buffer map named `rb` to pass data from the kernel to user space efficiently.
|
||||
|
||||
```c
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 1 << 24); // 16 MB buffer
|
||||
} rb SEC(".maps");
|
||||
```
|
||||
|
||||
#### Packet Parsing and Validation
|
||||
|
||||
The `is_tcp` helper function checks whether the incoming packet is a TCP packet by verifying the Ethernet and IP headers.
|
||||
|
||||
```c
|
||||
static bool is_tcp(struct ethhdr *eth, void *data_end)
|
||||
{
|
||||
// ... (checks omitted for brevity)
|
||||
}
|
||||
```
|
||||
|
||||
#### Capturing TCP Header Information
|
||||
|
||||
In the `xdp_pass` function, we:
|
||||
|
||||
1. Parse the Ethernet, IP, and TCP headers.
|
||||
2. Ensure all headers are within the packet bounds to prevent invalid memory access.
|
||||
3. Reserve space in the ring buffer to store the TCP header.
|
||||
4. Copy the TCP header bytes into the ring buffer.
|
||||
5. Submit the data to the ring buffer for user-space consumption.
|
||||
|
||||
```c
|
||||
// Reserve space in the ring buffer
|
||||
void *ringbuf_space = bpf_ringbuf_reserve(&rb, tcp_header_bytes, 0);
|
||||
if (!ringbuf_space) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Copy the TCP header bytes
|
||||
for (int i = 0; i < tcp_header_bytes; i++) {
|
||||
unsigned char byte = *((unsigned char *)tcp + i);
|
||||
((unsigned char *)ringbuf_space)[i] = byte;
|
||||
}
|
||||
|
||||
// Submit to ring buffer
|
||||
bpf_ringbuf_submit(ringbuf_space, 0);
|
||||
```
|
||||
|
||||
#### Using bpf_printk for Debugging
|
||||
|
||||
The `bpf_printk` function logs messages to the kernel's trace pipe, which can be invaluable for debugging.
|
||||
|
||||
```c
|
||||
bpf_printk("Captured TCP header (%d bytes)", tcp_header_bytes);
|
||||
```
|
||||
|
||||
## User-Space Code Analysis
|
||||
|
||||
Let's examine the user-space program that reads the captured TCP headers from the ring buffer and displays them.
|
||||
|
||||
### Full User-Space Code
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <net/if.h>
|
||||
|
||||
#include <bpf/libbpf.h>
|
||||
#include <bpf/bpf.h>
|
||||
|
||||
#include "xdp-tcpdump.skel.h" // Generated skeleton header
|
||||
|
||||
// Callback function to handle events from the ring buffer
|
||||
static int handle_event(void *ctx, void *data, size_t data_sz)
|
||||
{
|
||||
if (data_sz < 20) { // Minimum TCP header size
|
||||
fprintf(stderr, "Received incomplete TCP header\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse the raw TCP header bytes
|
||||
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;
|
||||
// Options and padding may follow
|
||||
} __attribute__((packed));
|
||||
|
||||
if (data_sz < sizeof(struct tcphdr)) {
|
||||
fprintf(stderr, "Data size (%zu) less than TCP header size\n", data_sz);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct tcphdr *tcp = (struct tcphdr *)data;
|
||||
|
||||
// Convert fields from network byte order to host byte order
|
||||
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);
|
||||
|
||||
// Extract flags
|
||||
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(" Source Port: %u\n", source_port);
|
||||
printf(" Destination Port: %u\n", dest_port);
|
||||
printf(" Sequence Number: %u\n", seq);
|
||||
printf(" Acknowledgment Number: %u\n", ack_seq);
|
||||
printf(" Data Offset: %u\n", tcp->doff);
|
||||
printf(" Flags: 0x%02x\n", flags);
|
||||
printf(" Window Size: %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;
|
||||
}
|
||||
|
||||
/* Open and load BPF application */
|
||||
skel = xdp_tcpdump_bpf__open();
|
||||
if (!skel)
|
||||
{
|
||||
fprintf(stderr, "Failed to open BPF skeleton\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Load & verify BPF programs */
|
||||
err = xdp_tcpdump_bpf__load(skel);
|
||||
if (err)
|
||||
{
|
||||
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Attach XDP program */
|
||||
err = xdp_tcpdump_bpf__attach(skel);
|
||||
if (err)
|
||||
{
|
||||
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Attach the XDP program to the specified interface */
|
||||
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("Successfully attached XDP program to interface %s\n", ifname);
|
||||
|
||||
/* Set up ring buffer polling */
|
||||
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("Start polling ring buffer\n");
|
||||
|
||||
/* Poll the ring buffer */
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
### Code Explanation
|
||||
|
||||
#### Handling Ring Buffer Events
|
||||
|
||||
The `handle_event` function processes TCP header data received from the ring buffer.
|
||||
|
||||
```c
|
||||
static int handle_event(void *ctx, void *data, size_t data_sz)
|
||||
{
|
||||
// Validate data size
|
||||
if (data_sz < 20) {
|
||||
fprintf(stderr, "Received incomplete TCP header\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse the TCP header
|
||||
// ... (parsing code)
|
||||
}
|
||||
```
|
||||
|
||||
#### Parsing the TCP Header
|
||||
|
||||
We define a local `tcphdr` structure to interpret the raw bytes.
|
||||
|
||||
```c
|
||||
struct tcphdr {
|
||||
uint16_t source;
|
||||
uint16_t dest;
|
||||
uint32_t seq;
|
||||
uint32_t ack_seq;
|
||||
// ... (other fields)
|
||||
} __attribute__((packed));
|
||||
```
|
||||
|
||||
#### Displaying Captured Information
|
||||
|
||||
After parsing, we print the TCP header fields in a readable format.
|
||||
|
||||
```c
|
||||
printf("Captured TCP Header:\n");
|
||||
printf(" Source Port: %u\n", source_port);
|
||||
printf(" Destination Port: %u\n", dest_port);
|
||||
// ... (other fields)
|
||||
```
|
||||
|
||||
#### Setting Up the eBPF Skeleton
|
||||
|
||||
We use the generated skeleton `xdp-tcpdump.skel.h` to load and attach the eBPF program.
|
||||
|
||||
```c
|
||||
/* Open and load BPF application */
|
||||
skel = xdp_tcpdump_bpf__open();
|
||||
if (!skel) {
|
||||
fprintf(stderr, "Failed to open BPF skeleton\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Load & verify BPF programs */
|
||||
err = xdp_tcpdump_bpf__load(skel);
|
||||
if (err) {
|
||||
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
|
||||
goto cleanup;
|
||||
}
|
||||
```
|
||||
|
||||
#### Attaching to the Network Interface
|
||||
|
||||
We attach the XDP program to the specified network interface by name.
|
||||
|
||||
```c
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
## Compilation and Execution Instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A Linux system with a kernel version that supports eBPF and XDP.
|
||||
- libbpf library installed.
|
||||
- Compiler with eBPF support (clang).
|
||||
|
||||
### Building the Program
|
||||
|
||||
Assuming you have cloned the repository from [GitHub](https://github.com/eunomia-bpf/bpf-developer-tutorial), navigate to the `bpf-developer-tutorial/src/41-xdp-tcpdump` directory.
|
||||
|
||||
```bash
|
||||
cd bpf-developer-tutorial/src/41-xdp-tcpdump
|
||||
make
|
||||
```
|
||||
|
||||
This command compiles both the kernel eBPF code and the user-space application.
|
||||
|
||||
### Running the Program
|
||||
|
||||
First, identify your network interfaces:
|
||||
|
||||
```bash
|
||||
ifconfig
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
Run the user-space program with the desired network interface:
|
||||
|
||||
```bash
|
||||
sudo ./xdp-tcpdump wlp0s20f3
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
Successfully attached XDP program to interface wlp0s20f3
|
||||
Start polling ring buffer
|
||||
Captured TCP Header:
|
||||
Source Port: 443
|
||||
Destination Port: 53500
|
||||
Sequence Number: 572012449
|
||||
Acknowledgment Number: 380198588
|
||||
Data Offset: 8
|
||||
Flags: 0x10
|
||||
Window Size: 16380
|
||||
```
|
||||
|
||||
### Complete Source Code and Resources
|
||||
|
||||
- **Source Code Repository:** [GitHub - bpf-developer-tutorial](https://github.com/eunomia-bpf/bpf-developer-tutorial)
|
||||
- **Tutorial Website:** [eunomia.dev Tutorials](https://eunomia.dev/tutorials/)
|
||||
|
||||
## Summary and Conclusion
|
||||
|
||||
In this tutorial, we explored how to use eBPF and XDP to capture TCP header information directly within the Linux kernel. By analyzing both the kernel eBPF code and the user-space application, we learned how to intercept packets, extract essential TCP fields, and communicate this data to user space efficiently using a ring buffer.
|
||||
|
||||
This approach offers a high-performance alternative to traditional packet capturing methods, with minimal impact on system resources. It's a powerful technique for network monitoring, security analysis, and debugging.
|
||||
|
||||
If you would like to learn more about eBPF, visit our tutorial code repository at <https://github.com/eunomia-bpf/bpf-developer-tutorial> or our website at <https://eunomia.dev/tutorials/>.
|
||||
|
||||
Happy coding!
|
||||
Reference in New Issue
Block a user