In the previous chapter our XDP program just logged traffic. In this chapter
we're going to extend it to allow the dropping of traffic.
Source Code
Full code for the example in this chapter is available here
Design
In order for our program to drop packets, we're going to need a list of IP
addresses to drop. Since we want to be able to lookup them up efficiently, we're
going to use a
HashMap to hold
them.
We're going to:
Create a HashMap in our eBPF program that will act as a blocklist
Check the IP address from the packet against the HashMap to make a policy
decision (pass or drop)
Add entries to the blocklist from userspace
Dropping packets in eBPF
We will create a new map called BLOCKLIST in our eBPF code. In order to make
the policy decision, we will need to lookup the source IP address in our
HashMap. If it exists we drop the packet, if it does not, we allow it. We'll
keep this logic in a function called block_ip.
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
blocklist.insert(). 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.
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.
useanyhow::Context;useaya::{include_bytes_aligned,maps::HashMap,programs::{Xdp,XdpFlags},Bpf,};useaya_log::BpfLogger;useclap::Parser;uselog::{info,warn};usestd::net::Ipv4Addr;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-drop"))?;#[cfg(not(debug_assertions))]letmutbpf=Bpf::load(include_bytes_aligned!("../../target/bpfel-unknown-none/release/xdp-drop"))?;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_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")?;// (1)letmutblocklist: HashMap<_,u32,u32>=HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?;// (2)letblock_addr: u32=Ipv4Addr::new(1,1,1,1).try_into()?;// (3)blocklist.insert(block_addr,0,0)?;info!("Waiting for Ctrl-C...");signal::ctrl_c().await?;info!("Exiting...");Ok(())}
Get a reference to the map
Create an IPv4Addr
Write this to our map
Running the program
$ RUST_LOG=info cargo xtask run
[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 1.1.1.1, ACTION: 1[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 192.168.1.21, ACTION: 2[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 192.168.1.21, ACTION: 2[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 18.168.253.132, ACTION: 2[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 1.1.1.1, ACTION: 1[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 18.168.253.132, ACTION: 2[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 18.168.253.132, ACTION: 2[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 1.1.1.1, ACTION: 1[2022-10-04T12:46:05Z INFO xdp_drop] SRC: 140.82.121.6, ACTION: 2