Skip to content

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:

myapp-ebpf/src/main.rs
#![no_std]
#![no_main]
#![allow(nonstandard_style, dead_code)]

use aya_bpf::{
    bindings::xdp_action,
    macros::{map, xdp},
    maps::{HashMap, PerfEventArray},
    programs::XdpContext,
};

use core::mem;
use memoffset::offset_of;
use myapp_common::PacketLog;

mod bindings;
use bindings::{ethhdr, iphdr};

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}

#[map(name = "EVENTS")]
static mut EVENTS: PerfEventArray<PacketLog> =
    PerfEventArray::<PacketLog>::with_max_entries(1024, 0);

#[map(name = "BLOCKLIST")] // (1)
static mut 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(());
    }

    Ok((start + offset) as *const T)
}

// (2)
fn block_ip(address: u32) -> bool {
    unsafe { BLOCKLIST.get(&address).is_some() }
}

fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
    let h_proto = u16::from_be(unsafe {
        *ptr_at(&ctx, offset_of!(ethhdr, h_proto))?
    });
    if h_proto != ETH_P_IP {
        return Ok(xdp_action::XDP_PASS);
    }
    let source = u32::from_be(unsafe {
        *ptr_at(&ctx, ETH_HDR_LEN + offset_of!(iphdr, saddr))?
    });

    // (3)
    let action = if block_ip(source) {
        xdp_action::XDP_DROP
    } else {
        xdp_action::XDP_PASS
    };

    let log_entry = PacketLog {
        ipv4_address: source,
        action: action,
    };
    unsafe {
        EVENTS.output(&ctx, &log_entry, 0);
    }
    Ok(action)
}

const ETH_P_IP: u16 = 0x0800;
const ETH_HDR_LEN: usize = mem::size_of::<ethhdr>();
  1. Create our map
  2. Check if we should allow or deny our packet
  3. 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:

myapp/src/main.rs
use aya::{include_bytes_aligned, Bpf};
use anyhow::Context;
use aya::programs::{Xdp, XdpFlags};
use aya::maps::{perf::AsyncPerfEventArray, HashMap};
use aya::util::online_cpus;
use bytes::BytesMut;
use std::net::{self, Ipv4Addr};
use clap::Parser;
use log::info;
use tokio::{signal, task};

use myapp_common::PacketLog;

#[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 `Bpf::load_file` instead.
    #[cfg(debug_assertions)]
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/debug/myapp"
    ))?;
    #[cfg(not(debug_assertions))]
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/release/myapp"
    ))?;
    let program: &mut Xdp = 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")?;

    // (1)
    let mut blocklist: HashMap<_, u32, u32> =
        HashMap::try_from(bpf.map_mut("BLOCKLIST")?)?;

    // (2)
    let block_addr: u32 = Ipv4Addr::new(1, 1, 1, 1).try_into()?;

    // (3)
    blocklist.insert(block_addr, 0, 0)?;

    let mut perf_array = AsyncPerfEventArray::try_from(bpf.map_mut("EVENTS")?)?;

    for cpu_id in online_cpus()? {
        let mut buf = perf_array.open(cpu_id, None)?;

        task::spawn(async move {
            let mut buffers = (0..10)
                .map(|_| BytesMut::with_capacity(1024))
                .collect::<Vec<_>>();

            loop {
                let events = buf.read_events(&mut buffers).await.unwrap();
                for i in 0..events.read {
                    let buf = &mut buffers[i];
                    let ptr = buf.as_ptr() as *const PacketLog;
                    let data = unsafe { ptr.read_unaligned() };
                    let src_addr = net::Ipv4Addr::from(data.ipv4_address);
                    info!("LOG: SRC {}, ACTION {}", src_addr, data.action);
                }
            }
        });
    }
    signal::ctrl_c().await.expect("failed to listen for event");
    Ok::<_, anyhow::Error>(())
}
  1. Get a reference to the map
  2. Create an IPv4Addr
  3. Write this to our map

Running the program

$ RUST_LOG=info cargo xtask run
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 1.1.1.1, ACTION 1
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 192.168.1.21, ACTION 2
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 192.168.1.21, ACTION 2
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 18.168.253.132, ACTION 2
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 1.1.1.1, ACTION 1
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 18.168.253.132, ACTION 2
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 18.168.253.132, ACTION 2
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 1.1.1.1, ACTION 1
[2022-10-04T12:46:05Z INFO  myapp] LOG: SRC 140.82.121.6, ACTION 2