From 0b0952c7788c52a7fbc2608014e34abe13f86c57 Mon Sep 17 00:00:00 2001 From: Michal Rostecki Date: Wed, 25 Jan 2023 17:54:27 +0800 Subject: [PATCH] xdp-log: Do not mention PerfEventArray The xdp-log example is using aya-log for logging the data. PerfEventArray is described in #93 instead. Signed-off-by: Michal Rostecki --- docs/book/start/logging-packets.md | 184 ---------------------- docs/book/start/parsing-packets.md | 101 ++++++++++++ examples/xdp-log/xdp-log-ebpf/src/main.rs | 6 +- examples/xdp-log/xdp-log/src/main.rs | 3 +- 4 files changed, 106 insertions(+), 188 deletions(-) delete mode 100644 docs/book/start/logging-packets.md create mode 100644 docs/book/start/parsing-packets.md diff --git a/docs/book/start/logging-packets.md b/docs/book/start/logging-packets.md deleted file mode 100644 index 7c7c054..0000000 --- a/docs/book/start/logging-packets.md +++ /dev/null @@ -1,184 +0,0 @@ -# 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. - -!!! 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, - } - - 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::() == - 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 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). - -Let's add it to our eBPF crate by adding a dependency on `network-types` in our -`xdp-log-ebpf/Cargo.toml`: - -=== "xdp-log-ebpf/Cargo.toml" - - ```toml linenums="1" - --8<-- "examples/xdp-log/xdp-log-ebpf/Cargo.toml" - ``` - -### 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 -pointer to the end of the data in kernel memory. In order to access this data -and ensure that the eBPF verifier is happy, we'll introduce a helper function -called `ptr_at`. This function will ensure that before we access any data, we -check that it's contained between `data` and `data_end`. It is marked as `unsafe` -because when calling the function, you must ensure that there is a valid `T` at -that location or there will be undefined behaviour. - -With our helper function in place, we can: - -1. Read the Ethertype field to check if we have an IPv4 packet. -1. Read the IPv4 Source Address from the IP header - -To do this efficiently we'll add a dependency on `memoffset = "0.8"` in our `myapp-ebpf/Cargo.toml` - -!!! tip "Reading Fields Using `offset_of!`" - - 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` - -The resulting code looks like this: - -```rust linenums="1" title="xdp-log-ebpf/src/main.rs" ---8<-- "examples/xdp-log/xdp-log-ebpf/src/main.rs" -``` - -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` - -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-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. - -## 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 -[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443 -[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443 -[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443 -[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443 -[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 234.130.159.162, SRC PORT: 443 -``` diff --git a/docs/book/start/parsing-packets.md b/docs/book/start/parsing-packets.md new file mode 100644 index 0000000..eda0a75 --- /dev/null +++ b/docs/book/start/parsing-packets.md @@ -0,0 +1,101 @@ +# Parsing 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 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) + +## Using Network Types + +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 [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`: + +=== "xdp-log-ebpf/Cargo.toml" + + ```toml linenums="1" + --8<-- "examples/xdp-log/xdp-log-ebpf/Cargo.toml" + ``` + +## 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 +pointer to the end of the data in kernel memory. In order to access this data +and ensure that the eBPF verifier is happy, we'll introduce a helper function +called `ptr_at`. This function will ensure that before we access any data, we +check that it's contained between `data` and `data_end`. It is marked as `unsafe` +because when calling the function, you must ensure that there is a valid `T` at +that location or there will be undefined behaviour. + +With our helper function in place, we can: + +1. Read the Ethertype field to check if we have an IPv4 packet. +1. Read the IPv4 Source Address from the IP header + +To do this efficiently we'll add a dependency on `memoffset = "0.8"` in our `myapp-ebpf/Cargo.toml` + +!!! tip "Reading Fields Using `offset_of!`" + + 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 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: + +```rust linenums="1" title="xdp-log-ebpf/src/main.rs" +--8<-- "examples/xdp-log/xdp-log-ebpf/src/main.rs" +``` + +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. Logging the IP address and port + +Don't forget to rebuild your eBPF program! + +## Receiving the logs + +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. 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 + +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 +[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443 +[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443 +[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443 +[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 172.52.22.104, SRC PORT: 443 +[2022-12-22T11:32:21Z INFO xdp_log] SRC IP: 234.130.159.162, SRC PORT: 443 +``` diff --git a/examples/xdp-log/xdp-log-ebpf/src/main.rs b/examples/xdp-log/xdp-log-ebpf/src/main.rs index 9559025..52eba03 100644 --- a/examples/xdp-log/xdp-log-ebpf/src/main.rs +++ b/examples/xdp-log/xdp-log-ebpf/src/main.rs @@ -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() } @@ -40,7 +39,7 @@ unsafe fn ptr_at(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> { } fn try_xdp_firewall(ctx: XdpContext) -> Result { - 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), @@ -63,6 +62,7 @@ fn try_xdp_firewall(ctx: XdpContext) -> Result { _ => return Err(()), }; + // (4) info!( &ctx, "SRC IP: {:ipv4}, SRC PORT: {}", source_addr, source_port diff --git a/examples/xdp-log/xdp-log/src/main.rs b/examples/xdp-log/xdp-log/src/main.rs index 295ca70..378c54a 100644 --- a/examples/xdp-log/xdp-log/src/main.rs +++ b/examples/xdp-log/xdp-log/src/main.rs @@ -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())