Dropping Packets
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
.
Here's what the code looks like now:
xdp-drop-ebpf/src/main.rs |
---|
| #![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,
};
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[map] // (1)
static BLOCKLIST: HashMap<u32, u32> =
HashMap::<u32, u32>::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<T>(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> {
let start = ctx.data();
let end = ctx.data_end();
let len = mem::size_of::<T>();
if start + offset + len > end {
return Err(());
}
let ptr = (start + offset) as *const T;
Ok(&*ptr)
}
// (2)
fn block_ip(address: u32) -> bool {
unsafe { BLOCKLIST.get(&address).is_some() }
}
fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
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 });
// (3)
let action = if block_ip(source) {
xdp_action::XDP_DROP
} else {
xdp_action::XDP_PASS
};
info!(&ctx, "SRC: {:i}, ACTION: {}", source, action);
Ok(action)
}
|
- Create our map
- Check if we should allow or deny our packet
- Return the correct action
Populating our map from userspace
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.
Here's how the userspace code looks:
xdp-drop/src/main.rs |
---|
| use anyhow::Context;
use aya::{
maps::HashMap,
programs::{Xdp, XdpFlags},
};
use aya_log::EbpfLogger;
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();
// 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 `Ebpf::load_file` instead.
let mut bpf = aya::Ebpf::load(aya::include_bytes_aligned!(concat!(
env!("OUT_DIR"),
"/xdp-drop"
)))?;
if let Err(e) = EbpfLogger::init(&mut bpf) {
// This can happen if you remove all log statements from your eBPF program.
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")?;
// (1)
let mut blocklist: HashMap<_, u32, u32> =
HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?;
// (2)
let block_addr: u32 = Ipv4Addr::new(1, 1, 1, 1).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 run --config 'target."cfg(all())".runner="sudo -E"'
[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