This article can be found at: https://www.ebpf.top/post/ebpf_rust_aya

image-20221112124939897

1. Introduction

A significant change in Linux Kernel version 6.1 is the introduction of support for the Rust programming language. Rust is a system programming language that offers robust compile-time guarantees and precise control over memory lifetimes. Integrating Rust language into kernel development will bring additional safety measures to the early stages of kernel development. eBPF is a technology in the kernel that enables running user-defined programs based on events, with a validator mechanism ensuring the security of eBPF programs running in the kernel.

Rust and eBPF share a common goal of ensuring kernel safety, albeit with different focuses.

Although writing eBPF programs in Rust often involves unsafe memory reads and writes in the kernel, leveraging Rust and Aya can indeed provide a fast and efficient development experience. This includes automatically generating the entire program framework (eBPF program and corresponding user-space code), parameter validation, error handling, unified build and management processes, and more.

Aya is an eBPF library focused on operability and developer experience, built entirely on Rust, using only the libc package for system calls. The official repository of Aya can be found at https://github.com/aya-rs/aya/, with the current version being v0.1.11, and the project is still in its early stages. Developing eBPF programs based on the Aya library offers the following conveniences:

  • Management, building, and testing of projects using Rust’s Cargo tool.
  • Support for directly generating CO-RE bindings with Rust and kernel files.
  • Easy code sharing between user tool code (Rust) and eBPF code running in the kernel.
  • No dependencies on LLVM, libbpf, bcc, and the like.

This article focuses on the process of writing eBPF programs and generating user-space programs using Aya, without delving into detailed explanations of generating Rust code.

2. Setting Up Rust Development Environment

2.1 Create a VM Virtual Machine

To write eBPF programs with Rust, we first need to set up a Rust development environment locally. Here, I will use the multipass tool to quickly create an environment with Ubuntu 22.04 LTS.

1
$ multipass launch --name rust-aya -d 20G

The default disk size is 5G, which can easily fill up, so here I’ve set the disk size to 20G. Adjust this according to your needs.

For existing multipass instances that need adjustment, the multipass version must be greater than 1.10, and the instance to be adjusted must be in a stopped state. Refer to Modifying an Instance for details on how to adjust instances, such as using multipass set local.rust-aya.cpus=4 or multipass set local.rust-aya.memory=8G to adjust CPU and memory sizes.

2.2 Install Rust Development Environment

Typically, the Rust development environment is managed using the rustup tool. We can quickly install this tool with the following command:

1
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

In most cases, we choose the default options for installation. The installation process involves downloading a script to install the rustup tool, as well as the latest stable version of Rust. If the installation is successful, you should see the following message at the end:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
	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"

Once rustup is installed, we can use it to install Rust stable (which is likely already installed by default) and nightly. Note that using Aya currently requires Rust Nightly.

Rust has 3 release channels:

  • Nightly
  • Beta
  • Stable

While most Rust developers primarily use the stable channel, developers wishing to experiment with new features may use nightly or beta versions. For more details, refer to Appendix G: How Rust is Developed and “Nightly Rust”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ source "$HOME/.cargo/env"
$ rustup install stable  # The rustup command is already installed by default
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

After installing nightly, we can use rustup toolchain list to check the development toolchain in the local environment.

2.3 Install Dependencies for bpf-linker and bpftool

To use Aya, we also need to install the dependencies such as bpf-linker, which relies on LLVM/Clang and other tools. Therefore, we need to install them beforehand:

1
2
3
$ sudo apt-get update
$ sudo apt-get install llvm clang -y
$ cargo install bpf-linker

Finally, to generate bindings for kernel data structures, we must install bpftool. You can install it from the distribution version or build it from the source code. Here, I choose the distribution version installation (based on Ubuntu 22.04). For source code installation, you can refer to the bpftool repository documentation:

1
$ sudo apt install linux-tools-common linux-tools-5.15.0-52-generic linux-cloud-tools-5.15.0-52-generic -y

This completes the installation of the entire environment and dependencies required for developing with Aya.

3. Aya Guide to Create eBPF Programs

3.1 Creating a Project Using the Guide

Aya provides a set of template guides for creating programs corresponding to eBPF types, and the guide creation depends on cargo-generate, so we need to install it before running the guide program:

1
$ cargo install cargo-generate

During the installation of cargo-generate, I encountered the following errors, mainly due to issues with the openssl library. If you encounter similar problems, you can refer to cargo-generate Installation Guide and Rust OpenSSL documentation. If everything goes smoothly, you can ignore this notice.

1
2
3
4
5
6
7
...
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
# Reinstall it

After installing the dependencies, we can use the guide to create an eBPF project. Here, we take the XDP type program as an example:

$ cargo generate https://github.com/aya-rs/aya-template

image-20221106221704771

Here, we enter the project name myapp, and choose the eBPF program type as xdp. After completing the related settings, the guide will automatically create a Rust project named myapp, which includes a simple XDP type eBPF program and its corresponding user-space program. The overall structure of the myapp directory is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
├── Cargo.lock
├── Cargo.toml
├── README.md
├── myapp  # User-space program
│   ├── Cargo.toml
│   └── src
│       └── main.rs
├── myapp-common  # Code library reused by eBPF programs and user-space programs
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── myapp-ebpf  # eBPF program
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── rust-toolchain.toml
│   └── src
│       └── main.rs
└── xtask  # Code related to building
    ├── Cargo.toml
    └── src
        ├── build_ebpf.rs
        ├── main.rs
        └── run.rs

8 directories, 15 files

The generated eBPF program is located in the myapp-ebpf/src directory, with the file name main.rs. The complete content of the file is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ 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;  ```rust
#[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"); // Log a message for each packet received
    Ok(xdp_action::XDP_PASS)
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}

3.2 Compile eBPF Program

Firstly, let’s compile the corresponding eBPF program using the cargo tool:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ 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 

After the compilation is completed, the program is saved in the target directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
~/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

Thus, the compilation of the eBPF program has been completed. Next, we need to proceed with compiling the user-space code.

3.3 Run User-space Program

We can directly run the user-space program using the cargo command:

1
2
3
4
5
6
7
$ 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

Setting the log level environment variable with RUST_LOG=info is necessary. By default, the log level is set to warn, but the code generated by the wizard prints logs at the info level. Specifying it during runtime is crucial to avoid missing log messages during program execution.

The cargo xtask run command compiles the user-space code and runs it directly. However, during execution, we encountered an error unknown network interface eth0. This happened because the default program specified to load the XDP program onto the eth0 network interface, which was not present in our VM setup. Here, we explicitly specified to use the lo interface for testing, and running it again yields the following result:

1
2
3
4
5
$ 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...

This time, the user-space program is running correctly, and the corresponding eBPF program is loaded into the kernel.

1
2
3
4
5
6
7
8
9
$ sudo bpftool prog list
42: 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 show
1: lo: <LOOPBACK,UP,LOWER_UP> 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 # <=== Loaded eBPF program id 42

We initiate a ping packet validation on the lo interface:

image-20221106224917574

We can observe that when running the ping -c 1 127.0.0.1 command in another terminal window, the corresponding log received a packet is printed in the log of the user-space myapp program simultaneously.

Thus, we have completed the validation of the simplest XDP program based on Aya. If you plan to advance further to print packet logs or to filter specific packets, you can refer to the corresponding chapters in Aya Book.

4. Conclusion

Through the usage of Aya throughout the process, we have found that developing eBPF programs with Aya indeed brings us many conveniences. The wizard helps set up the basic framework of the entire project and manages tasks like compilation and loading, making it particularly beginner-friendly. The default generated user-space code and eBPF code achieve a certain level of code reuse, especially making logging more convenient. At the same time, the project’s current documentation is not fully comprehensive yet, with documents on program types like Probe/Tracepoint/XDP still being improved. If you are interested, you are also welcome to contribute to the related development. More information can be found in Aya: Your Trusty eBPF Companion.

Moreover, it is anticipated that the libbpf-bootstrap project can implement Aya’s wizard-style creation of eBPF program codes soon, providing a fast onboarding experience for writing eBPF-related programs.

References

  1. A 30-minute Introduction to Rust

  2. https://aya-rs.dev/

  3. LWN: Aya: writing BPF in Rust 2021-6-15

  4. Aya: your tRusty eBPF companion 2022-6-22 Translation: Aya: Rust-Style eBPF Companion

  5. Adding BPF target support to the Rust compiler

  6. Rust and Tell - Berlin - Aya: Extending the Linux Kernel with eBPF and Rust by Michal Rostecki 2022-10-24

  7. Writing an eBPF/XDP load-balancer in Rust

  8. Wanting to use BPF with Rust (Part 1)