这是一个实战系列文章,它是eBPF学习计划里面的应用场景之网络部分,终极目标是源码级别学习云原生网络方案Cilium(声明:下文提到的BPF字样是泛指,包括cBPF和eBPF)。

Cilium方案中大量使用了XDP、TC等网络相关的BPF hook,以实现高性能的网络RX和TX。

今天我们先来讲讲XDP应用理论,再来学习编写第一个XDP BPF程序。

TL;DR

文章涉及的实验环境和代码可以到这个git repo获取。

https://github.com/nevermosby/linux-bpf-learning

网络钩子(hook)

在计算机网络中,Hook钩子在操作系统中用于在调用前或执行过程中拦截网络数据包。Linux内核中暴露了多个钩子,BPF程序可以连接到这些钩子上,实现数据收集和自定义事件处理。虽然Linux内核中的钩子点很多,但我们将重点关注网络子系统中存在的两个钩子:XDP和TC。它们结合在一起,可以用来处理RX和TX上两个链路上靠近NIC的数据包,从而实现了许多网络应用的开发。今天我们先来讲讲XDP。

XDP使用介绍

XDP全称为eXpress Data Path,是Linux内核网络栈的最底层。它只存在于RX路径上,允许在网络设备驱动内部网络堆栈中数据来源最早的地方进行数据包处理,在特定模式下可以在操作系统分配内存(skb)之前就已经完成处理。

XDP暴露了一个可以加载BPF程序的网络钩子。在这个钩子中,程序能够对传入的数据包进行任意修改和快速决策,避免了内核内部处理带来的额外开销。这使得XDP在性能速度方面成为最佳钩子,例如缓解DDoS攻击等,相关背景知识可以看这篇文章

XDP输入参数

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  */
};

程序执行时,datadata_end字段分别是数据包开始和结束的指针,它们是用来获取和解析传来的数据,第三个值是data_meta指针,初始阶段它是一个空闲的内存地址,供XDP程序与其他层交换数据包元数据时使用。最后两个字段分别是接收数据包的接口和对应的RX队列的索引。当访问这两个值时,BPF代码会在内核内部重写,以访问实际持有这些值的内核结构struct xdp_rxq_info

XDP输出参数

在处理完一个数据包后,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之后网络Data Path

在没有引入XDP之前,原来是的网络数据包传输路径是这样的:

启用XDP后,网络包传输路径是这样的:

可以看到多了3个红色方框圈起来的新链路,我们来一一介绍:

  • offload模式,XDP程序直接hook到可编程网卡硬件设备上,与其他两种模式相比,它的处理性能最强;由于处于数据链路的最前端,过滤效率也是最高的。如果需要使用这种模式,需要在加载程序时明确声明。目前支持这种模式的网卡设备不多,有一家叫netronome
  • native模式,XDP程序hook到网络设备的驱动上,它是XDP最原始的模式,因为还是先于操作系统进行数据处理,它的执行性能还是很高的,当然你的网络驱动需要支持,目前已知的有i40e, nfp, mlx系列ixgbe系列
  • generic模式,这是操作系统内核提供的通用 XDP兼容模式,它可以在没有硬件或驱动程序支持的主机上执行XDP程序。在这种模式下,XDP的执行是由操作系统本身来完成的,以模拟native模式执行。好处是,只要内核够高,人人都能玩XDP;缺点是由于是仿真执行,需要分配额外的套接字缓冲区(SKB),导致处理性能下降,跟native模式在10倍左右的差距。

当前主流内核版本的Linux系统在加载XDP BPF程序时,会自动在native和generic这两种模式选择,完成加载后,可以使用ip命令行工具来查看选择的模式。

XDP 开发环境搭建

说完了理论,我们就要开始动手了。先来搭建实验环境。

  • 操作系统
    建议使用Ubuntu 18.04 和CentOS 8系列最新稳定版,内核版本不要低于4.15,有条件的话,请选择最新稳定版。本次演示环境是Ubuntu 18.04 with Kernel 4.15。
  • 安装依赖的软件包

设计你的第一个XDP程序

通过上文的介绍,可以了解到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类型,最后是代码许可证声明。做个简单的解释:

  1. 第一部分是第一行的头文件linux/bpf.h,它包含了BPF程序使用到的所有结构和常量的定义(除了一些特定的子系统,如TC,它需要额外的头文件)。理论上来说,所有的eBPF程序第一行都是这个头文件。
  2. 第二部分是第二行的宏定义,它的作用是赋予了SEC(NAME)这一串字符具有意义,即可以被编译通过。我截取了Linux内核代码里的注释,可以看出这段宏定义是为了ELF格式添加Section信息的。ELF全称是Executable and Linkable Format,就是可执行文件的一种主流格式(详细介绍点这里),广泛用于Linux系统,我们的BPF程序一旦通过编译后,也会是这种格式。下面代码中的SEC("xdp")SEC("license")都是基于这个宏定义。
  3. 第三部分,也就是我们的代码主体,它是一个命名为xdp_drop_the_world函数,,返回值为int类型,接受一个参数,类型为xdp_md结构,上文已经介绍过,这个例子没有使用到这个参数。函数内的就是一行返回语句,使用XDP_DROP,也就是1,意思就是丢弃所有收到的数据包。
  4. 第四部分是最后一行的许可证声明。这行其实是给程序加载到内核时BPF验证器看的,因为有些eBPF函数只能被具有GPL兼容许可证的程序调用。因此,验证器会检查程序所使用的函数的许可证和程序的许可证是否兼容,如果不兼容,则拒绝该程序。
  5. 还有一点,大家是否注意到整个程序是没有main入口的,事实上,程序的执行入口可以由前面提到的ELF格式的对象文件中的Section来指定。入口也有默认值,它是ELF格式文件中.text这个标识的内容,程序编译时会将能看到的函数放到.text里面。

以上是对我们第一个XDP程序的简要介绍,很多相关的背景知识没有一一说明,大家可移步Linux内核源码一探究竟。

编译XDP程序

设计并编写完我们的程序代码后,接下来就是编译工作了。可以利用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), E (exclude),
  p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Linux BPF is not currently supported.

Symbol table '.symtab' contains 3 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    4 _license
     2: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    3 xdp_drop_the_world

No version information found in this file.

还可以通过llvm-objdump这个工具来分析下这个可执行文件的反汇编指令信息:

> llvm-objdump -S xdp-drop-world.o
xdp-world-drop.o:	file format ELF64-BPF

Disassembly of section xdp:
xdp_drop_the_world:
       0:	b7 00 00 00 01 00 00 00 	r0 = 1 # 1代表XDP_DROP,这句指令表示赋值1到代表返回值的寄存器r0
       1:	95 00 00 00 00 00 00 00 	exit

加载XDP程序

由于我们的实验环境配置的网卡和网卡驱动都不支持XDP hook,所以肯定是用了 xdpgeneric模式,我们不进行性能对比,因此不影响我们演示效果。加载XDP程序就要用到ip这个命令行工具了,它能帮助我们将程序加载到内核的XDP Hook上。上命令:

ip link set dev [device name] xdp obj xdp-drop-world.o sec [section name]

简单解释下:

  1. sec [section name]就是上文提到的通过Section来指定程序入口
  2. device name是本机某个网卡设备的名称,可以通过ip a查看本机所有的网卡设备。一般可以选取本机对外的IP所在的网卡设备。

因此完整的命令为:

# 查看主机的网卡设备列表,选取本机IP所在的网卡设备
> ip a
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:5c:7d:8f brd ff:ff:ff:ff:ff:ff
    inet 192.168.58.112/24 brd 192.168.58.255 scope global enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe5c:7d8f/64 scope link
       valid_lft forever preferred_lft forever
# 加载XDP程序到这个网卡设备上
> ip link set dev enp0s8 xdp obj xdp-drop-world.o sec xdp verbose
# 如下信息就是没有报错,说明已经通过BPF验证器并attach到内核XDP hook上了
Prog section 'xdp' loaded (5)!
 - Type:         6
 - Instructions: 2 (0 over limit)
 - License:      GPL

Verifier analysis:

0: (b7) r0 = 1
1: (95) exit
processed 2 insns, stack depth 0

验证第一个XDP程序的效果

测试场景很简单:

  1. 从外部ping作为实验环境的虚拟机IP(就是上文中的192.168.58.112),期望是无法ping通。
  2. 然后通过以下命令把XDP卸载掉,即detach from XDP hook,发现又能ping通:

那么就能说明我们的第一个XDP程序就工作了!期间,可以tcpdump来观察网络联通情况。下面是完整的Demo视频,结果符合我们的期望。

Ingress or Egress

上面的测试场景是验证了经过目标网络设备的Ingress流量被我们的XDP程序drop了,专业术语叫RX流向。那么Egress流量是否也会被drop掉呢?

答案是,不会。XDP hook不会作用到Egress流量,也就是TX流向。读者可以自行在已经attach XDP程序的实验环境中,ping一个外部地址,请保证这次请求会经过被attach XDP程序的网络设备。其结果就是请求没有收到任何影响。

那么谁能帮我们解决Egress流量控制的问题呢?

那就是TC。下篇文章我们来讨论它。

推荐阅读Linux内核代码的利器

这款是一个在线查看Linux内核代码的网站,支持快速查看某个变量和结构的引用和定义。给大家放一个BPF示例程序的链接:

https://elixir.bootlin.com/linux/latest/source/samples/bpf

特别通知

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1zc1v65a3om8c

4 对 “你的第一个XDP BPF 程序”的想法;

  1. [root@0xfe.com.cn xdp]# llvm-objdump -disassemble xdp-drop-world.o

    xdp-drop-world.o: file format ELF64-unknown

    error: no register info for target unknown-unknown-unknown

    [root@0xfe.com.cn xdp]# ip link set dev docker0 xdp obj xdp-drop-world.o sec .text
    Program section ‘.text’ not found in ELF file!
    Error fetching program/map!

    [root@0xfe.com.cn xdp]# 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: : 0xf7
    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), E (exclude),
    p (processor specific)

    There are no section groups in this file.

    There are no program headers in this file.

    There are no relocations in this file.

    The decoding of unwind sections for machine type : 0xf7 is not currently supported.

    Symbol table ‘.symtab’ contains 3 entries:
    Num: Value Size Type Bind Vis Ndx Name
    0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
    1: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 4 _license
    2: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 3 xdp_drop_the_world

    No version information found in this file.
    碰到问题了。

    1. 你好,从报错信息看,是.text这个section没找到。请确认下源文件中是否有SEC(“xdp”)这样的关键词,如果有,请使用SEC名称替换text这个字符,参考如下:
      ip link set dev docker0 xdp obj xdp-drop-world.o sec xdp

      “.text”这个参数比较特殊,源文件中没有声明任何SEC的话,可以使用它,把整个源码声明为一个section,常用于源码只包含一个方法的时候。

发表评论

电子邮件地址不会被公开。 必填项已用*标注