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 eBPF program
logged the string "received a packet". In this chapter we're going to show how
to parse packets.
While we could go all out and parse data all the way up to L7, we'll constrain
our example to L3, and to make things easier, IPv4 only.
Source code
Full code for the example in this chapter is available
here
Using network types
We're going to log the source IP address of incoming packets. So we'll need to:
Read the Ethernet header to determine if we're dealing with an IPv4 packet,
else terminate parsing.
Read the source IP Address from the IPv4 header.
We could read the specifications of those protocols and parse manually, but
instead we're going to use the network-types
crate which provides convenient type definitions for many of the common Internet
protocols.
Let's add it to our eBPF crate by adding a dependency on network-types in our
xdp-log-ebpf/Cargo.toml:
XdpContext contains two fields that we're going to use: data and data_end,
which are respectively a pointer to the beginning and to the end of the packet.
In order to access the data in the packet and to ensure that we do so in a way
that keeps the eBPF verifier happy, we're going to introduce a helper function
called ptr_at. The function ensures that before we access any packet data, we
insert the bound checks which are required by the verifier.
Finally to access individual fields from the Ethernet and IPv4 headers, we're
going to use the memoffset crate, let's add a dependency for it in
xdp-log-ebpf/Cargo.toml.
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.
useanyhow::Context;useaya::{include_bytes_aligned,programs::{Xdp,XdpFlags},Bpf,};useaya_log::BpfLogger;useclap::Parser;uselog::{info,warn};usetokio::signal;#[derive(Debug, Parser)]structOpt{#[clap(short, long, default_value = "eth0")]iface: String,}#[tokio::main]asyncfnmain()-> Result<(),anyhow::Error>{letopt=Opt::parse();env_logger::init();// This will include your eBPF object file as raw bytes at compile-time and load it at// runtime. This approach is recommended for most real-world use cases. If you would// like to specify the eBPF program at runtime rather than at compile-time, you can// reach for `Bpf::load_file` instead.#[cfg(debug_assertions)]letmutbpf=Bpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/debug/xdp-log"))?;#[cfg(not(debug_assertions))]letmutbpf=Bpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/release/xdp-log"))?;ifletErr(e)=BpfLogger::init(&mutbpf){// This can happen if you remove all log statements from your eBPF program.warn!("failed to initialize eBPF logger: {}",e);}letprogram: &mutXdp=bpf.program_mut("xdp").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")?;info!("Waiting for Ctrl-C...");signal::ctrl_c().await?;info!("Exiting...");Ok(())}
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.
$ 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