1. Aya 面向谁

Rust 因其安全特性和优秀的 C 互操作性(Interoperability),成为流行的系统编程语言。在 eBPF 的上下文中,安全特性不重要。因为程序通常需要读取内核内存,该操作被认为是不安全的。Rust 结合 Aya 所提供的是快速、高效的开发体验:


2. eBPF 程序限制

运行 eBPF 程序的 eBPF 虚拟机是受限制的运行时环境:

即便使用 C 编写的应用程序也被限制为语言功能的子集,在 Rust 中,Aya 也有类似的限制:

除此之外,我们编写的很多代码都是 unsafe,因为我们直接从内核内存读取。


3. 开发环境

3.1. 先决条件

在开始前,需要在系统上安装 Rust stable 和 nightly toolchain。通过 rustup 实现:

安装 Rust toolchain 后,必须安装 bpf-linker。该链接器依赖 LLVM,如果运行在 Linux x86_64 系统上,那么可以根据 Rust toolchain 附带的版本构建:

补充:

  1. 在 Ubuntu 上,需要安装依赖:apt install -y build-essential libssl-dev

如果运行在 macos 或其它架构的 Linux 上,那么需要先安装 LLVM 16,然后安装链接器:

为生成脚手架,需要安装 cargo-generate

补充:

  1. 在 Ubuntu 上,需要安装依赖:apt install pkgconf

最后,为生成内核数据结构的绑定,必须安装 bpftool(从发行版或从 source 构建)。

运行在 Ubuntu 20.04 LTS (Focal) 上?

如果运行在 Ubuntu 20.04 上,发行版安装的 bpftool 和默认内核存在 Bug。为避免遇到该问题,可以安装更新的、不包含该 Bug 的 bpftool 版本:

3.2. 开始新项目

使用 cargo-generate 开始新项目:

该命令将提示输入项目名称 - 在本例中使用 myapp。也将提示输入程序类型,并且可能根据所选类型,提示输入其它选项(比如,网络分类器的附加方向)。

可以直接从命令行设置模版选项,比如:

查看 the cargo-generate.toml file (in the aya-template repository) 获取可用选项的完整列表。


4. 简单的 XDP 程序

在本节中,将编写、构建以及运行简单的 eBPF/XDP 程序和用户空间应用程序。

4.1. Hello XDP!

源码

本章中的示例的完整代码在这里

4.1.1. 示例项目

XDP(eXpress Data Path)程序允许 eBPF 程序对其被附加到的的接口上接收到的数据包做出决策。为简单起见,本示例构建非常简单的防火墙,以允许或拒绝流量。

4.1.2. eBPF 组件
4.1.2.1. 允许全部流量

首先编写 eBPF 组件。下面的最小化 XDP 程序允许全部流量。该程序的逻辑在 xdp-hello-ebpf/src/main.rs,目前是这样的:

现在使用 cargo xtask build-ebpf 进行编译。

4.1.2.2. 验证程序

接下来查看编译的 eBPF 程序:

为简洁起见,已删减输出。可以看到 xdp/xdp_hello 部分。在 <LBB0_2> 中,r0 = 2 将寄存器 0 设置为 2XDP_PASS 动作的值)。exit 结束程序。

4.1.3. 用户空间组件

编译 eBPF 程序后,需要用户空间程序加载它,并且将其附加到追踪点。该逻辑在 xdp-hello/src/main.rs

4.1.3.1. 开始

接下来查看用户空间应用程序的细节:

尝试一下!

接口名称

该命令假定接口默认为 eth0。如果希望附加到其它接口,使用 RUST_LOG=info cargo xtask run -- --iface wlp2s0,其中 wlp2s0 是接口。

每次在接口上收到数据包时,都打印一条日志。

加载程序出错?

如果加载程序出错,尝试将 XdpFlags::default() 更改为 XdpFlags::SKB_MODE

4.1.3.2. eBPF 程序的生命周期

程序一直运行,直到按下 CTRL + C。退出时,Aya 负责卸载程序。

xdp_hello 运行时,如果发出 sudo bpftool prog list 命令,可以验证 xdp_hello 已被加载:

xdp_hello 退出时,运行该命令将显示该程序不再运行。

4.2. 解析数据包

前一章的 XDP 程序在按下 CTRL-C 之前,一直运行,并且允许全部流量。每次收到数据包时,该 eBPF 程序记录字符串“received a packet”。本章将展示如何解析数据包。

虽然可以将数据解析到 L7(应用层),但在示例中限制为 L3(网络层),并且为简化起见,仅限于 IPv4。

源代码

本章节中的示例的完整代码在这里

4.2.1. 使用 network types

我们将记录流入数据包的源 IP 地址,因此需要:

可以阅读这些协议的规范,手动解析,但是将使用 network-types 库,该库为许多常见的因特网协议提供类型定义。

通过在 xdp-log-ebpf/Cargo.toml 中添加 network-types 依赖的方式,将其添加到 eBPF 库:

xdp-log-ebpf/Cargo.toml:

4.2.2. 从上下文中获取数据包数据

XdpContext 包含将要使用的两个字段:datadata_end,它们分别是指向数据包开头和结尾的指针。

为访问数据包中的数据,并且确保以使 eBPF 验证器满意的方式进行访问,引入名为 ptr_at 的辅助函数。该函数保证在访问数据包数据之前,插入验证器所需的边界检查。

代码如下:

xdp-log-ebpf/src/main.rs:

不要忘记重新构建 eBPF 程序。

4.2.3. 用户空间组件

用户空间代码如下:

xdp-log/src/main.rs:

4.2.4. 运行程序

与之前一样,可以通过将接口名作为参数的方式,覆盖接口,比如 RUST_LOG=info cargo xtask run -- --iface wlp2s0

4.3. 丢弃数据包

前面章节中的 XDP 程序只记录流量。在本节中,将扩展它,支持丢弃流量。

源代码

本章节中的示例的完整代码在这里

4.3.1. 设计

为丢弃数据包,需要将要丢弃的 IP 地址的列表。为高效地查找它们,将使用 HashMap 持有它们。

我们将:

4.3.2. 在 eBPF 中丢弃数据包

在 eBPF 代码中,创建名为 BLOCKLIST 的新 Map。为做出策略决策,需要在 HashMap 中查找源 IP 地址。如果存在,那么丢弃数据包,否则允许。函数 block_ip 实现该逻辑。

代码如下:

xdp-drop-ebpf/src/main.rs:

4.3.3. 从用户空间填充 Map

为添加要阻止的地址,需要先获取 BLOCKLIST Map 的引用。拥有引用后,只需要调用 blocklist.insert()。使用 IPv4Addr 类型表示 IP 地址,因为它易读,并且容易转换为 u32。在本例中,将阻止来源于 1.1.1.1 的所有流量。

字节序

IP 地址在数据包中始终以网络字节序(大端序)进行编码。在本例中,在检查阻止列表之前,使用 u32::from_be 将其转换成主机字节序。因此,在用户空间中以主机字节序格式写 IP 地址是正确的。

另外一种方式也可以生效:当从用户空间插入时,将 IP 转换为网络字节序,当从 eBPF 程序进行索引时,无需要转换。

用户空间代码如下:

xdp-drop/src/main.rs:

4.3.4. 运行程序

附录 1 - Linux XDP

Linux XDP(eXpress Data Path)是一种高性能的数据包处理技术,它在 Linux 内核中引入新数据包处理路径。XDP 允许用户空间程序在数据包到达网络设备驱动程序之前拦截和处理数据包,从而实现低延迟、高吞吐量的数据包处理。

XDP 的主要特点和优势包括:

  1. 低延迟和高吞吐量:XDP 在内核中使用高效的数据结构和处理方式,使数据包处理能够以非常低的延迟和高吞吐量进行,适用于对网络性能要求较高的场景。

  2. 内核级别处理:XDP 提供在内核空间中进行数据包处理的机制,避免从用户空间到内核空间的频繁上下文切换,提高处理效率。

  3. 灵活的数据包处理:使用 XDP,可以编写自定义的数据包处理程序,对数据包进行过滤、修改、丢弃等操作,实现各种网络功能,比如防火墙、负载均衡、数据包采集等。

  4. 与网络设备驱动程序紧密集成:XDP 与网络设备驱动程序直接集成,可以在设备驱动程序中执行数据包处理逻辑,避免额外的数据包拷贝和处理开销。

  5. 支持多种程序语言和工具:XDP 支持使用多种编程语言编写数据包处理程序,如 C、C++、Rust,同时提供辅助工具和库,如 libbpf 和 bpftool,简化开发和调试过程。

XDP 技术的应用场景包括数据中心网络加速、DDoS 防护、高性能网络函数、智能网卡等。通过利用 XDP 技术,可以在 Linux 网络栈中实现更高效、更灵活的数据包处理,满足高性能网络应用的需求。