Skip to content

Commit d27510f

Browse files
committed
Add a chapter about logging custom structs with PerfEventArray
1 parent 334c23b commit d27510f

22 files changed

Lines changed: 667 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Getting data to user-space
2+
3+
In previous chapters we were logging packets with aya-log. However, what if we
4+
need to send additional data about the packet or any other type of information
5+
for the user-space program to utilize? In this chapter, we will explore how to
6+
leverage perf buffers and define data structures in the *-common* crate to
7+
transfer data from eBPF program to user-space applications. By doing so,
8+
user-space programs can access and utilize the transferred data effectively.
9+
10+
!!! example "Source code"
11+
12+
Full code for the example in this chapter is available
13+
[here](https://github.com/aya-rs/book/tree/main/examples/xdp-perfbuf-custom-data)
14+
15+
## Sharing data
16+
17+
In this chapter, we will be sending data from the kernel space to the user space
18+
by writing it into a struct and outputting it to user space. We can achieve this
19+
by using an eBPF map. There are different types of maps available, but in this
20+
case, we will use `PerfEventArray`.
21+
22+
`PerfEventArray` is a collection of per-CPU circular buffers that enable the
23+
kernel to emit events (defined as custom structs) to user space. Each CPU has
24+
its own buffer, and the eBPF program emits an event to the buffer of the CPU
25+
it's currently running on. The events are unordered, meaning that they arrive
26+
in the user-space in a different order than they were created and sent from the
27+
eBPF program.
28+
29+
To gather events from all CPUs, we are going to spawn a task for each CPU to
30+
poll for the events and then iterate over them.
31+
32+
The data structure we'll be using needs to hold an IPv4 address and a port.
33+
34+
```rust linenums="1" title="xdp-perfbuf-custom-data-common/src/lib.rs"
35+
--8<-- "examples/xdp-perfbuf-custom-data/xdp-perfbuf-custom-data-common/src/lib.rs"
36+
```
37+
38+
1. We implement the `aya::Pod` trait for our struct since it is Plain Old Data
39+
as can be safely converted to a byte slice and back.
40+
41+
1. Events emitted with `PerfEventArray` are copied from kernel memory to user
42+
memory, therefore they must implement the `aya::Pod` trait (where `Pod` stands
43+
for "plain old data") which expresses that it's safe to convert them into a
44+
sequence of bytes.
45+
46+
!!! tip "Alignment, padding and verifier errors"
47+
48+
At program load time, the eBPF verifier checks that all the memory used is
49+
properly initialized. This can be a problem if - to ensure alignment - the
50+
compiler inserts padding bytes between fields in your types.
51+
52+
**Example:**
53+
54+
```rust
55+
#[repr(C)]
56+
struct SourceInfo {
57+
source_port: u16,
58+
source_ip: u32,
59+
}
60+
61+
let source_port = ...;
62+
let source_ip = ...;
63+
let si = SourceInfo { source_port, source_ip };
64+
```
65+
66+
In the example above, the compiler will insert two extra bytes between the
67+
struct fields `source_port` and `source_ip` to make sure that `source_ip` is
68+
correctly aligned to a 4-byte address (assuming `mem::align_of::<u32>() ==
69+
4`). Since padding bytes are typically not initialized by the compiler,
70+
this will result in the infamous `invalid indirect read from stack` verifier
71+
error.
72+
73+
To avoid the error, you can either manually ensure that all the fields in
74+
your types are correctly aligned (e.g. by explicitly adding padding or by
75+
making field types larger to enforce alignment) or use `#[repr(packed)]`.
76+
Since the latter comes with its own foot-guns and can perform less
77+
efficiently, explicitly adding padding or tweaking alignment is recommended.
78+
79+
**Solution ensuring alignment using larger types:**
80+
81+
```rust
82+
#[repr(C)]
83+
pub struct SourceInfo {
84+
pub source_port: u32,
85+
pub source_ip: u32,
86+
}
87+
88+
let source_port = ...;
89+
let source_ip = ...;
90+
let si = SourceInfo { source_port, source_ip };
91+
```
92+
93+
**Solution with explicit padding:**
94+
95+
```rust
96+
#[repr(C)]
97+
pub struct SourceInfo {
98+
pub source_port: u16,
99+
_padding: u16,
100+
pub source_ip: u32,
101+
}
102+
103+
let source_port = ...;
104+
let source_ip = ...;
105+
let si = SourceInfo { source_port, padding: 0, source_ip };
106+
```
107+
108+
## Extracting packet data from the context and into the map
109+
110+
The eBPF program code in this section is similar to the one in the previous
111+
chapters. It extracts the source IP address and port information from packet
112+
headers.
113+
114+
The difference is that after obtaining the data from the headers, we create a
115+
`PacketLog` struct and output it to our `PerfEventArray` instead of logging data
116+
directly.
117+
118+
The resulting code looks like this:
119+
120+
```rust linenums="1" title="xdp-perfbuf-custom-data-ebpf/src/main.rs"
121+
--8<-- "examples/xdp-perfbuf-custom-data/xdp-perfbuf-custom-data-ebpf/src/main.rs"
122+
```
123+
124+
1. Create our map.
125+
2. Output the event to the map.
126+
127+
## Reading data
128+
129+
To read from the perf event array in user space, we need to choose one of the
130+
following types:
131+
132+
* `AsyncPerfEventArray` which is designed for use with
133+
[async Rust](https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html).
134+
* `PerfEventArray`, intended for synchronous Rust.
135+
136+
By default, [our project template](https://github.com/aya-rs/aya-template) is
137+
written in async Rust and uses the [Tokio runtime](https://tokio.rs/). Therefore,
138+
we will use `AsyncPerfEventArray` in this chapter.
139+
140+
To read from the `AsyncPerfEventArray`, we must call
141+
`AsyncPerfEventArray::open()` for each online CPU and poll the file descriptor
142+
for events.
143+
144+
Additionally, we need to add a dependency on `bytes` to `xdp-log/Cargo.toml`.
145+
This library simplifies handling the chunks of bytes yielded by the
146+
`AsyncPerfEventArray`.
147+
148+
Here's the code:
149+
150+
```rust linenums="1" title="xdp-perfbuf-custom-data/src/main.rs"
151+
--8<-- "examples/xdp-perfbuf-custom-data/xdp-perfbuf-custom-data/src/main.rs"
152+
```
153+
154+
1. Define our map.
155+
2. Call `open()` for each online CPU.
156+
3. Spawn a `tokio::task`.
157+
4. Create buffers.
158+
5. Read events in to buffers.
159+
6. Use `read_unaligned` to read the event data into a `PacketLog`.
160+
7. Log the packet data.
161+
162+
## Running the program
163+
164+
As before, you can overwrite the interface by by providing the interface name as
165+
a parameter, for example, `RUST_LOG=info cargo xtask run -- --iface wlp2s0`.
166+
167+
```console
168+
$ RUST_LOG=info cargo xtask run
169+
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 60.235.240.157, SRC_PORT: 443
170+
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 98.21.76.76, SRC_PORT: 443
171+
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 95.194.217.172, SRC_PORT: 443
172+
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 95.194.217.172, SRC_PORT: 443
173+
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 95.10.251.142, SRC_PORT: 443
174+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[alias]
2+
xtask = "run --package xtask --"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### https://raw.github.com/github/gitignore/master/Rust.gitignore
2+
3+
# Generated by Cargo
4+
# will have compiled files and executables
5+
debug/
6+
target/
7+
8+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
9+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
10+
Cargo.lock
11+
12+
# These are backup files generated by rustfmt
13+
**/*.rs.bk
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"rust-analyzer.linkedProjects": ["Cargo.toml", "xdp-perfbuf-custom-data-ebpf/Cargo.toml"]
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"rust-analyzer.linkedProjects": ["Cargo.toml", "xdp-perfbuf-custom-data-ebpf/Cargo.toml"]
3+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[workspace]
2+
members = ["xdp-perfbuf-custom-data", "xdp-perfbuf-custom-data-common", "xtask"]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# xdp-perfbuf-custom-data
2+
3+
## Prerequisites
4+
5+
1. Install a rust stable toolchain: `rustup install stable`
6+
1. Install a rust nightly toolchain: `rustup install nightly`
7+
1. Install bpf-linker: `cargo install bpf-linker`
8+
9+
## Build eBPF
10+
11+
```bash
12+
cargo xtask build-ebpf
13+
```
14+
15+
To perform a release build you can use the `--release` flag.
16+
You may also change the target architecture with the `--target` flag
17+
18+
## Build Userspace
19+
20+
```bash
21+
cargo build
22+
```
23+
24+
## Run
25+
26+
```bash
27+
RUST_LOG=info cargo xtask run
28+
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "xdp-perfbuf-custom-data-common"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[features]
7+
default = []
8+
user = [ "aya" ]
9+
10+
[dependencies]
11+
aya = { version = ">=0.11", optional=true }
12+
13+
[lib]
14+
path = "src/lib.rs"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#![no_std]
2+
3+
#[repr(C)]
4+
#[derive(Clone, Copy)]
5+
pub struct PacketLog {
6+
pub ipv4_address: u32,
7+
pub port: u32,
8+
}
9+
10+
#[cfg(feature = "user")]
11+
unsafe impl aya::Pod for PacketLog {} // (1)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build]
2+
target-dir = "../target"
3+
target = "bpfel-unknown-none"
4+
5+
[unstable]
6+
build-std = ["core"]

0 commit comments

Comments
 (0)