https://cloud.tencent.com/developer/inventory/600/article/1626925
这是一个实战系列文章,它是eBPF学习计划里面的应用场景之网络部分,终极目标是源码级别学习云原生网络方案Cilium(声明:下文提到的BPF字样是泛指,包括cBPF和eBPF)。 Cilium方案中大量使用了XDP、TC等网络相关的BPF hook,以实现高性能的网络RX和TX。 今天我们先来讲讲XDP应用理论,再来学习编写第一个XDP BPF程序。
目录
文章涉及的实验环境和代码可以到这个git repo获取。
https://github.com/nevermosby/linux-bpf-learning
在计算机网络中,Hook钩子在操作系统中用于在调用前或执行过程中拦截网络数据包。Linux内核中暴露了多个钩子,BPF程序可以连接到这些钩子上,实现数据收集和自定义事件处理。虽然Linux内核中的钩子点很多,但我们将重点关注网络子系统中存在的两个钩子:XDP)和TC。它们结合在一起,可以用来处理RX和TX上两个链路上靠近NIC的数据包,从而实现了许多网络应用的开发。今天我们先来讲讲XDP。
XDP全称为eXpress Data Path,是Linux内核网络栈的最底层。它只存在于RX路径上,允许在网络设备驱动内部网络堆栈中数据来源最早的地方进行数据包处理,在特定模式下可以在操作系统分配内存(skb)之前就已经完成处理。
XDP暴露了一个可以加载BPF程序的网络钩子。在这个钩子中,程序能够对传入的数据包进行任意修改和快速决策,避免了内核内部处理带来的额外开销。这使得XDP在性能速度方面成为最佳钩子,例如缓解DDoS攻击等,相关背景知识可以看这篇文章。
XDP暴露的钩子具有特定的输入上下文,它是单一输入参数。它的类型为 struct xdp_md,在内核头文件bpf.h 中定义,具体字段如下所示:
/* user accessible metadata for XDP packet hook
* new fields must be added to the end of this structure
*/
struct xdp_md {
__u32 data;
__u32 data_end;
__u32 data_meta;
/* Below access go through struct xdp_rxq_info */
__u32 ingress_ifindex; /* rxq->dev->ifindex */
__u32 rx_queue_index; /* rxq->queue_index */
};
程序执行时,data和data_end字段分别是数据包开始和结束的指针,它们是用来获取和解析传来的数据,第三个值是data_meta指针,初始阶段它是一个空闲的内存地址,供XDP程序与其他层交换数据包元数据时使用。最后两个字段分别是接收数据包的接口和对应的RX队列的索引。当访问这两个值时,BPF代码会在内核内部重写,以访问实际持有这些值的内核结构struct xdp_rxq_info。
在处理完一个数据包后,XDP程序会返回一个动作(Action)作为输出,它代表了程序退出后对数据包应该做什么样的最终裁决,也是在内核头文件bpf.h 定义了以下5种动作类型:
enum xdp_action {
XDP_ABORTED = 0, // Drop packet while raising an exception
XDP_DROP, // Drop packet silently
XDP_PASS, // Allow further processing by the kernel stack
XDP_TX, // Transmit from the interface it came from
XDP_REDIRECT, // Transmit packet from another interface
};
可以看出这个动作的本质是一个int值。前面4个动作是不需要参数的,最后一个动作需要额外指定一个NIC网络设备名称,作为转发这个数据包的目的地。
在没有引入XDP之前,原来是的网络数据包传输路径是这样的:
启用XDP后,网络包传输路径是这样的:
可以看到多了3个红色方框圈起来的新链路,我们来一一介绍:
当前主流内核版本的Linux系统在加载XDP BPF程序时,会自动在native和generic这两种模式选择,完成加载后,可以使用ip
命令行工具来查看选择的模式。
说完了理论,我们就要开始动手了。先来搭建实验环境。
通过上文的介绍,可以了解到XDP能够实现操控传入的数据包,它在业界最出名的一个应用场景就是Facebook基于XDP实现高效的防DDoS攻击,其本质上就是实现尽可能早地实现「丢包」,而不去消耗系统资源创建完整的网络栈链路,即「early drop」。
那么第一个XDP程序就来模拟防DDoS最重要的操作,取名为「丢掉你的整个世界」。如果你对第一个程序的设计有其他想法,建议可以去Linux源码里找找灵感:https://github.com/torvalds/linux/tree/master/samples/bpf,那里以xdp
开头的文件都是很好的参考。
闲话不说,上代码(文件名为xdp-drop-world.c):
#include <linux/bpf.h>
/*
* Comments from Linux Kernel:
* Helper macro to place programs, maps, license in
* different sections in elf_bpf file. Section names
* are interpreted by elf_bpf loader.
* End of comments
* You can either use the helper header file below
* so that you don't need to define it yourself:
* #include <bpf/bpf_helpers.h>
*/
#define SEC(NAME) __attribute__((section(NAME), used))
SEC("xdp")
int xdp_drop_the_world(struct xdp_md *ctx) {
// drop everything
// 意思是无论什么网络数据包,都drop丢弃掉
return XDP_DROP;
}
char _license[] SEC("license") = "GPL";
这是C语言代码,应该不会太难看懂吧。其中定义了一个函数,里面就一行代码,返回一个int类型,最后是代码许可证声明。做个简单的解释:
linux/bpf.h
,它包含了BPF程序使用到的所有结构和常量的定义(除了一些特定的子系统,如TC,它需要额外的头文件)。理论上来说,所有的eBPF程序第一行都是这个头文件。SEC(NAME)
这一串字符具有意义,即可以被编译通过。我截取了Linux内核代码里的注释,可以看出这段宏定义是为了ELF格式添加Section信息的。ELF全称是Executable and Linkable Format,就是可执行文件的一种主流格式(详细介绍点这里),广泛用于Linux系统,我们的BPF程序一旦通过编译后,也会是这种格式。下面代码中的SEC("xdp")
和SEC("license")
都是基于这个宏定义。xdp_drop_the_world
函数,,返回值为int类型,接受一个参数,类型为xdp_md结构,上文已经介绍过,这个例子没有使用到这个参数。函数内的就是一行返回语句,使用XDP_DROP,也就是1,意思就是丢弃所有收到的数据包。以上是对我们第一个XDP程序的简要介绍,很多相关的背景知识没有一一说明,大家可移步Linux内核源码一探究竟。
设计并编写完我们的程序代码后,接下来就是编译工作了。可以利用clang命令行工具配合后端编译器LLVM来进行操作(相关介绍看这里):
# -02: Moderate level of optimization which enables most optimizations,对生成的执行文件进行中等程度的优化
> clang -O2 -target bpf -c xdp-drop-world.c -o xdp-drop-world.o
# 查看生成的elf格式的可执行文件的相关信息
# 能看到上文提到的Section信息
> readelf -a xdp-drop-world.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Linux BPF
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 216 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 6
Section header string table index: 1
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .strtab STRTAB 0000000000000000 000000a0
0000000000000037 0000000000000000 0 0 1
[ 2] .text PROGBITS 0000000000000000 00000040
0000000000000000 0000000000000000 AX 0 0 4
[ 3] xdp PROGBITS 0000000000000000 00000040
0000000000000010 0000000000000000 AX 0 0 8
[ 4] license PROGBITS 0000000000000000 00000050
0000000000000004 0000000000000000 WA 0 0 1
[ 5] .symtab SYMTAB 0000000000000000 00000058
0000000000000048 0000000000000018 1 1 8
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific)