BruceFan's Blog

Stay hungry, stay foolish

0%

eBPF XDP入门

eBPF使用XDP(eXpress Data Path)解析数据包是eBPF技术中一个非常重要的应用场景。XDP是Linux内核中一个高性能数据包处理机制,它允许开发者在网络报文进入内核协议栈之前,对其进行处理和操作。通过在数据包的接收路径上运行eBPF程序,XDP提供极低的延迟和高效的网络流量处理能力。

什么是 XDP?

XDP全称是eXpress Data Path,它是Linux内核中一个高性能、高吞吐量的数据包处理框架。XDP的工作原理是将eBPF程序附加到网卡驱动的接收路径(RX Path)的最底层,在数据包进入内核协议栈之前对其进行处理。

XDP的关键特性:

  • 低延迟、高性能:XDP在驱动层直接处理数据包,避免了数据包进入内核协议栈和用户态的额外开销。
  • 灵活性:通过eBPF程序实现定制化的数据包处理逻辑。
  • 高吞吐量:适用于高性能网络应用,例如DDoS防御、负载均衡、数据包过滤等。

eBPF与XDP的结合

eBPF程序可以被加载到XDP hook上,用来分析、修改或丢弃数据包。以下是一些常见的操作:

  • 数据包解析:解析数据包头部(如 Ethernet、IP、TCP/UDP 等协议头部)。
  • 数据包过滤:根据协议或字段过滤特定的数据包(如丢弃不需要的流量)。
  • 负载均衡:将数据包转发到不同的目的地。
  • DDoS防护:识别和丢弃恶意流量。

通过XDP,eBPF程序可以在数据包接收的最早阶段进行处理,从而提供极高的效率。

使用 XDP 解析数据包的实践步骤

以下是一个使用XDP解析数据包的完整流程:
1.编写eBPF程序
eBPF程序需要用C语言编写,然后通过编译工具编译成字节码加载到内核,这里借助libbpf进行编译。
以下是一个简单的eBPF程序示例,用于解析以太网帧和IPv4数据包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define ETH_P_IP 0x0800

SEC("xdp")
int xdp_packet_parser(struct xdp_md *ctx) {
// get packet start and end addr
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;

// parse Ethernet header
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) {
return XDP_ABORTED;
}
// check eth proto type
if (eth->h_proto == bpf_htons(ETH_P_IP)) { // IPv4
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) {
return XDP_ABORTED;
}
// check ip proto type
if (ip->protocol == IPPROTO_TCP) { // TCP
struct tcphdr *tcp = (void *)((void *)ip + (ip->ihl * 4));
if ((void *)(tcp + 1) > data_end) {
return XDP_ABORTED;
}
// example: record TCP src port and dst port
bpf_printk("TCP src port: %d, dst port: %d", bpf_ntohs(tcp->source), bpf_ntohs(tcp->dest));
} else if (ip->protocol == IPPROTO_UDP) { // UDP
struct udphdr *udp = (void *)((void *)ip + (ip->ihl * 4));
if ((void *)(udp + 1) > data_end) {
return XDP_ABORTED;
}
bpf_printk("UDP src port: %d, dst port: %d", bpf_ntohs(udp->source), bpf_ntohs(udp->dest));
}
}
return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

2.编写用户态程序
使用libbpf将eBPF程序加载到XDP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/resource.h>
#include <net/if.h>

#include <bpf/libbpf.h>
#include <bpf/bpf.h>

#include "xdp.skel.h"

static volatile sig_atomic_t exiting = 0;

static void sig_int(int signo) {
exiting = 1;
}

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) {
return vfprintf(stderr, format, args);
}

int main(int argc, char **argv) {
struct xdp_bpf *skel;
int ifindex;
int err;
if (argc != 2) {
fprintf(stderr, "Usage: %s <ifname>\n", argv[0]);
return 1;
}
libbpf_set_print(libbpf_print_fn);

const char *ifname = argv[1];
ifindex = if_nametoindex(ifname);
if (ifindex == 0) {
fprintf(stderr, "Invalid interface name %s\n", ifname);
return 1;
}
skel = xdp_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
err = xdp_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton: %d\n", err);
goto cleanup;
}
err = xdp_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
goto cleanup;
}
skel->links.xdp_packet_parser = bpf_program__attach_xdp(skel->progs.xdp_packet_parser, ifindex);
if (!skel->links.xdp_packet_parser) {
err = -errno;
fprintf(stderr, "Failed to attach XDP program: %s", strerror(errno));
goto cleanup;
}
if (signal(SIGINT, sig_int) == SIG_ERR) {
fprintf(stderr, "can't set signal handler: %s\n", strerror(errno));
goto cleanup;
}
printf("Successfully attached XDP program to interface %s\n", ifname);
while (!exiting) {
fprintf(stderr, ".");
sleep(1);
}
cleanup:
xdp_bpf__destroy(skel);

return -err;
}

3.编译运行
我这里的编译是将两个文件丢到libbpf-bootstrap项目的examples/c中编译的,需要在Makefile中添加xdp程序的文件名,然后执行make <xdp文件名>生成可执行文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ make xdp
BPF .output/xdp.bpf.o
GEN-SKEL .output/xdp.skel.h
CC .output/xdp.o
BINARY xdp
$ sudo ./xdp eno1
libbpf: CO-RE relocating [22] struct udphdr: found target candidate [12168] struct udphdr in [vmlinux]
libbpf: prog 'xdp_packet_parser': relo #8: <byte_off> [22] struct udphdr.source (0:0 @ offset 0)
libbpf: prog 'xdp_packet_parser': relo #8: matching candidate #0 <byte_off> [12168] struct udphdr.source (0:0 @ offset 0)
libbpf: prog 'xdp_packet_parser': relo #8: patched insn #47 (LDX/ST/STX) off 0 -> 0
libbpf: prog 'xdp_packet_parser': relo #9: <byte_off> [22] struct udphdr.dest (0:1 @ offset 2)
libbpf: prog 'xdp_packet_parser': relo #9: matching candidate #0 <byte_off> [12168] struct udphdr.dest (0:1 @ offset 2)
libbpf: prog 'xdp_packet_parser': relo #9: patched insn #49 (LDX/ST/STX) off 2 -> 2
libbpf: map 'xdp_bpf.rodata': created successfully, fd=3
Successfully attached XDP program to interface eno1
.....................................

另起一个终端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ sudo cat /sys/kernel/tracing/trace_pipe
<idle>-0 [005] ..s21 4494866.066820: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.068955: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.072233: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.075536: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.078468: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.081657: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.084106: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.086325: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.088399: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.090771: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.093293: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.099956: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.102167: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.104241: bpf_trace_printk: TCP src port: 57771, dst port: 22
<idle>-0 [005] ..s21 4494866.105401: bpf_trace_printk: TCP src port: 57771, dst port: 22

XDP的返回动作

XDP程序的返回值决定了如何处理数据包。常见的动作包括:
| 返回值 | 含义 |
|:—-|:—-|
| XDP_PASS | 允许数据包继续进入内核协议栈处理。 |
| XDP_DROP | 丢弃数据包,不进一步处理。 |
| XDP_TX | 将数据包直接发回到接收的网络接口(环回)。 |
| XDP_REDIRECT | 将数据包重定向到另一个网络接口或用户态处理。 |
| XDP_ABORTED | 表示程序发生错误,数据包处理失败。 |

XDP加载的几种模式

在eBPF框架中,XDP(eXpress Data Path)程序可以以不同的模式加载到网络接口上。这些模式决定了XDP程序的运行位置和性能表现。
| 模式 | 描述 | 优点 | 适用场景 |
|:—-|:—-|:—-|:—-|
| XDP Native | XDP程序直接加载到网卡驱动的最低层(数据包接收的最初阶段),绕过内核协议栈 | 性能最高,因为数据包处理在驱动层完成,最大限度减少了延迟和 CPU 消耗 | 高性能场景:DDoS 防护、负载均衡等 |
| XDP Generic | XDP程序运行在内核协议栈的XDP模拟层,不依赖网卡驱动的支持 | 兼容性更好,即使网卡驱动不支持XDP,也可以运行;便于开发和调试 | 测试开发、实验环境,或硬件不支持XDP时使用 |
| XDP Offload | XDP程序卸载到支持XDP的智能网卡(SmartNIC)进行运行,由网卡硬件处理XDP操作 | 减少主机CPU负载,进一步提升性能;由硬件完成数据包处理,可以释放更多的计算资源给其他任务 | 大流量场景、高端硬件环境 |

可以使用ip命令加载XDP字节码到相应位置,这里用上面编写的eBPF程序,用clang命令编译:

1
$ clang -O2 -target bpf -c xdp.bpf.c -o xdp.bpf.o

可能会报找不到vmlinux.h的错误,使用bpftool在当前目录生成一个即可:

1
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

1.Native模式加载

1
2
$ sudo ip link set dev eno1 xdpdrv obj xdp.bpf.o sec xdp
Error: Underlying driver does not support XDP in native mode.

我这里的驱动不支持,所以会报错。
2.Generic模式加载

1
$ sudo ip link set dev eno1 xdp obj xdp.bpf.o sec xdp

3.Offload模式加载

1
2
3
4
$ sudo ip link set dev eno1 xdpoffload obj xdp.bpf.o sec xdp
libbpf: load bpf program failed: Operation not supported
libbpf: failed to load program 'xdp_packet_parser'
libbpf: failed to load object 'xdp.bpf.o'

也是网卡不支持。
4.查看XDP加载情况

1
2
3
4
5
$ sudo ip link show dev eno1
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether d8:bb:c1:bb:76:76 brd ff:ff:ff:ff:ff:ff
prog/xdp id 4382 tag 625502b268781999 jited <---
altname enp0s31f6

5.卸载XDP

1
$ sudo ip link set dev eno1 xdp off

总结

这一篇中介绍了XDP基本原理和编译加载方式,后面会继续探索XDP的实际用途。

reference
https://blog.nsfocus.net/bpf-enable-software-definition-kernel/