大杂烩,基本翻译自
A brief introduction to XDP and eBPF
Kernel Bypass
在过去几年中,我们看到了编程工具包和技术的升级,以克服Linux kernel的限制,来进行高性能数据包处理。最流行的技术之一是kernel bypass内核旁路),这意味着跳过内核的网络层,在用户态user-sapce)做全部的包处理。kernel bypass涉及从user-space管理NICnetwork interface controller,也就是常说的网卡),也就是说需要用户态的驱动程序user space driver)来处理NIC
用户态程序完全控制NIC,有什么好处呢?减少了内核开销;等
坏处呢?用户程序需要直接管理硬件;kernel被完全跳过,所以内核提供的所有网络功能也被跳过,用户程序可能需要实现一些原来内核提供的功能;
本质上kernel bypass实现高性能包处理是通过将数据包从kernel移动到user-space
XDP后面会讲)实际上正好相反,XDP允许我们在数据包到达NIC时,在它移动到 kernel’s networking subsystem之前,执行我们定义的处理函数,从而显著提高数据包处理速度。但是用户态定义的程序如何在内核中执行呢?
这就用到了BPF,BPF就是一种在内核中运行用户指定的程序的设计
BPF
Berkeley packet filter,用于过滤网络报文packet)
是tcpdumplinux)和wiresharkwindows)乃至整个网络监控network monitoring)的基石
BPF实际上并不只是包处理,而更像一个VMvirtual machine)
BPF虚拟机及其字节码由Steve McCanne和Van Jacobson于1992年底在其论文《The BSD Packet Filter: A New Architecture for User-level Packet Capture》中介绍,并首次在1993年冬季Usenix会议上提出。
由于BPF是一个VM,它定义了一个程序执行的环境。除了字节码,它还定义了基于数据包的内存模型packet-based memory model)、寄存器A and X; Accumulator ans Index register)、暂存内存scratch memory)、隐式程序计数器implicit pc)。有趣的是,BPF的字节码是模仿摩托罗拉6502ISA的。Steve McCanne在他的 Sharkfest ‘11 keynote主题演讲中回忆道,他在初中时就熟悉6502 assembly在Apple II上的编程,这在他设计BPF字节码时对他产生了影响
Linux内核从v2.5开始就支持BPF,主要由Jay Schullist添加。直到2011年,BPF代码才发生重大变化,Eric Dumazet将BPF解释器转换为JIT(来源:A JIT for packet filters)。现在内核不再解释BPF字节码,而是能够将BPF程序直接转换为目标体系结构:x86、ARM、MIPS等。
随后,在2014年,Alexei Starovoitov引入了新的BPF JIT。这种新的JIT实际上是一种基于BPF的新体系结构,称为eBPF。我认为这两个虚拟机共存了一段时间,但现在包过滤是在eBPF之上实现的。事实上,许多文档现在将eBPF称为BPF,而经典的BPF称为cBPF。
eBPF
eBPF在以下几个方面扩展了传统的BPF虚拟机:
利用现代64位体系结构。eBPF使用64位寄存器,并将可用寄存器的数量从2(累加器和X寄存器)增加到10。eBPF还扩展了操作码的数量(BPF_MOV、BPF_JNE、BPF_CALL…
与网络子系统分离。BPF被绑定到基于数据包的数据模型。由于它被用于数据包过滤,其代码位于网络子系统中。但是,eBPF VM不再局限于数据模型,它可以用于任何目的。现在可以将eBPF程序连接到跟踪点或kprobe。这为eBPF在其他内核子系统中的插装、性能分析和更多用途打开了大门。eBPF代码现在位于自己的路径:kernel/bpf
增加Maps用来存储全局数据。Maps是键值对的存储方式,允许在user-sapce和kernel-space做数据交互。eBPF提供了多种类型的Map
增加辅助函数helper function)。例如数据包重写、校验和计算或数据包克隆。与用户空间编程不同,这些函数在内核中执行。此外,还可以从eBPF程序执行系统调用
增加尾调用tail call)。eBPF程序限制为4096字节。尾部调用功能允许eBPF程序通过控制一个新的eBPF程序,从而克服此限制(最多可以链接32个程序)
eBPF怎么使用呢?
看一个例子,也是Linux kernel自带的样例。它们可在 samples/bpf/上获得。要编译这些示例,可参考我前面的一篇文章。
我们选择 tracex4程序分析,eBPF编程通常包括两个程序:eBPF程序和user-sapce程序
tracex4_kern.c, contains the source code to be executed in the kernel as eBPF bytecode.
tracex4_user.c, contains the user-space program.
首先我们需要将tracex4_kern.c
编程成eBPF bytecode,gcc缺乏BPF后端,幸运的是,Clang支持,自带的Makefile利用Clang将trace4_kern.c
编译成一个目标文件object file)
阅读以下tracex4_kern.c
源码:
Maps are key/value stores that allow to exchange data between user-space and kernel-space programs. tracex4_kern defines one map:
struct pair { u64 val; u64 ip; }; struct bpf_map_def SEC"maps") my_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeoflong), .value_size = sizeofstruct pair), .max_entries = 1000000, };
BPF_MAP_TYPE_HASH是eBPF提供的多个Map中的一个,你还能看到 SEC”map”),SEC是一个宏用来在二进制文件目标文件,.o文件)中生成一个新的section
tracex4_kern.c
还定义了另外两个section:
SEC"kprobe/kmem_cache_free") int bpf_prog1struct pt_regs *ctx) { long ptr = PT_REGS_PARM2ctx); bpf_map_delete_elem&my_map, &ptr); return 0; } SEC"kretprobe/kmem_cache_alloc_node") int bpf_prog2struct pt_regs *ctx) { long ptr = PT_REGS_RCctx); long ip = 0; // get ip address of kmem_cache_alloc_node) caller BPF_KRETPROBE_READ_RET_IPip, ctx); struct pair v = { .val = bpf_ktime_get_ns), .ip = ip, }; bpf_map_update_elem&my_map, &ptr, &v, BPF_ANY); return 0; }
这两个函数允许我们在map中增加一个entrykprobe/kmem_cache_free)
和添加一条entrykretprobe/kmem_cache_alloc_node)
。
所有大写字母的函数实际上都是宏,定义在 bpf_helpers.h中
如果我们反汇编目标文件,我们可以看见新的section被定义:
$ objdump -h tracex4_kern.o tracex4_kern.o: file format elf64-little Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2 CONTENTS, ALLOC, LOAD, DATA 4 license 00000004 0000000000000000 0000000000000000 00000164 2**0 CONTENTS, ALLOC, LOAD, DATA 5 version 00000004 0000000000000000 0000000000000000 00000168 2**2 CONTENTS, ALLOC, LOAD, DATA 6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
main program是 tracex4_user.c
,大体上,这个程序的作用就是监听kmem_cache_alloc_node上的事件,当事件发生时,对应的eBPF code会被执行,且把ip信息保存到Map,main program从map中读取并打印出来
$ sudo ./tracex4 obj 0xffff8d6430f60a00 is 2sec old was allocated at ip ffffffff9891ad90 obj 0xffff8d6062ca5e00 is 23sec old was allocated at ip ffffffff98090e8f obj 0xffff8d5f80161780 is 6sec old was allocated at ip ffffffff98090e8f
这user-sapce program 和 eBPF program是怎么连接在一起的?在初始化的时候,tracex4_user.c
使用load_bpf_file
加载 tracex4_kern.o
int mainint ac, char **argv) { struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY}; char filename[256]; int i; snprintffilename, sizeoffilename), "%s_kern.o", argv[0]); if setrlimitRLIMIT_MEMLOCK, &r)) { perror"setrlimitRLIMIT_MEMLOCK, RLIM_INFINITY)"); return 1; } if load_bpf_filefilename)) { printf"%s", bpf_log_buf); return 1; } for i = 0; ; i++) { print_old_objectsmap_fd[1]); sleep1); } return 0; }
执行 load_bpf_file时,eBPF文件中定义的探测kprobe)将添加到/sys/kernel/debug/tracing/kprobe_events中。我们现在正在监听这些事件,当它们发生时,我们的程序可以做一些事情。
$ sudo cat /sys/kernel/debug/tracing/kprobe_events p:kprobes/kmem_cache_free kmem_cache_free r:kprobes/kmem_cache_alloc_node kmem_cache_alloc_node
XDP
XDP的设计源于Cloudflare在Netdev 1.1上提出的DDoS攻击缓解解决方案
因为Cloudflare希望保持使用iptables(以及内核网络堆栈的其余部分)的便利性,所以他们无法使用完全控制硬件的解决方案(即前面的kernel bypass),例如DPDK
Cloudflare的解决方案使用Netmap工具包实现其部分内核旁路partial kernel bypass)(来源:Single Rx queue kernel bypass with Netmap)。这个想法可以通过在Linux内核网络堆栈中添加一个检查点checkpoint),最好是在NIC中接收到数据包之后。该checkpoint应将数据包传递给eBPF程序,该程序将决定如何处理该数据包:丢弃该数据包drop)或让其继续通过正常路径pass). 就像这幅图一样:
Example: An IPv6 packet filter
介绍XDP的典型例子是DDos过滤器,它的作用是:果数据包来自可疑来源,就丢弃它们。在我的例子中,我将使用更简单的功能:一个过滤除IPv6之外的所有流量的功能。
为了简单处理,我们不需要管理可疑地址列表。我们只简单地检查数据包的ethertype值,并让它继续通过网络堆栈network stack),或者根据是否是IPv6数据包丢弃它。
SEC"prog") int xdp_ipv6_filter_programstruct xdp_md *ctx) { void *data_end = void *)long)ctx->data_end; void *data = void *)long)ctx->data; struct ethhdr *eth = data; u16 eth_type = 0; if !parse_etheth, data_end, eth_type))) { bpf_debug"Debug: Cannot parse L2\n"); return XDP_PASS; } bpf_debug"Debug: eth_type:0x%x\n", ntohseth_type)); if eth_type == ntohs0x86dd)) { return XDP_PASS; } else { return XDP_DROP; } }
函数xdp_ipv6_filter_程序是我们的主程序。我们在二进制文件中定义了一个称为prog的新部分。这是我们的程序和XDP之间的挂钩。每当XDP收到一个数据包,我们的代码就会被执行。
CTX表示一个上下文,一个包含访问数据包所需的所有数据的结构。我们的程序调用parse_eth来获取ethertype。然后检查其值是否为0x86dd(IPv6以太网类型),如果是,数据包将通过。否则,数据包将被丢弃。此外,出于调试目的,所有ethertype值都会打印出来。
bpf_debug实际上是一个宏,定义如下:
#define bpf_debugfmt, ...) \ { \ char ____fmt[] = fmt; \ bpf_trace_printk____fmt, sizeof____fmt), \ ##__VA_ARGS__); \ })
内部其实也是调用了bpf_trace_printk,这个函数会打印在/sys/kernel/debug/tracing/trace_pipe 中的信息
函数parse_eth获取数据包的开头和结尾,并解析其内容:
static __always_inline bool parse_ethstruct ethhdr *eth, void *data_end, u16 *eth_type) { u64 offset; offset = sizeof*eth); if void *)eth + offset > data_end) return false; *eth_type = eth->h_proto; return true; }
在内核中运行外部代码涉及某些风险。例如,无限循环可能会冻结内核,或者程序可能会访问不受限制的内存区域。为避免这些潜在危险,在加载eBPF代码时运行验证器。验证器遍历所有可能的代码路径,检查我们的程序没有访问超出范围的内存,也没有越界跳转;验证器还确保程序在有限时间内终止。
我们的eBPF程序符合这些要求。现在我们只需要编译它(完整的源代码可以在:xdp_ipv6_filter上找到)。
$ make
这会生成 xdp_ipv6_filter.o
,一个eBPF object file
现在我们需要把这个object file加载到network interface,这有两种方式可以做到这一点:
写一个user-space program加载目标文件到network interface
使用iproute
来加载目标文件到interface
在这个例子中,我们将使用后一种方法
目前,支持XDP的网络接口数量有限(ixgbe、i40e、mlx5、veth、tap、tun、virtio_net和其他),尽管数量在不断增加。其中一些网络接口在驱动程序级别支持XDP(言下之意有些还不能在驱动级别)。这意味着,XDP钩子是在网络层的最低点实现的,就在NIC在Rx ring中接收到数据包的时候。在其他情况下,XDP钩子在网络堆栈中的较高点实现。前者提供了更好的性能结果,尽管后者使XDP可用于任何网络接口。
幸运的是,XDP支持veth interfaces,我将创建一个veth对,并将eBPF程序连接到它的一端。记住veth总是成对的,它就像一根虚拟电缆连接两个端口,任何在一端传送的东西都会到达另一端,反之亦然。
$ sudo ip link add dev veth0 type veth peer name veth1 $ sudo ip link set up dev veth0 $ sudo ip link set up dev veth1
现在我们将eBPF program attach到veth1上:
$ sudo ip link set dev veth1 xdp object xdp_ipv6_filter.o
您可能已经注意到,我将eBPF程序的部分称为“prog”。这是iproute2
希望查找的节的名称,使用其他名称命名该节将导致错误。
如果程序成功加载,我将在veth1接口中看到一个xdp标志:
$ sudo ip link sh veth1 8: veth1@veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp qdisc noqueue state UP mode DEFAULT group default qlen 1000 link/ether 32:05:fc:9a:d8:75 brd ff:ff:ff:ff:ff:ff prog/xdp id 32 tag bdb81fb6a5cf3154 jited
为了验证我的程序是否按预期工作,我将把IPv4和IPv6数据包的混合推送到veth0(IPv4-and-IPv6-data.pcap)。我的示例总共有20个数据包(10个IPv4和10个IPv6)。但在这样做之前,我将在veth1上启动一个tcpdump程序,它只准备捕获10个IPv6数据包。
$ sudo tcpdump "ip6" -i veth1 -w captured.pcap -c 10 tcpdump: listening on veth1, link-type EN10MB Ethernet), capture size 262144 bytes
送packets到veth0:
$ sudo tcpreplay -i veth0 ipv4-and-ipv6-data.pcap
过滤后的数据包到达另一端。由于收到了所有预期的数据包,tcpdump程序终止。
10 packets captured 10 packets received by filter 0 packets dropped by kernel
我们也可以打印出/sys/kernel/debug/tracing/trace_pipe,来检查ethertype value.
$ sudo cat /sys/kernel/debug/tracing/trace_pipe tcpreplay-4496 [003] ..s1 15472.046835: 0: Debug: eth_type:0x86dd tcpreplay-4496 [003] ..s1 15472.046847: 0: Debug: eth_type:0x86dd tcpreplay-4496 [003] ..s1 15472.046855: 0: Debug: eth_type:0x86dd tcpreplay-4496 [003] ..s1 15472.046862: 0: Debug: eth_type:0x86dd tcpreplay-4496 [003] ..s1 15472.046869: 0: Debug: eth_type:0x86dd tcpreplay-4496 [003] ..s1 15472.046878: 0: Debug: eth_type:0x800 tcpreplay-4496 [003] ..s1 15472.046885: 0: Debug: eth_type:0x800 tcpreplay-4496 [003] ..s1 15472.046892: 0: Debug: eth_type:0x800 tcpreplay-4496 [003] ..s1 15472.046903: 0: Debug: eth_type:0x800 tcpreplay-4496 [003] ..s1 15472.046911: 0: Debug: eth_type:0x800 ...
非常建议大家亲自动手做这个实验的!!
最后mark一些没详细看完的资料:
LINUX.CONG.AU_BPF: Tracing and More
Taiwan Linux Kernel Hackers_主題分享:Introduction to eBPF and XDP
个性签名:时间会解决一切