Skip to content

Commit

Permalink
xdp-log: Do not mention PerfEventArray
Browse files Browse the repository at this point in the history
The xdp-log example is using aya-log for logging the data.

PerfEventArray is described in aya-rs#93 instead.

Signed-off-by: Michal Rostecki <vadorovsky@gmail.com>
  • Loading branch information
vadorovsky committed Jan 28, 2023
1 parent f001417 commit a49f6e7
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 108 deletions.
125 changes: 21 additions & 104 deletions docs/book/start/logging-packets.md
Original file line number Diff line number Diff line change
@@ -1,104 +1,29 @@
# Logging Packets

In the previous chapter, our XDP application ran until Ctrl-C was hit and permitted all the traffic.
Each time a packet was received, the BPF program created a log entry.
Let's expand this program to log the traffic that is being permitted in the user-space application instead of the BPF program.
In the previous chapter, our XDP application ran until Ctrl-C was hit and permitted
all the traffic. Each time a packet was received, the BPF program created a logged
the string "received a packet" for each packet received. In this chapter we're
going to show how to parse packets.

While we could go all out and extract data all the way up to L7, we'll constrain
our firewall to L3, and to make things easier, IPv4 only.

!!! example "Source Code"

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

## Getting Data to User-Space

### 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.

While we could go all out and extract data all the way up to L7, we'll constrain our firewall to L3, and to make things easier, IPv4 only.
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-log-common/src/lib.rs"
--8<-- "examples/xdp-log/xdp-log-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,
}
## Using Network Types

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 };
```

## Writing Data

### Using Kernel Network Types

To get useful data to add to our maps, we first need some useful data structures
To get useful data to log, we first need some useful data structures to
to populate with data from the `XdpContext`.
We want to log the Source IP Address of incoming traffic, so we'll need to:

1. Read the Ethernet Header to determine if this is an IPv4 Packet
1. Read the Source IP Address from the IPv4 Header

The two structs in the kernel for this are `ethhdr` from `uapi/linux/if_ether.h`
and `iphdr` from `uapi/linux/ip.h`. Rust equivalents of those structures (`EthHdr`
and `Ipv4Hdr`) are provided by the [network-types crate](https://crates.io/crates/network-types).
The [network-types crate](https://crates.io/crates/network-types) provides
networking structs, including `EthHdr` and `Ipv4Hdr` which we are ging to use
in our program.

Let's add it to our eBPF crate by adding a dependency on `network-types` in our
`xdp-log-ebpf/Cargo.toml`:
Expand All @@ -109,7 +34,7 @@ Let's add it to our eBPF crate by adding a dependency on `network-types` in our
--8<-- "examples/xdp-log/xdp-log-ebpf/Cargo.toml"
```

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

The `XdpContext` contains two fields, `data` and `data_end`.
`data` is a pointer to the start of the data in kernel memory and `data_end`, a
Expand All @@ -132,7 +57,9 @@ To do this efficiently we'll add a dependency on `memoffset = "0.8"` in our `mya
As there is limited stack space, it's more memory efficient to use the `offset_of!` macro to read
a single field from a struct, rather than reading the whole struct and accessing the field by name.

Once we have our IPv4 source address, we can create a `PacketLog` struct and output this to our `PerfEventArray`
Once we have our IPv4 source address, we can log it with macros coming from aya-log. Those macros are
sending the log message and all the logged data to the user-space program, which then does the
actual logging.

The resulting code looks like this:

Expand All @@ -143,32 +70,22 @@ The resulting code looks like this:
1. Create our map
2. Here's `ptr_at`, which gives ensures packet access is bounds checked
3. Using `ptr_at` to read our ethernet header
4. Outputting the event to the `PerfEventArray`
4. Logging the IP address and port

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.
## Receiving the logs

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`.
In order to receive the logs in the user-space program, we have to call `BpfLogger::init()`.

Here's the code:

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

1. Name was not defined in `xdp-log-ebpf/src/main.rs`, so use `xdp`
2. Define our map
3. Call `open()` for each online CPU
4. Spawn a `tokio::task`
5. Create buffers
6. Read events in to buffers
7. Use `read_unaligned` to read our data into a `PacketLog`.
8. Log the event to the console.
1. Initialize `BpfLogger` to receive and process log messages and data from eBPF.
2. Name was not defined in `xdp-log-ebpf/src/main.rs`, so use `xdp`

## Running the program

Expand Down
6 changes: 3 additions & 3 deletions examples/xdp-log/xdp-log-ebpf/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ use aya_log_ebpf::info;
use core::mem;
use network_types::{
eth::{EthHdr, EtherType},
ip::{Ipv4Hdr, IpProto},
ip::{IpProto, Ipv4Hdr},
tcp::TcpHdr,
udp::UdpHdr,
};


#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
Expand Down Expand Up @@ -40,7 +39,7 @@ unsafe fn ptr_at<T>(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> {
}

fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? };
let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? }; // (3)
match unsafe { (*ethhdr).ether_type } {
EtherType::Ipv4 => {}
_ => return Ok(xdp_action::XDP_PASS),
Expand All @@ -63,6 +62,7 @@ fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
_ => return Err(()),
};

// (4)
info!(
&ctx,
"SRC IP: {:ipv4}, SRC PORT: {}", source_addr, source_port
Expand Down
3 changes: 2 additions & 1 deletion examples/xdp-log/xdp-log/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ async fn main() -> Result<(), anyhow::Error> {
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/xdp-log"
))?;
// (1)
if let Err(e) = BpfLogger::init(&mut bpf) {
// This can happen if you remove all log statements from your eBPF program.
warn!("failed to initialize eBPF logger: {}", e);
}
// (1)
// (2)
let program: &mut Xdp = bpf.program_mut("xdp").unwrap().try_into()?;
program.load()?;
program.attach(&opt.iface, XdpFlags::default())
Expand Down

0 comments on commit a49f6e7

Please sign in to comment.