
本文地址:https://www.ebpf.top/post/ebpf_rust_aya
1. 前言
Linux 内核 6.1 版本中有一个非常引人注意的变化:引入了对 Rust 编程语言的支持。Rust 是一种系统编程语言,Rust 通过提供非常强大的编译时保证和对内存生命周期的明确控制。在内核开发中引入 Rust 语言,将会为内核开发的早期带来更多的安全保障。eBPF 是在内核中基于事件运行用户自定义程序的技术,其验证器机制可以保障运行在内核中 eBPF 程序的安全性。
Rust 与 eBPF 有着一个共同的目标:保证内核安全,只是两者侧重的维度有所不同。
尽管使用 Rust 编写 eBPF 程序多数情况下都需要通过不安全的方式在内核进行内存读写,但是基于 Rust 和 Aya ,的确能够给我们带来一个快速和高效的开发体验,这包括自动生成整个程序框架(eBPF 程序及对应的用户空间代码)、参数检查确认、错误处理、统一的相关构建和管理方式等等 。
Aya 是一个以可操作性和开发者体验为重点的 eBPF 库,完全是在 Rust 基础上建立的,只使用 libc 包来执行系统调用。Aya 官方仓库地址为 https://github.com/aya-rs/aya/,当前版本为 v0.1.11,项目还处于偏早期。基于 Aya 库开发 eBPF 程序可以给我们带来以下的便利:
- 基于 Rust 的 Cargo 工具来管理、构建和测试项目;
- 支持 CO-RE 直接生成与 Rust 与内核文件的绑定;
- 用户工具代码(Rust)与运行在的内核中的 eBPF 代码轻松共享代码;
- 对于 LLVM、libbpf、bcc 等完全没有任何依赖;
本文仅是基于 Aya 编写 eBPF 程序及用户空间程序的生成和测试的过程记录,不涉及到对于生成 Rust 代码的详细解读。
2. Rust 开发环境搭建
2.1 创建 VM 虚拟机
为了使用 Rust 进行 eBPF 程序编写,那么我们首先需要在本地搭建一个 Rust 开发环境。这里我仍然采用 multipass 工具快速搭建一个 Ubuntu 22.04 LTS 的环境。- $ multipass launch --name rust-aya -d 20G
复制代码 默认磁盘为 5G,比较容易造成磁盘空间满,因此这里将磁盘空间大小设置为 20G,你可以根据自己的情况调整。
对于已经创建的 mulipass 实例可以在创建后进行调整,则需要 multipass 版本大于 1.10,而且需要调整的实例处于停止状态,详细可参见调整实例配置,例如 multipass set local.rust-aya.cpus=4 或 multipass set local.rust-aya.memory=8G 分别用于调整实例的 CPU 和 MEM 大小。
2.2 安装 Rust 开发环境
通常情况下,Rust 开发环境推荐通过 rustup 工具管理,我们可以通过以下命令快速安装该工具:- $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
复制代码 一般情况下我们选择默认选项安装。整个安装过程会下载一个脚本完成 rustup 工具安装,同时也会安装最新的 Rust 稳定版本。如果安装顺利,我们可以在最后看到如下的信息:- ...
- stable-x86_64-unknown-linux-gnu installed - rustc 1.65.0 (897e37553 2022-11-02)
- Rust is installed now. Great!
- To get started you may need to restart your current shell.
- This would reload your PATH environment variable to include
- Cargo's bin directory ($HOME/.cargo/bin).
- To configure your current shell, run:
- source "$HOME/.cargo/env"
复制代码 在 rustup 工具安装完成后,我们可以使用其安装 Rust 稳定版(实际上默认已经安装)和 nightly ,其中 nightly 为开发者体验新功能的发布通道,Rust 2021 年开始支持编译 eBPF,当前使用 Aya 需要基于 Rust Nightly 版本。
Rust 有 3 个发布通道:
大部分 Rust 开发者主要采用稳定版通道,不过希望实验新功能的开发者可能会使用 nightly 或 beta 版。详情参见附录 G:Rust 是如何开发的与 “Nightly Rust”
- $ source "$HOME/.cargo/env"
- $ rustup install stable # rustup 命令已经默认安装
- info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
- stable-x86_64-unknown-linux-gnu unchanged - rustc 1.65.0 (897e37553 2022-11-02)
- info: checking for self-updates
- $ rustup toolchain install nightly --component rust-src
- ...
- info: installing component 'rustfmt'
- nightly-x86_64-unknown-linux-gnu installed - rustc 1.67.0-nightly (09508489e 2022-11-04)
- info: checking for self-updates
- $ rustup toolchain list
- stable-x86_64-unknown-linux-gnu (default)
- nightly-x86_64-unknown-linux-gnu
复制代码 安装 nightly 以后我们可以使用 rustup toolchain list 查看本地开发环境的开发工具链。
2.3 安装 bpf-linker 依赖 和 bpftool 工具
为了使用 Aya,我们还需要安装依赖包 bpf-linker,但其依赖与 LLVM/Clang 等工具,因此我们也需要提前安装:- $ sudo apt-get update
- $ sudo apt-get install llvm clang -y
- $ cargo install bpf-linker
复制代码 最后,为了生成内核数据结构的绑定,我们还必须安装 bpftool,可以从发行版中安装或从源代码中构建,这里我选用发行版安装方式(基于 Ubuntu 22.04),源码安装可参考 bpftool 仓库说明文档:- $ sudo apt install linux-tools-common linux-tools-5.15.0-52-generic linux-cloud-tools-5.15.0-52-generic -y
复制代码 支持我们完成了基于 Aya 开发的整个环境及依赖的安装。
3. Aya 向导创建 eBPF 程序
3.1 使用向导创建项目
Aya 提供了一套模版向导用于创建 eBPF 对应的程序类型,向导创建依赖于 cargo-generate,因此我们需要在运行程序向导前提前安装:- $ cargo install cargo-generate
复制代码我在安装 cargo-generate 过程中遇到了如下的错误,主要是由于依赖 openssl 库问题导致,如果你也遇到类似问题可参考 cargo-generate 安装指南 和 Rust OpenSSL 文档,如果一切顺利,则可忽略此处的提示。- ...
- warning: build failed, waiting for other jobs to finish...
- error: failed to compile `cargo-generate v0.16.0`, intermediate artifacts can be found at `/tmp/cargo-install8NrREg
- ...
- $ sudo apt install openssl pkg-config libssl-dev gcc m4 ca-certificates make perl -y
- # 重新安装即可
复制代码 在完成依赖后,我们就可以使用向导来创建 eBPF 项目,这里以 XDP 类型程序为例:- $ cargo generate https://github.com/aya-rs/aya-template
复制代码
这里我们输入项目名称 myapp,eBPF 程序类型选择 xdp,完成相关设定后,向导会自动帮我们创建一个名为 myapp 的 Rust 项目,项目包括了一个最简单的 XDP 类型的 eBPF 程序及相对应的用户空间程序。 myapp 目录的整体夹头如下所示:- ├── Cargo.lock
- ├── Cargo.toml
- ├── README.md
- ├── myapp # 用户空间程序
- │ ├── Cargo.toml
- │ └── src
- │ └── main.rs
- ├── myapp-common # eBPF 程序与用户空间程序复用的代码库
- │ ├── Cargo.toml
- │ └── src
- │ └── lib.rs
- ├── myapp-ebpf # eBPF 程序
- │ ├── Cargo.lock
- │ ├── Cargo.toml
- │ ├── rust-toolchain.toml
- │ └── src
- │ └── main.rs
- └── xtask # build 相关的代码
- ├── Cargo.toml
- └── src
- ├── build_ebpf.rs
- ├── main.rs
- └── run.rs
- 8 directories, 15 files
复制代码 生成的 eBPF 程序位于 myapp-ebpf/src 目录下,文件名为 main.rs,完整内容如下所示:- $ cat myapp-ebpf/src/main.rs
- #![no_std]
- #![no_main]
- use aya_bpf::{
- bindings::xdp_action,
- macros::xdp,
- programs::XdpContext,
- };
- use aya_log_ebpf::info;
- #[xdp(name="myapp")]
- pub fn myapp(ctx: XdpContext) -> u32 {
- match try_myapp(ctx) {
- Ok(ret) => ret,
- Err(_) => xdp_action::XDP_ABORTED,
- }
- }
- fn try_myapp(ctx: XdpContext) -> Result<u32, u32> {
- info!(&ctx, "received a packet"); // 每接受到一个数据包则打印一个日志
- Ok(xdp_action::XDP_PASS)
- }
- #[panic_handler]
- fn panic(_info: &core::panic::PanicInfo) -> ! {
- unsafe { core::hint::unreachable_unchecked() }
- }
复制代码 3.2 编译 eBPF 程序
首先,我们使用 cargo 工具编译 eBPF 对应的程序:- $ cd myapp
- $ cargo xtask build-ebpf
- ...
- Compiling myapp-ebpf v0.1.0 (/home/ubuntu/myapp/myapp-ebpf)
- Running `rustc --crate-name myapp --edition=2021 src/main.rs --error-format=json \
- --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type bin \
- --emit=dep-info,link -C opt-level=3 -C panic=abort -C lto -C codegen-units=1
- -C metadata=dd6140d48c387b43 -C extra-filename=-dd6140d48c387b43 \
- --out-dir \
- ...
- -Z unstable-options \
- Finished dev [optimized] target(s) in 11.76s
复制代码 编译完成后,对应的程序保存在 target 目录下:- ~/myapp$ ls -hl target/bpfel-unknown-none/debug/
- ...
- -rw-rw-r-- 2 ubuntu ubuntu 3.5K Nov 6 22:24 myapp
- ~/myapp$ file target/bpfel-unknown-none/debug/myapp
- target/bpfel-unknown-none/debug/myapp: ELF 64-bit LSB relocatable, eBPF, version 1 (SYSV), not stripped
- /myapp$ llvm-objdump -S target/bpfel-unknown-none/debug/myapp
- target/bpfel-unknown-none/debug/myapp: file format elf64-bpf
- Disassembly of section xdp/myapp:
- 0000000000000000 <myapp>:
- ...
- 242: bf 61 00 00 00 00 00 00 r1 = r6
- 243: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll
- 245: 18 03 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 r3 = 4294967295 ll
- 247: bf 04 00 00 00 00 00 00 r4 = r0
- 248: b7 05 00 00 aa 00 00 00 r5 = 170
- 249: 85 00 00 00 19 00 00 00 call 25
复制代码 至此,已经完成了 eBPF 程序的编译工作,接着我们需要继续编译用户空间代码。
3.3 运行用户空间程序
我们可以直接使用 cargo 命令来运行用户空间程序:- $ RUST_LOG=info cargo xtask run
- ...
- Finished dev [unoptimized + debuginfo] target(s) in 8.38s
- Error: failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE
- Caused by:
- unknown network interface eth0
复制代码RUST_LOG=info 为设置日志级别的环境变量,默认为 warn,但向导生成的代码打印的日志级别默认为 info,因此需要运行时制定,否则可能会出现程序运行查看不到日志的情况。
cargo xtask run 命令会直接编译用户空间代码并运行,但是运行过程中我们发现出现错误 unknown network interface eth0,这是因为默认生成的程序指定将 XDP 程序加载到 eth0 网卡,而我们的 VM 默认网卡不为 eth0 导致,这里我们明确制定网卡使用 lo 测试,再次运行结果如下:- $ RUST_LOG=info cargo xtask run -- --iface lo
- ...
- Finished dev [optimized] target(s) in 0.19s
- Finished dev [unoptimized + debuginfo] target(s) in 0.12s
- [2022-11-05T16:25:27Z INFO myapp] Waiting for Ctrl-C...
复制代码 这次可以发现用户空间程序已经正常运行,并且将对应的 eBPF 程序加载至内核中。
[code]$ sudo bpftool prog list42: xdp name myapp tag 2929f83b3be0f64b gpl loaded_at 2022-11-06T22:42:54+0800 uid 0 xlated 2016B jited 1151B memlock 4096B map_ids 14,13,15 $ ip link show1: lo: mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 prog/xdp id 42 # |