Skip to content

Commit

Permalink
Add a chapter about logging custom structs with PerfEventArray
Browse files Browse the repository at this point in the history
Signed-off-by: Michal Rostecki <[email protected]>
  • Loading branch information
vadorovsky committed Jan 25, 2023
1 parent bab873c commit addbe17
Show file tree
Hide file tree
Showing 22 changed files with 643 additions and 0 deletions.
147 changes: 147 additions & 0 deletions docs/book/start/getting-data-to-user-space.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Getting Data to User-Space

In previous chapters we were logging packets using aya-log. But what if we want
to send data about the packet (or in general - any kind of data) for the
user-space program to use?

This chapter is going to explain how to use perf buffers and define structures
in the *-common* crate to send data from eBPF program to user-space.

!!! example "Source Code"

Full code for the example in this chapter is available [here](https://github.com/aya-rs/book/tree/main/examples/xdp-perfbuf-custom-data)

## Sharing Data

To get data from kernel-space to user-space we use an eBPF map. There are
numerous types of maps to chose from, but in this example we'll be using a
`PerfEventArray`.

This time, instead of logging the information, we are going to write it into a
struct and output it to user-space. The data structure that we'll need to send
information to user-space will need to hold an IPv4 address and an action for
Permit/Deny, we'll encode both as a `u32`.

```rust linenums="1" title="xdp-perfbuf-custom-data-common/src/lib.rs"
--8<-- "examples/xdp-perfbuf-custom-data/xdp-perfbuf-custom-data-common/src/lib.rs"
```

1. We implement the `aya::Pod` trait for our struct since it is Plain Old Data
as can be safely converted to a byte-slice and back.

!!! tip "Alignment, padding and verifier errors"

At program load time, the eBPF verifier checks that all the memory used is
properly initialized. This can be a problem if - to ensure alignment - the
compiler inserts padding bytes between fields in your types.

**Example:**

```rust
#[repr(C)]
struct SourceInfo {
source_port: u16,
source_ip: u32,
}

let port = ...;
let ip = ...;
let si = SourceInfo { source_port: port, source_ip: ip };
```

In the example above, the compiler will insert two extra bytes between the
struct fields `source_port` and `source_ip` to make sure that `source_ip` is
correctly aligned to a 4 bytes address (assuming `mem::align_of::<u32>() ==
4`). Since padding bytes are typically not initialized by the compiler,
this will result in the infamous `invalid indirect read from stack` verifier
error.

To avoid the error, you can either manually ensure that all the fields in
your types are correctly aligned (eg by explicitly adding padding or by
making field types larger to enforce alignment) or use `#[repr(packed)]`.
Since the latter comes with its own footguns and can perform less
efficiently, explicitly adding padding or tweaking alignment is recommended.

**Solution ensuring alignment using larger types:**

```rust
#[repr(C)]
struct SourceInfo {
source_port: u32,
source_ip: u32,
}

let port = ...;
let ip = ...;
let si = SourceInfo { source_port: port, source_ip: ip };
```

**Solution with explicit padding:**

```rust
#[repr(C)]
struct SourceInfo {
source_port: u16,
padding: u16,
source_ip: u32,
}

let port = ...;
let ip = ...;
let si = SourceInfo { source_port: port, padding: 0, source_ip: ip };
```

## Getting Packet Data From The Context And Into the Map

Our eBPF program code is going to be similar to the one in the previous chapters.
It's going to get the information about source IP address and port from packet
headers.

The difference is that once we get the data from headers, instead of logging
them, we create a `PacketLog` struct and output it to our `PerfEventArray`

The resulting code looks like this:

```rust linenums="1" title="xdp-perfbuf-custom-data-ebpf/src/main.rs"
--8<-- "examples/xdp-perfbuf-custom-data/xdp-perfbuf-custom-data-ebpf/src/main.rs"
```

1. Create our map
2. Outputting the event to the `PerfEventArray`

Don't forget to rebuild your eBPF program!

## Reading Data

In order to read from the `AsyncPerfEventArray`, we have to call `AsyncPerfEventArray::open()` for each online CPU, then we have to poll the file descriptor for events.
While this is do-able using `PerfEventArray` and `mio` or `epoll`, the code is much less easy to follow. Instead, we'll use `tokio`, which was added to our template for us.

We'll need to add a dependency on `bytes = "1"` to `xdp-log/Cargo.toml` since this will make it easier
to deal with the chunks of bytes yielded by the `AsyncPerfEventArray`.

Here's the code:

```rust linenums="1" title="xdp-perfbuf-custom-data/src/main.rs"
--8<-- "examples/xdp-perfbuf-custom-data/xdp-perfbuf-custom-data/src/main.rs"
```

1. Define our map
2. Call `open()` for each online CPU
3. Spawn a `tokio::task`
4. Create buffers
5. Read events in to buffers
6. Use `read_unaligned` to read our data into a `PacketLog`.
7. Log the event to the console.

## Running the program

As before, the interface can be overwritten by providing the interface name as a parameter, for example, `RUST_LOG=info cargo xtask run -- --iface wlp2s0`.

```console
$ RUST_LOG=info cargo xtask run
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 60.235.240.157, SRC_PORT: 443
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 98.21.76.76, SRC_PORT: 443
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 95.194.217.172, SRC_PORT: 443
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 95.194.217.172, SRC_PORT: 443
[2023-01-25T08:57:41Z INFO xdp_perfbuf_custom_data] SRC IP: 95.10.251.142, SRC_PORT: 443
```
2 changes: 2 additions & 0 deletions examples/xdp-perfbuf-custom-data/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"
13 changes: 13 additions & 0 deletions examples/xdp-perfbuf-custom-data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### https://raw.github.com/github/gitignore/master/Rust.gitignore

# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk
3 changes: 3 additions & 0 deletions examples/xdp-perfbuf-custom-data/.vim/coc-settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rust-analyzer.linkedProjects": ["Cargo.toml", "xdp-perfbuf-custom-data-ebpf/Cargo.toml"]
}
3 changes: 3 additions & 0 deletions examples/xdp-perfbuf-custom-data/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rust-analyzer.linkedProjects": ["Cargo.toml", "xdp-perfbuf-custom-data-ebpf/Cargo.toml"]
}
2 changes: 2 additions & 0 deletions examples/xdp-perfbuf-custom-data/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[workspace]
members = ["xdp-perfbuf-custom-data", "xdp-perfbuf-custom-data-common", "xtask"]
28 changes: 28 additions & 0 deletions examples/xdp-perfbuf-custom-data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# xdp-perfbuf-custom-data

## Prerequisites

1. Install a rust stable toolchain: `rustup install stable`
1. Install a rust nightly toolchain: `rustup install nightly`
1. Install bpf-linker: `cargo install bpf-linker`

## Build eBPF

```bash
cargo xtask build-ebpf
```

To perform a release build you can use the `--release` flag.
You may also change the target architecture with the `--target` flag

## Build Userspace

```bash
cargo build
```

## Run

```bash
RUST_LOG=info cargo xtask run
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "xdp-perfbuf-custom-data-common"
version = "0.1.0"
edition = "2021"

[features]
default = []
user = [ "aya" ]

[dependencies]
aya = { version = ">=0.11", optional=true }

[lib]
path = "src/lib.rs"
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![no_std]

#[repr(C)]
#[derive(Clone, Copy)]
pub struct PacketLog {
pub ipv4_address: u32,
pub port: u32,
}

#[cfg(feature = "user")]
unsafe impl aya::Pod for PacketLog {} // (1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[build]
target-dir = "../target"
target = "bpfel-unknown-none"

[unstable]
build-std = ["core"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"rust-analyzer.cargo.target": "bpfel-unknown-none",
"rust-analyzer.checkOnSave.allTargets": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"rust-analyzer.cargo.target": "bpfel-unknown-none",
"rust-analyzer.checkOnSave.allTargets": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "xdp-perfbuf-custom-data-ebpf"
version = "0.1.0"
edition = "2021"

[dependencies]
aya-bpf = { git = "https://github.com/aya-rs/aya", branch = "main" }
aya-log-ebpf = { git = "https://github.com/aya-rs/aya", branch = "main" }
xdp-perfbuf-custom-data-common = { path = "../xdp-perfbuf-custom-data-common" }
network-types = "0.0.4"

[[bin]]
name = "xdp-perfbuf-custom-data"
path = "src/main.rs"

[profile.dev]
opt-level = 3
debug = false
debug-assertions = false
overflow-checks = false
lto = true
panic = "abort"
incremental = false
codegen-units = 1
rpath = false

[profile.release]
lto = true
panic = "abort"
codegen-units = 1

[workspace]
members = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[toolchain]
channel="nightly-2023-01-10"
# The source code of rustc, provided by the rust-src component, is needed for
# building eBPF programs.
components = [ "rustc", "rust-std", "cargo", "rust-docs", "rustfmt", "clippy", "rust-src" ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#![no_std]
#![no_main]

use core::mem;

use aya_bpf::{
bindings::xdp_action,
macros::{map, xdp},
maps::PerfEventArray,
programs::XdpContext,
};
use network_types::{
eth::{EthHdr, EtherType},
ip::{IpProto, Ipv4Hdr},
tcp::TcpHdr,
udp::UdpHdr,
};

use xdp_perfbuf_custom_data_common::PacketLog;

#[map(name = "EVENTS")] // (1)
static mut EVENTS: PerfEventArray<PacketLog> =
PerfEventArray::<PacketLog>::with_max_entries(1024, 0);

#[xdp(name = "xdp_perfbuf_custom_data")]
pub fn xdp_perfbuf_custom_data(ctx: XdpContext) -> u32 {
match try_xdp_perfbuf_custom_data(ctx) {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}

#[inline(always)]
unsafe fn ptr_at<T>(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> {
let start = ctx.data();
let end = ctx.data_end();
let len = mem::size_of::<T>();

if start + offset + len > end {
return Err(());
}

Ok((start + offset) as *const T)
}

fn try_xdp_perfbuf_custom_data(ctx: XdpContext) -> Result<u32, ()> {
let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? };
match unsafe { (*ethhdr).ether_type } {
EtherType::Ipv4 => {}
_ => return Ok(xdp_action::XDP_PASS),
}

let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? };
let source_addr = unsafe { (*ipv4hdr).src_addr };

let source_port = match unsafe { (*ipv4hdr).proto } {
IpProto::Tcp => {
let tcphdr: *const TcpHdr =
unsafe { ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN) }?;
u16::from_be(unsafe { (*tcphdr).source })
}
IpProto::Udp => {
let udphdr: *const UdpHdr =
unsafe { ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN) }?;
u16::from_be(unsafe { (*udphdr).source })
}
_ => return Err(()),
};

let log_entry = PacketLog {
ipv4_address: source_addr,
port: source_port as u32,
};
unsafe {
EVENTS.output(&ctx, &log_entry, 0); // (2)
}

Ok(xdp_action::XDP_PASS)
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "xdp-perfbuf-custom-data"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
aya = { version = ">=0.11", features=["async_tokio"] }
aya-log = "0.1"
xdp-perfbuf-custom-data-common = { path = "../xdp-perfbuf-custom-data-common", features=["user"] }
anyhow = "1.0.42"
clap = { version = "4.0", features = ["derive"] }
env_logger = "0.10"
log = "0.4"
tokio = { version = "1.23", features = ["macros", "rt", "rt-multi-thread", "net", "signal"] }
bytes = "1.3"

[[bin]]
name = "xdp-perfbuf-custom-data"
path = "src/main.rs"
Loading

0 comments on commit addbe17

Please sign in to comment.