From 04b92d73b55e0deeb3e6158343591a36f431a18a Mon Sep 17 00:00:00 2001 From: shard Date: Wed, 24 Apr 2024 11:23:57 +0200 Subject: [PATCH 1/7] docs: add Program Types/XDP documentation --- docs/book/programs/xdp.md | 481 +++++++++++++++++++++++++++++++++++++- 1 file changed, 480 insertions(+), 1 deletion(-) diff --git a/docs/book/programs/xdp.md b/docs/book/programs/xdp.md index 62bb43a..1c1efab 100644 --- a/docs/book/programs/xdp.md +++ b/docs/book/programs/xdp.md @@ -1,3 +1,482 @@ # XDP -This page is a work in progress, please feel free to open a Pull Request! +!!! example "Source Code" + + Full code for the example in this chapter is available [here](https://github.com/aya-rs/book/tree/main/examples/xdp-drop) + + +## What is XBP in eBPF? + +XBP (eXpress Data Path) is a type of eBPF program that attaches to the network interface. It allows for the processing of network packets as soon as they are received from the network driver, even before they enter the networking stack. XDP enables efficient packet filtering, manipulation and redirection at the earliest possible stage, resulting in low-latency and high-throughput. + +The idea behind XDP is to add an early hook in the `RX` path of the kernel, and let a user supplied eBPF program decide the fate of the packet. The hook is placed in the NIC driver just after the interrupt processing, and before any memory allocation needed by the network stack itself. + +The XDP program is allowed to edit the packet data and, after the XDP program returns, an action code determines what to do with the packet: + +* `XDP_PASS`: let the packet continue through the network stack +* `XDP_DROP`: silently drop the packet +* `XDP_ABORTED`: drop the packet with trace point exception +* `XDP_TX`: bounce the packet back to the same NIC it arrived on +* `XDP_REDIRECT`: redirect the packet to another NIC or user space socket via the [`AF_XDP`](https://www.kernel.org/doc/html/latest/networking/af_xdp.html) address family + +XDP requires support in the NIC driver but, as not all drivers support it, it can fallback to a generic operation mode, which performs the eBPF processing in the network stack, though with slower performance. + +## AF_XDP +Along with XDP, a new addres familiy entered in the Linux kernel, starting at 4.18. +`AF_XDP`, formerly known as `AF_PACKETv4` (which was never included in the mainline kernel), is a raw socket optimized for high performance packet processing and allows zero-copy between kernel and applications. As the socket can be used for both receiving and transmitting, it supportshigh performance network applications purely in user-space. + +If you want a more extensive explanation about `AF_XDP`, you can find it in the [kernel documentation](https://www.kernel.org/doc/html/latest/networking/af_xdp.html). + +## XDP Operation Modes +You can connect an XDP program to an interface using the following models: + +### Generic XDP +* XDP programs are loaded into the kernel as part of the ordinary network path +* Doesn't need support from the network card driver to function +* Doesn't provide full performance benefits +* Easy way to test XDP programs + +### Native XDP +* XDP programs are loaded by the network card driver as part of its initial receive path +* Requires support from the network card driver to function +* Default operation mode + +### Offloaded XDP +* XDP programs are loaded directly on the NIC, and executed without using the CPU +* Requires support from the NIC + +## Driver support for native XDP + +A list of drivers supporting native XDP can be found in the table below: + +|Vendor|Driver|XDP Support| +|------|------|-----------| +|Amazon|ena |>=5.6 | +|Broadcom|bnxt_en|>=4.11 | +|Cavium|thunderx|>=4.12 | +|Freescale|dpaa2|>=5.0 | +|Intel |ixgbe |>=4.12 | +|Intel |ixgbevf|>=4.17 | +|Intel |i40e |>=4.13 | +|Intel |ice |>=5.5 | +|Marvell|mvneta|>=5.5 | +|Mellanox|mlx4|>=4.8 | +|Mellanox|mlx5|>=4.9 | +|Microsoft|hv_netvsc|>=5.6| +|Netronome|nfp|>=4.10 | +|Others|virtio_net|>=4.10 | +|Others|tun/tap|>=4.14 | +|Others|bond|>=5.15 | +|Qlogic|qede|>=4.10 | +|Socionext|netsec|>=5.3 | +|Solarflare|sfc|>=5.5 | +|Texas Instruments|cpsw|>=5.3| + +You can use the following command to check your interface's network driver name: +`ethtool -i `. + +## Driver support for offloaded XDP + +Currently, only the Netronome NFP drivers have support for offloaded XDP. + +## Example Project + +Now that you have a little more understanding about what XDP is and does, let's follow up with a practical example. We are going to write a simple XDP Program that drops packets incoming from certain IPs. + +### Setting up the development environment + +Make sure you already have the [prerequisites](https://aya-rs.dev/book/start/development/). + +Since we are writing an XDP program, we will going to be using the XDP template with `cargo generate`: + +``` +cargo generate --name simple-xdp-program -d program_type=xdp https://github.com/aya-rs/aya-template +``` + +### Creating the eBPF Component + +First, we must create the eBPF component for our program, in this component, we will decide what to do with the incoming packets. + +Since we want to drop packets incoming from certain IPs, we are going to use the `XDP_DROP` action code for our blacklist, and everything else will be treated with the `XDP_PASS` action code. + +```rust +#![no_std] +#![no_main] + +use aya_ebpf::{ + bindings::xdp_action, + macros::{map, xdp}, + maps::HashMap, + programs::XdpContext, +}; + +use aya_log_ebpf::info; + +use core::mem; +use network_types::{ + eth::{EthHdr, EtherType}, + ip::Ipv4Hdr, +}; +``` + +We import the necessary dependencies: + +* `aya_ebpf`: For XDP actions (`bindings::xdp_action`), the XDP context struct `XdpContext` (`programs:XdpContext`), map definitions (for our HashMap) and XDP program macros (`macros::{map, xdp}`) +* `aya_log_ebpf`: For logging within the eBPF program +* `core::mem`: For memory manipulation +* `network_types`: For Ethernet and IP header definitions + +!!! note "Important" + Make sure you add the `network_types` dependency in your `Cargo.toml`. + +Here's how the code looks: + +```rust +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} +``` + +We add an eBPF-compatible panic handler is provided because eBPF programs cannot use the default panic behavior. + +```rust +#[map] +static BLOCKLIST: HashMap = HashMap::::with_max_entries(1024, 0); +``` + +Here, we define our blocklist with a `HashMap`, which stores integers (u32), with a maximum of 1024 entries. + +```rust +#[xdp] +pub fn xdp_firewall(ctx: XdpContext) -> u32 { + match try_xdp_firewall(ctx) { + Ok(ret) => ret, + Err(_) => xdp_action::XDP_ABORTED, + } +} +``` + +The `xdp_firewall` function (picked up in user-space) accepts an `XdpContext` and returns a `u32`. It delegates the main packet processing logic to the `try_xdp_firewall` function. If an error occurs, the function returns `xdp_action::XDP_ABORTED` (which is equal to the u32 `0`). + +```rust +#[inline(always)] +unsafe fn ptr_at(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> { + let start = ctx.data(); + let end = ctx.data_end(); + let len = mem::size_of::(); + + if start + offset + len > end { + return Err(()); + } + + let ptr = (start + offset) as *const T; + Ok(&*ptr) +} +``` + +Our `ptr_at` function is designed to provide safe access to a generic type `T` within an `XdpContext` at a specified offset. It performs bounds checking by comparing the desired memory range (`start + offset + len`) against the end of the data (`end`). If the access is within bounds, it returns a pointer to the specified type; otherwise, it returns an error. We are going to use this function to retrieve data from the `XdpContext`. + +```rust + +fn block_ip(address: u32) -> bool { + unsafe { BLOCKLIST.get(&address).is_some() } +} + +fn try_xdp_firewall(ctx: XdpContext) -> Result { + 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 = u32::from_be(unsafe { (*ipv4hdr).src_addr }); + + let action = if block_ip(source) { + xdp_action::XDP_DROP + } else { + xdp_action::XDP_PASS + }; + info!(&ctx, "SRC: {:i}, ACTION: {}", source, action); + + Ok(action) +} +``` + +The `block_ip` function checks if a given IP address (address) exists in the blocklist. + +As said before, the `try_xdp_firewall` contains the main logic for our firewall, we first retrieve the Ethernet header from the `XdpContext` with the `ptr_at` function, the header is located at the beginning of the `XdpContext`, therefore we use `0` as an offset. + +If the packet is not IPv4 (`ether_type` check), the function returns `xdp_action::XDP_PASS` and allows the packet to pass through the network stack. + +`ipv4hdr` is used to retrieve the IPv4 header, `source` is used to store the source IP address from the IPv4 header. We then compare the IP address with those that are in our blocklist, using the `block_ip` function we created earlier. If `block_ip` matches, meaning that the IP is in the blocklist, we use the `XDP_DROP` action code so that it doesn't get through the network stack, otherwise we let it pass with the `XDP_PASS` action code. + +Lastly, we log the activity, `SRC` is the source IP address and `ACTION` is the action code that has been used on it. We then return `Ok(action)` as a result. + +The full code: +```rust +#![no_std] +#![no_main] +#![allow(nonstandard_style, dead_code)] + +use aya_ebpf::{ + bindings::xdp_action, + macros::{map, xdp}, + maps::HashMap, + programs::XdpContext, +}; +use aya_log_ebpf::info; + +use core::mem; +use network_types::{ + eth::{EthHdr, EtherType}, + ip::Ipv4Hdr, +}; + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} + +#[map] +static IP_BLOCKLIST: HashMap = HashMap::::with_max_entries(1024, 0); + +#[xdp] +pub fn xdp_firewall(ctx: XdpContext) -> u32 { + match try_xdp_firewall(ctx) { + Ok(ret) => ret, + Err(_) => xdp_action::XDP_ABORTED, + } +} + +#[inline(always)] +unsafe fn ptr_at(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> { + let start = ctx.data(); + let end = ctx.data_end(); + let len = mem::size_of::(); + + if start + offset + len > end { + return Err(()); + } + + let ptr = (start + offset) as *const T; + Ok(&*ptr) +} + +fn block_ip(address: u32) -> bool { + unsafe { IP_BLOCKLIST.get(&address).is_some() } +} + +fn try_xdp_firewall(ctx: XdpContext) -> Result { + 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 = u32::from_be(unsafe { (*ipv4hdr).src_addr }); + + let action = if block_ip(source) { + xdp_action::XDP_DROP + } else { + xdp_action::XDP_PASS + }; + info!(&ctx, "SRC: {:i}, ACTION: {}", source, action); + + Ok(action) +} +``` + +### Populating our map from user-space + +In order to add the addresses to block, we first need to get a reference to the `BLOCKLIST` map. + +Once we have it, it's simply a case of calling `ip_blocklist.insert()` to insert the ips into the blocklist. + +We'll use the `IPv4Addr` type to represent our IP address as it's human-readable and can be easily converted to a u32. + +We'll block all traffic originating from `1.1.1.1` in this example. + +!!! note "Endianness" + + IP addresses are always encoded in network byte order (big endian) within + packets. In our eBPF program, before checking the blocklist, we convert them + to host endian using `u32::from_be`. Therefore it's correct to write our IP + addresses in host endian format from userspace. + + The other approach would work too: we could convert IPs to network endian + when inserting from userspace, and then we wouldn't need to convert when + indexing from the eBPF program. + +Let's begin with writing the user-space code: + +#### Importing dependencies + +```rust +use anyhow::Context; +use aya::{ + include_bytes_aligned, + maps::HashMap, + programs::{Xdp, XdpFlags}, + Bpf, +}; +use aya_log::BpfLogger; +use clap::Parser; +use log::{info, warn}; +use std::net::Ipv4Addr; +use tokio::signal; +``` + +* `anyhow::Context`: Provides additional context for error handling +* `aya`: Provides the Bpf structure and related functions for loading eBPF programs, as well as the XDP program and its flags (`aya::programs::{Xdp, XdpFlags}`) +* `aya_log::BpfLogger`: For logging within the eBPF program +* `clap::Parser`: Provides argument parsing +* `log::{info, warn}`: The [logging library](https://docs.rs/log/latest/log/index.html) we use for informational and warning messages +* `std::net::Ipv4Addr`: A struct to work with IPv4 addresses +* `tokio::signal`: For handling signals asynchronously, see [this link](https://docs.rs/tokio/latest/tokio/signal/) for more information + +!!! note + `aya::Bpf` is deprecated since version `0.13.0` and `aya_log:BpfLogger` since `0.2.1`. Use [`aya::Ebpf`](https://docs.aya-rs.dev/aya/struct.ebpf) and [`aya_log:EbpfLogger`](https://docs.aya-rs.dev/aya_log/struct.ebpflogger) instead if you are using the more recent versions. + +#### Defining command-line arguments + +```rust +#[derive(Debug, Parser)] +struct Opt { + #[clap(short, long, default_value = "eth0")] + iface: String, +} +``` + +A simple struct is defined for command-line parsing using [clap's derive feature](https://docs.rs/clap/latest/clap/_derive/index.html), with the optional argument `iface` to provide our network interface name. + +#### Main Function + +```rust +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let opt = Opt::parse(); + + env_logger::init(); + + #[cfg(debug_assertions)] + let mut bpf = Bpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/debug/simple-xdp-program" + ))?; + #[cfg(not(debug_assertions))] + let mut bpf = Ebpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/release/xdp-simple-xdp-program" + ))?; + if let Err(e) = BpfLogger::init(&mut bpf) { + warn!("failed to initialize eBPF logger: {}", e); + } + let program: &mut Xdp = + bpf.program_mut("xdp_firewall").unwrap().try_into()?; + program.load()?; + program.attach(&opt.iface, XdpFlags::default()) + .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?; + + let mut blocklist: HashMap<_, u32, u32> = + HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?; + + let block_addr: u32 = Ipv4Addr::new(1, 1, 1, 1).try_into()?; + + blocklist.insert(block_addr, 0, 0)?; + + info!("Waiting for Ctrl-C..."); + signal::ctrl_c().await?; + info!("Exiting..."); + + Ok(()) +} +``` + +##### Parsing command-line arguments + +Inside the `main` function, we first parse the command-line arguments, using [`Opt::parse()`](https://docs.rs/clap/latest/clap/trait.Parser.html#method.parse) and the struct defined earlier. + +##### Initializing environment logging + +Logging is initialized using [`env_logger::init()`](https://docs.rs/env_logger/latest/env_logger/fn.init.html), we will make use of the environment logger later in our code. + +##### Loading the eBPF program + +The eBPF program is loaded using `Bpf::load()`, choosing the debug or release version based on the build configuration (`debug_assertions`). + +##### Loading and attaching our XDP + +The XDP program named `xdp_firewall` is retrieved from the eBPF program we defined earlier using `bpf.program_mut()`. The XDP program is then loaded and attached to our network interface. + +##### Setting up the ip blocklist + +The IP blocklist (`BLOCKLIST` map) is loaded from the eBPF program and converted to a `HashMap`. +The IP `1.1.1.1` is added to the blocklist. + +##### Waiting for the exit signal + +The program awais the `CTRL+C` signal asynchronously using `signal::ctrl_c().await`, once received, it logs an exit message and returns `Ok(())`. + +#### Full user-space code + +```rust +use anyhow::Context; +use aya::{ + include_bytes_aligned, + maps::HashMap, + programs::{Xdp, XdpFlags}, + Bpf, +}; +use aya_log::BpfLogger; +use clap::Parser; +use log::{info, warn}; +use std::net::Ipv4Addr; +use tokio::signal; + +#[derive(Debug, Parser)] +struct Opt { + #[clap(short, long, default_value = "eth0")] + iface: String, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let opt = Opt::parse(); + + env_logger::init(); + + #[cfg(debug_assertions)] + let mut bpf = Bpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/debug/simple-xdp-program" + ))?; + #[cfg(not(debug_assertions))] + let mut bpf = Ebpf::load(include_bytes_aligned!( + "../../target/bpfel-unknown-none/release/xdp-simple-xdp-program" + ))?; + if let Err(e) = BpfLogger::init(&mut bpf) { + warn!("failed to initialize eBPF logger: {}", e); + } + let program: &mut Xdp = + bpf.program_mut("xdp_firewall").unwrap().try_into()?; + program.load()?; + program.attach(&opt.iface, XdpFlags::default()) + .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?; + + let mut blocklist: HashMap<_, u32, u32> = + HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?; + + let block_addr: u32 = Ipv4Addr::new(1, 1, 1, 1).try_into()?; + + blocklist.insert(block_addr, 0, 0)?; + + info!("Waiting for Ctrl-C..."); + signal::ctrl_c().await?; + info!("Exiting..."); + + Ok(()) +} +``` + +### Running our program! +Now that we have all the pieces for our eBPF program, we can run it using: `RUST_LOG=info cargo xtask run` or `RUST_LOG=info cargo xtask run -- --iface ` if you want to provide another network interface name, note that you can also use `cargo xtask run` without the rest, but you won't get any logging. From 92d2afcc544f8f20fc386cac5b30403f686c91cb Mon Sep 17 00:00:00 2001 From: shard <106669955+shard77@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:32:47 +0200 Subject: [PATCH 2/7] Update docs/book/programs/xdp.md Co-authored-by: Michal Rostecki --- docs/book/programs/xdp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/programs/xdp.md b/docs/book/programs/xdp.md index 1c1efab..b8e4361 100644 --- a/docs/book/programs/xdp.md +++ b/docs/book/programs/xdp.md @@ -5,7 +5,7 @@ Full code for the example in this chapter is available [here](https://github.com/aya-rs/book/tree/main/examples/xdp-drop) -## What is XBP in eBPF? +## What is XDP in eBPF? XBP (eXpress Data Path) is a type of eBPF program that attaches to the network interface. It allows for the processing of network packets as soon as they are received from the network driver, even before they enter the networking stack. XDP enables efficient packet filtering, manipulation and redirection at the earliest possible stage, resulting in low-latency and high-throughput. From b556b61e26ad435ddd04760085e30dd6d8b5efb3 Mon Sep 17 00:00:00 2001 From: shard <106669955+shard77@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:32:58 +0200 Subject: [PATCH 3/7] Update docs/book/programs/xdp.md Co-authored-by: Michal Rostecki --- docs/book/programs/xdp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/programs/xdp.md b/docs/book/programs/xdp.md index b8e4361..8515147 100644 --- a/docs/book/programs/xdp.md +++ b/docs/book/programs/xdp.md @@ -7,7 +7,7 @@ ## What is XDP in eBPF? -XBP (eXpress Data Path) is a type of eBPF program that attaches to the network interface. It allows for the processing of network packets as soon as they are received from the network driver, even before they enter the networking stack. XDP enables efficient packet filtering, manipulation and redirection at the earliest possible stage, resulting in low-latency and high-throughput. +XDP (eXpress Data Path) is a type of eBPF program that attaches to the network interface. It enables filtering, manipulation and refirection of network packets as soon as they are received from the network driver, even before they enter the Linux kernel networking stack, resulting in low latency and high throughput. The idea behind XDP is to add an early hook in the `RX` path of the kernel, and let a user supplied eBPF program decide the fate of the packet. The hook is placed in the NIC driver just after the interrupt processing, and before any memory allocation needed by the network stack itself. From a6125f1fbc8bf24bc5872391b45c9dc3bcdf7733 Mon Sep 17 00:00:00 2001 From: shard <106669955+shard77@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:40:26 +0200 Subject: [PATCH 4/7] Update xdp.md --- docs/book/programs/xdp.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/book/programs/xdp.md b/docs/book/programs/xdp.md index 8515147..e652edd 100644 --- a/docs/book/programs/xdp.md +++ b/docs/book/programs/xdp.md @@ -6,7 +6,6 @@ ## What is XDP in eBPF? - XDP (eXpress Data Path) is a type of eBPF program that attaches to the network interface. It enables filtering, manipulation and refirection of network packets as soon as they are received from the network driver, even before they enter the Linux kernel networking stack, resulting in low latency and high throughput. The idea behind XDP is to add an early hook in the `RX` path of the kernel, and let a user supplied eBPF program decide the fate of the packet. The hook is placed in the NIC driver just after the interrupt processing, and before any memory allocation needed by the network stack itself. @@ -19,8 +18,6 @@ The XDP program is allowed to edit the packet data and, after the XDP program re * `XDP_TX`: bounce the packet back to the same NIC it arrived on * `XDP_REDIRECT`: redirect the packet to another NIC or user space socket via the [`AF_XDP`](https://www.kernel.org/doc/html/latest/networking/af_xdp.html) address family -XDP requires support in the NIC driver but, as not all drivers support it, it can fallback to a generic operation mode, which performs the eBPF processing in the network stack, though with slower performance. - ## AF_XDP Along with XDP, a new addres familiy entered in the Linux kernel, starting at 4.18. `AF_XDP`, formerly known as `AF_PACKETv4` (which was never included in the mainline kernel), is a raw socket optimized for high performance packet processing and allows zero-copy between kernel and applications. As the socket can be used for both receiving and transmitting, it supportshigh performance network applications purely in user-space. @@ -76,15 +73,12 @@ You can use the following command to check your interface's network driver name: `ethtool -i `. ## Driver support for offloaded XDP - Currently, only the Netronome NFP drivers have support for offloaded XDP. ## Example Project - Now that you have a little more understanding about what XDP is and does, let's follow up with a practical example. We are going to write a simple XDP Program that drops packets incoming from certain IPs. ### Setting up the development environment - Make sure you already have the [prerequisites](https://aya-rs.dev/book/start/development/). Since we are writing an XDP program, we will going to be using the XDP template with `cargo generate`: @@ -94,7 +88,6 @@ cargo generate --name simple-xdp-program -d program_type=xdp https://github.com/ ``` ### Creating the eBPF Component - First, we must create the eBPF component for our program, in this component, we will decide what to do with the incoming packets. Since we want to drop packets incoming from certain IPs, we are going to use the `XDP_DROP` action code for our blacklist, and everything else will be treated with the `XDP_PASS` action code. @@ -290,7 +283,6 @@ fn try_xdp_firewall(ctx: XdpContext) -> Result { ``` ### Populating our map from user-space - In order to add the addresses to block, we first need to get a reference to the `BLOCKLIST` map. Once we have it, it's simply a case of calling `ip_blocklist.insert()` to insert the ips into the blocklist. @@ -394,28 +386,22 @@ async fn main() -> Result<(), anyhow::Error> { ``` ##### Parsing command-line arguments - Inside the `main` function, we first parse the command-line arguments, using [`Opt::parse()`](https://docs.rs/clap/latest/clap/trait.Parser.html#method.parse) and the struct defined earlier. ##### Initializing environment logging - Logging is initialized using [`env_logger::init()`](https://docs.rs/env_logger/latest/env_logger/fn.init.html), we will make use of the environment logger later in our code. ##### Loading the eBPF program - The eBPF program is loaded using `Bpf::load()`, choosing the debug or release version based on the build configuration (`debug_assertions`). ##### Loading and attaching our XDP - The XDP program named `xdp_firewall` is retrieved from the eBPF program we defined earlier using `bpf.program_mut()`. The XDP program is then loaded and attached to our network interface. ##### Setting up the ip blocklist - The IP blocklist (`BLOCKLIST` map) is loaded from the eBPF program and converted to a `HashMap`. The IP `1.1.1.1` is added to the blocklist. ##### Waiting for the exit signal - The program awais the `CTRL+C` signal asynchronously using `signal::ctrl_c().await`, once received, it logs an exit message and returns `Ok(())`. #### Full user-space code From 4367ea9fc492b8f16d4bab6a8495292a8bac37fa Mon Sep 17 00:00:00 2001 From: shard <106669955+shard77@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:46:14 +0200 Subject: [PATCH 5/7] Update xdp.md --- docs/book/programs/xdp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/programs/xdp.md b/docs/book/programs/xdp.md index e652edd..acc8bd6 100644 --- a/docs/book/programs/xdp.md +++ b/docs/book/programs/xdp.md @@ -25,7 +25,7 @@ Along with XDP, a new addres familiy entered in the Linux kernel, starting at 4. If you want a more extensive explanation about `AF_XDP`, you can find it in the [kernel documentation](https://www.kernel.org/doc/html/latest/networking/af_xdp.html). ## XDP Operation Modes -You can connect an XDP program to an interface using the following models: +You can connect an XDP program to an interface using the following modes: ### Generic XDP * XDP programs are loaded into the kernel as part of the ordinary network path From 3dfcf8503f11d6d0533ebe160766b0d8fed5fb73 Mon Sep 17 00:00:00 2001 From: shard <106669955+shard77@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:06:04 +0200 Subject: [PATCH 6/7] Update xdp.md --- docs/book/programs/xdp.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/book/programs/xdp.md b/docs/book/programs/xdp.md index acc8bd6..92bfaa1 100644 --- a/docs/book/programs/xdp.md +++ b/docs/book/programs/xdp.md @@ -19,8 +19,8 @@ The XDP program is allowed to edit the packet data and, after the XDP program re * `XDP_REDIRECT`: redirect the packet to another NIC or user space socket via the [`AF_XDP`](https://www.kernel.org/doc/html/latest/networking/af_xdp.html) address family ## AF_XDP -Along with XDP, a new addres familiy entered in the Linux kernel, starting at 4.18. -`AF_XDP`, formerly known as `AF_PACKETv4` (which was never included in the mainline kernel), is a raw socket optimized for high performance packet processing and allows zero-copy between kernel and applications. As the socket can be used for both receiving and transmitting, it supportshigh performance network applications purely in user-space. +Along with XDP, a new address familiy entered in the Linux kernel, starting at 4.18. +`AF_XDP`, formerly known as `AF_PACKETv4` (which was never included in the mainline kernel), is a raw socket optimized for high performance packet processing and allows zero-copy between kernel and applications. As the socket can be used for both receiving and transmitting, it supports high performance network applications purely in user-space. If you want a more extensive explanation about `AF_XDP`, you can find it in the [kernel documentation](https://www.kernel.org/doc/html/latest/networking/af_xdp.html). @@ -76,12 +76,13 @@ You can use the following command to check your interface's network driver name: Currently, only the Netronome NFP drivers have support for offloaded XDP. ## Example Project -Now that you have a little more understanding about what XDP is and does, let's follow up with a practical example. We are going to write a simple XDP Program that drops packets incoming from certain IPs. +Now that you have a little more understanding about XDP, let's follow up with a practical example. +We are going to write a simple XDP Program that drops packets incoming from certain IPs. ### Setting up the development environment Make sure you already have the [prerequisites](https://aya-rs.dev/book/start/development/). -Since we are writing an XDP program, we will going to be using the XDP template with `cargo generate`: +Since we are writing an XDP program, we will use the XDP template (created with `cargo generate`): ``` cargo generate --name simple-xdp-program -d program_type=xdp https://github.com/aya-rs/aya-template @@ -90,7 +91,7 @@ cargo generate --name simple-xdp-program -d program_type=xdp https://github.com/ ### Creating the eBPF Component First, we must create the eBPF component for our program, in this component, we will decide what to do with the incoming packets. -Since we want to drop packets incoming from certain IPs, we are going to use the `XDP_DROP` action code for our blacklist, and everything else will be treated with the `XDP_PASS` action code. +Since we want to drop the incoming packets from certain IPs, we are going to use the `XDP_DROP` action code whenever the IP is in our blacklist, and everything else will be treated with the `XDP_PASS` action code. ```rust #![no_std] @@ -131,7 +132,7 @@ fn panic(_info: &core::panic::PanicInfo) -> ! { } ``` -We add an eBPF-compatible panic handler is provided because eBPF programs cannot use the default panic behavior. +An eBPF-compatible panic handler is provided because eBPF programs cannot use the default panic behavior. ```rust #[map] @@ -199,11 +200,11 @@ fn try_xdp_firewall(ctx: XdpContext) -> Result { The `block_ip` function checks if a given IP address (address) exists in the blocklist. -As said before, the `try_xdp_firewall` contains the main logic for our firewall, we first retrieve the Ethernet header from the `XdpContext` with the `ptr_at` function, the header is located at the beginning of the `XdpContext`, therefore we use `0` as an offset. +As said before, the `try_xdp_firewall` contains the main logic for our firewall. We first retrieve the Ethernet header from the `XdpContext` with the `ptr_at` function, the header is located at the beginning of the `XdpContext`, therefore we use `0` as an offset. If the packet is not IPv4 (`ether_type` check), the function returns `xdp_action::XDP_PASS` and allows the packet to pass through the network stack. -`ipv4hdr` is used to retrieve the IPv4 header, `source` is used to store the source IP address from the IPv4 header. We then compare the IP address with those that are in our blocklist, using the `block_ip` function we created earlier. If `block_ip` matches, meaning that the IP is in the blocklist, we use the `XDP_DROP` action code so that it doesn't get through the network stack, otherwise we let it pass with the `XDP_PASS` action code. +`ipv4hdr` is used to retrieve the IPv4 header, `source` is used to store the source IP address from the IPv4 header. We then compare the IP address with those that are in our blocklist using the `block_ip` function we created earlier. If `block_ip` matches, meaning that the IP is in the blocklist, we use the `XDP_DROP` action code so that it doesn't get through the network stack, otherwise we let it pass with the `XDP_PASS` action code. Lastly, we log the activity, `SRC` is the source IP address and `ACTION` is the action code that has been used on it. We then return `Ok(action)` as a result. From 471d5b497283f129423e37b6d1b9257d6070d0e7 Mon Sep 17 00:00:00 2001 From: shard <106669955+shard77@users.noreply.github.com> Date: Thu, 30 May 2024 20:45:46 +0200 Subject: [PATCH 7/7] Update xdp.md --- docs/book/programs/xdp.md | 149 +++++++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 51 deletions(-) diff --git a/docs/book/programs/xdp.md b/docs/book/programs/xdp.md index 92bfaa1..e3674d9 100644 --- a/docs/book/programs/xdp.md +++ b/docs/book/programs/xdp.md @@ -6,23 +6,36 @@ ## What is XDP in eBPF? -XDP (eXpress Data Path) is a type of eBPF program that attaches to the network interface. It enables filtering, manipulation and refirection of network packets as soon as they are received from the network driver, even before they enter the Linux kernel networking stack, resulting in low latency and high throughput. +XDP (eXpress Data Path) is a type of eBPF program that attaches to the network interface. +It enables filtering, manipulation and refirection of network packets +as soon as they are received from the network driver, +even before they enter the Linux kernel networking stack, resulting in low latency and high throughput. -The idea behind XDP is to add an early hook in the `RX` path of the kernel, and let a user supplied eBPF program decide the fate of the packet. The hook is placed in the NIC driver just after the interrupt processing, and before any memory allocation needed by the network stack itself. +The idea behind XDP is to add an early hook in the `RX` path of the kernel, +and let a user supplied eBPF program decide the fate of the packet. +The hook is placed in the NIC driver just after the interrupt processing, +and before any memory allocation needed by the network stack itself. -The XDP program is allowed to edit the packet data and, after the XDP program returns, an action code determines what to do with the packet: +The XDP program is allowed to edit the packet data and, +after the XDP program returns, an action code determines what to do with the packet: * `XDP_PASS`: let the packet continue through the network stack * `XDP_DROP`: silently drop the packet * `XDP_ABORTED`: drop the packet with trace point exception * `XDP_TX`: bounce the packet back to the same NIC it arrived on -* `XDP_REDIRECT`: redirect the packet to another NIC or user space socket via the [`AF_XDP`](https://www.kernel.org/doc/html/latest/networking/af_xdp.html) address family +* `XDP_REDIRECT`: redirect the packet to another NIC or user space socket via the +[`AF_XDP`](https://www.kernel.org/doc/html/latest/networking/af_xdp.html) address family ## AF_XDP Along with XDP, a new address familiy entered in the Linux kernel, starting at 4.18. -`AF_XDP`, formerly known as `AF_PACKETv4` (which was never included in the mainline kernel), is a raw socket optimized for high performance packet processing and allows zero-copy between kernel and applications. As the socket can be used for both receiving and transmitting, it supports high performance network applications purely in user-space. +`AF_XDP`, formerly known as `AF_PACKETv4` (which was never included in the mainline kernel), +is a raw socket optimized for high performance packet processing and +allows zero-copy between kernel and applications. +As the socket can be used for both receiving and transmitting, +it supports high performance network applications purely in user-space. -If you want a more extensive explanation about `AF_XDP`, you can find it in the [kernel documentation](https://www.kernel.org/doc/html/latest/networking/af_xdp.html). +If you want a more extensive explanation about `AF_XDP`, +you can find it in the [kernel documentation](https://www.kernel.org/doc/html/latest/networking/af_xdp.html). ## XDP Operation Modes You can connect an XDP program to an interface using the following modes: @@ -46,28 +59,28 @@ You can connect an XDP program to an interface using the following modes: A list of drivers supporting native XDP can be found in the table below: -|Vendor|Driver|XDP Support| -|------|------|-----------| -|Amazon|ena |>=5.6 | -|Broadcom|bnxt_en|>=4.11 | -|Cavium|thunderx|>=4.12 | -|Freescale|dpaa2|>=5.0 | -|Intel |ixgbe |>=4.12 | -|Intel |ixgbevf|>=4.17 | -|Intel |i40e |>=4.13 | -|Intel |ice |>=5.5 | -|Marvell|mvneta|>=5.5 | -|Mellanox|mlx4|>=4.8 | -|Mellanox|mlx5|>=4.9 | -|Microsoft|hv_netvsc|>=5.6| -|Netronome|nfp|>=4.10 | -|Others|virtio_net|>=4.10 | -|Others|tun/tap|>=4.14 | -|Others|bond|>=5.15 | -|Qlogic|qede|>=4.10 | -|Socionext|netsec|>=5.3 | -|Solarflare|sfc|>=5.5 | -|Texas Instruments|cpsw|>=5.3| +| Vendor | Driver | XDP Support | +| ----------------- | ---------- | ----------- | +| Amazon | ena | >=5.6 | +| Broadcom | bnxt_en | >=4.11 | +| Cavium | thunderx | >=4.12 | +| Freescale | dpaa2 | >=5.0 | +| Intel | ixgbe | >=4.12 | +| Intel | ixgbevf | >=4.17 | +| Intel | i40e | >=4.13 | +| Intel | ice | >=5.5 | +| Marvell | mvneta | >=5.5 | +| Mellanox | mlx4 | >=4.8 | +| Mellanox | mlx5 | >=4.9 | +| Microsoft | hv_netvsc | >=5.6 | +| Netronome | nfp | >=4.10 | +| Others | virtio_net | >=4.10 | +| Others | tun/tap | >=4.14 | +| Others | bond | >=5.15 | +| Qlogic | qede | >=4.10 | +| Socionext | netsec | >=5.3 | +| Solarflare | sfc | >=5.5 | +| Texas Instruments | cpsw | >=5.3 | You can use the following command to check your interface's network driver name: `ethtool -i `. @@ -89,9 +102,12 @@ cargo generate --name simple-xdp-program -d program_type=xdp https://github.com/ ``` ### Creating the eBPF Component -First, we must create the eBPF component for our program, in this component, we will decide what to do with the incoming packets. +First, we must create the eBPF component for our program, +in this component, we will decide what to do with the incoming packets. -Since we want to drop the incoming packets from certain IPs, we are going to use the `XDP_DROP` action code whenever the IP is in our blacklist, and everything else will be treated with the `XDP_PASS` action code. +Since we want to drop the incoming packets from certain IPs, +we are going to use the `XDP_DROP` action code whenever the IP is in our blacklist, +and everything else will be treated with the `XDP_PASS` action code. ```rust #![no_std] @@ -115,7 +131,8 @@ use network_types::{ We import the necessary dependencies: -* `aya_ebpf`: For XDP actions (`bindings::xdp_action`), the XDP context struct `XdpContext` (`programs:XdpContext`), map definitions (for our HashMap) and XDP program macros (`macros::{map, xdp}`) +* `aya_ebpf`: For XDP actions (`bindings::xdp_action`), the XDP context struct `XdpContext` (`programs:XdpContext`), +map definitions (for our HashMap) and XDP program macros (`macros::{map, xdp}`) * `aya_log_ebpf`: For logging within the eBPF program * `core::mem`: For memory manipulation * `network_types`: For Ethernet and IP header definitions @@ -132,14 +149,16 @@ fn panic(_info: &core::panic::PanicInfo) -> ! { } ``` -An eBPF-compatible panic handler is provided because eBPF programs cannot use the default panic behavior. +An eBPF-compatible panic handler is provided because +eBPF programs cannot use the default panic behavior. ```rust #[map] static BLOCKLIST: HashMap = HashMap::::with_max_entries(1024, 0); ``` -Here, we define our blocklist with a `HashMap`, which stores integers (u32), with a maximum of 1024 entries. +Here, we define our blocklist with a `HashMap`, +which stores integers (u32), with a maximum of 1024 entries. ```rust #[xdp] @@ -151,7 +170,9 @@ pub fn xdp_firewall(ctx: XdpContext) -> u32 { } ``` -The `xdp_firewall` function (picked up in user-space) accepts an `XdpContext` and returns a `u32`. It delegates the main packet processing logic to the `try_xdp_firewall` function. If an error occurs, the function returns `xdp_action::XDP_ABORTED` (which is equal to the u32 `0`). +The `xdp_firewall` function (picked up in user-space) accepts an `XdpContext` and returns a `u32`. +It delegates the main packet processing logic to the `try_xdp_firewall` function. +If an error occurs, the function returns `xdp_action::XDP_ABORTED` (which is equal to the u32 `0`). ```rust #[inline(always)] @@ -169,7 +190,11 @@ unsafe fn ptr_at(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> { } ``` -Our `ptr_at` function is designed to provide safe access to a generic type `T` within an `XdpContext` at a specified offset. It performs bounds checking by comparing the desired memory range (`start + offset + len`) against the end of the data (`end`). If the access is within bounds, it returns a pointer to the specified type; otherwise, it returns an error. We are going to use this function to retrieve data from the `XdpContext`. +Our `ptr_at` function is designed to provide safe access to a generic type `T` +within an `XdpContext` at a specified offset. +It performs bounds checking by comparing the desired memory range (`start + offset + len`) against the end of the data (`end`). +If the access is within bounds, it returns a pointer to the specified type; otherwise, +it returns an error. We are going to use this function to retrieve data from the `XdpContext`. ```rust @@ -200,13 +225,20 @@ fn try_xdp_firewall(ctx: XdpContext) -> Result { The `block_ip` function checks if a given IP address (address) exists in the blocklist. -As said before, the `try_xdp_firewall` contains the main logic for our firewall. We first retrieve the Ethernet header from the `XdpContext` with the `ptr_at` function, the header is located at the beginning of the `XdpContext`, therefore we use `0` as an offset. +As said before, the `try_xdp_firewall` contains the main logic for our firewall. +We first retrieve the Ethernet header from the `XdpContext` with the `ptr_at` function, +the header is located at the beginning of the `XdpContext`, therefore we use `0` as an offset. -If the packet is not IPv4 (`ether_type` check), the function returns `xdp_action::XDP_PASS` and allows the packet to pass through the network stack. +If the packet is not IPv4 (`ether_type` check), the function returns `xdp_action::XDP_PASS` and +allows the packet to pass through the network stack. -`ipv4hdr` is used to retrieve the IPv4 header, `source` is used to store the source IP address from the IPv4 header. We then compare the IP address with those that are in our blocklist using the `block_ip` function we created earlier. If `block_ip` matches, meaning that the IP is in the blocklist, we use the `XDP_DROP` action code so that it doesn't get through the network stack, otherwise we let it pass with the `XDP_PASS` action code. +`ipv4hdr` is used to retrieve the IPv4 header, `source` is used to store the source IP address from the IPv4 header. +We then compare the IP address with those that are in our blocklist using the `block_ip` function we created earlier. +If `block_ip` matches, meaning that the IP is in the blocklist, we use the `XDP_DROP` action code so that it doesn't +get through the network stack, otherwise we let it pass with the `XDP_PASS` action code. -Lastly, we log the activity, `SRC` is the source IP address and `ACTION` is the action code that has been used on it. We then return `Ok(action)` as a result. +Lastly, we log the activity, `SRC` is the source IP address and `ACTION` +is the action code that has been used on it. We then return `Ok(action)` as a result. The full code: ```rust @@ -286,9 +318,11 @@ fn try_xdp_firewall(ctx: XdpContext) -> Result { ### Populating our map from user-space In order to add the addresses to block, we first need to get a reference to the `BLOCKLIST` map. -Once we have it, it's simply a case of calling `ip_blocklist.insert()` to insert the ips into the blocklist. +Once we have it, it's simply a case of calling `ip_blocklist.insert()` +to insert the ips into the blocklist. -We'll use the `IPv4Addr` type to represent our IP address as it's human-readable and can be easily converted to a u32. +We'll use the `IPv4Addr` type to represent our IP address as +it's human-readable and can be easily converted to a u32. We'll block all traffic originating from `1.1.1.1` in this example. @@ -323,15 +357,19 @@ use tokio::signal; ``` * `anyhow::Context`: Provides additional context for error handling -* `aya`: Provides the Bpf structure and related functions for loading eBPF programs, as well as the XDP program and its flags (`aya::programs::{Xdp, XdpFlags}`) +* `aya`: Provides the Bpf structure and related functions for loading eBPF programs, +as well as the XDP program and its flags (`aya::programs::{Xdp, XdpFlags}`) * `aya_log::BpfLogger`: For logging within the eBPF program * `clap::Parser`: Provides argument parsing -* `log::{info, warn}`: The [logging library](https://docs.rs/log/latest/log/index.html) we use for informational and warning messages +* `log::{info, warn}`: The [logging library](https://docs.rs/log/latest/log/index.html) +we use for informational and warning messages * `std::net::Ipv4Addr`: A struct to work with IPv4 addresses * `tokio::signal`: For handling signals asynchronously, see [this link](https://docs.rs/tokio/latest/tokio/signal/) for more information !!! note - `aya::Bpf` is deprecated since version `0.13.0` and `aya_log:BpfLogger` since `0.2.1`. Use [`aya::Ebpf`](https://docs.aya-rs.dev/aya/struct.ebpf) and [`aya_log:EbpfLogger`](https://docs.aya-rs.dev/aya_log/struct.ebpflogger) instead if you are using the more recent versions. + `aya::Bpf` is deprecated since version `0.13.0` and `aya_log:BpfLogger` since `0.2.1`. + Use [`aya::Ebpf`](https://docs.aya-rs.dev/aya/struct.ebpf) and + [`aya_log:EbpfLogger`](https://docs.aya-rs.dev/aya_log/struct.ebpflogger) instead if you are using the more recent versions. #### Defining command-line arguments @@ -343,7 +381,8 @@ struct Opt { } ``` -A simple struct is defined for command-line parsing using [clap's derive feature](https://docs.rs/clap/latest/clap/_derive/index.html), with the optional argument `iface` to provide our network interface name. +A simple struct is defined for command-line parsing using [clap's derive feature](https://docs.rs/clap/latest/clap/_derive/index.html), +with the optional argument `iface` to provide our network interface name. #### Main Function @@ -387,23 +426,29 @@ async fn main() -> Result<(), anyhow::Error> { ``` ##### Parsing command-line arguments -Inside the `main` function, we first parse the command-line arguments, using [`Opt::parse()`](https://docs.rs/clap/latest/clap/trait.Parser.html#method.parse) and the struct defined earlier. +Inside the `main` function, we first parse the command-line arguments, +using [`Opt::parse()`](https://docs.rs/clap/latest/clap/trait.Parser.html#method.parse) and the struct defined earlier. ##### Initializing environment logging -Logging is initialized using [`env_logger::init()`](https://docs.rs/env_logger/latest/env_logger/fn.init.html), we will make use of the environment logger later in our code. +Logging is initialized using [`env_logger::init()`](https://docs.rs/env_logger/latest/env_logger/fn.init.html), +we will make use of the environment logger later in our code. ##### Loading the eBPF program -The eBPF program is loaded using `Bpf::load()`, choosing the debug or release version based on the build configuration (`debug_assertions`). +The eBPF program is loaded using `Bpf::load()`, choosing the debug or +release version based on the build configuration (`debug_assertions`). ##### Loading and attaching our XDP -The XDP program named `xdp_firewall` is retrieved from the eBPF program we defined earlier using `bpf.program_mut()`. The XDP program is then loaded and attached to our network interface. +The XDP program named `xdp_firewall` is retrieved from the eBPF program + we defined earlier using `bpf.program_mut()`. + The XDP program is then loaded and attached to our network interface. ##### Setting up the ip blocklist The IP blocklist (`BLOCKLIST` map) is loaded from the eBPF program and converted to a `HashMap`. The IP `1.1.1.1` is added to the blocklist. ##### Waiting for the exit signal -The program awais the `CTRL+C` signal asynchronously using `signal::ctrl_c().await`, once received, it logs an exit message and returns `Ok(())`. +The program awais the `CTRL+C` signal asynchronously using `signal::ctrl_c().await`, +once received, it logs an exit message and returns `Ok(())`. #### Full user-space code @@ -466,4 +511,6 @@ async fn main() -> Result<(), anyhow::Error> { ``` ### Running our program! -Now that we have all the pieces for our eBPF program, we can run it using: `RUST_LOG=info cargo xtask run` or `RUST_LOG=info cargo xtask run -- --iface ` if you want to provide another network interface name, note that you can also use `cargo xtask run` without the rest, but you won't get any logging. +Now that we have all the pieces for our eBPF program, we can run it using: `RUST_LOG=info cargo xtask run` +or `RUST_LOG=info cargo xtask run -- --iface ` if you want to provide another network interface name, +note that you can also use `cargo xtask run` without the rest, but you won't get any logging.