Chapter 3: Basic Networking with XDP

3.1 The eXpress Data Path (XDP)

XDP is a specialized eBPF program type designed for high-performance packet processing. It attaches directly to the receive (RX) path of a network interface card (NIC).

In standard networking, when a packet arrives, the NIC driver allocates an sk_buff structure, copies the packet data, and forwards it to the Linux TCP/IP stack. XDP bypasses this overhead by executing immediately upon packet reception, before memory allocation occurs. At this stage, the packet is processed as a raw byte array.

XDP Return Codes

Upon inspecting a packet, an XDP program dictates its fate by returning one of several action codes:

  • XDP_PASS: Permits the packet to proceed to the standard Linux network stack.
  • XDP_DROP: Discards the packet immediately (efficient for DDoS mitigation).
  • XDP_TX: Transmits the packet back out through the receiving interface.
  • XDP_REDIRECT: Forwards the packet to a different network interface or CPU.
  • XDP_ABORTED: Indicates an error during processing; the packet is dropped and a tracepoint is triggered.

3.2 Exercise: Project Generation

Generate a dedicated workspace for XDP development:

cargo generate -n xdp_firewall -d program_type=xdp https://github.com/aya-rs/aya-template

3.3 Exercise: Implementing an ICMP Filter

This exercise demonstrates how to manually parse network headers to identify and drop ICMP (Ping) packets.

Step 1: Network Parsing Dependencies

The network-types crate provides safe, zero-copy Rust structures for mapping network headers over raw byte arrays.

cd xdp_firewall/xdp_firewall-ebpf
cargo add network-types

Step 2: The eBPF Parser Implementation

Replace the contents of xdp_firewall-ebpf/src/main.rs with the following implementation. The ptr_at function enforces strict bounds checking, a requirement for passing the eBPF Verifier.

#![allow(unused)]
#![no_std]
#![no_main]

fn main() {
use aya_ebpf::{
    bindings::xdp_action, 
    macros::xdp, 
    programs::XdpContext
};
use aya_log_ebpf::info;
use network_types::{
    eth::{EthHdr, EtherType},
    ip::{Ipv4Hdr, IpProto},
};

#[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 = core::mem::size_of::<T>();

    if start + offset + len > end {
        return Err(());
    }

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

fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
    let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? };

    // Validate IPv4 traffic using safe getter methods
    match unsafe { (*ethhdr).ether_type() } {
        Ok(EtherType::Ipv4) => {}
        _ => return Ok(xdp_action::XDP_PASS),
    }

    let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? };
    let source_addr = u32::from_be_bytes(unsafe { (*ipv4hdr).src_addr });

    // Validate and handle ICMP protocol
    let proto = unsafe { (*ipv4hdr).proto() }.unwrap_or(IpProto::Unknown);

    if proto == IpProto::Icmp {
        info!(&ctx, "BLOCKING Ping (ICMP) from source IP: {:i}", source_addr);
        return Ok(xdp_action::XDP_DROP);
    }

    Ok(xdp_action::XDP_PASS)
}

#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

#[unsafe(link_section = "license")]
#[unsafe(no_mangle)]
static LICENSE: [u8; 13] = *b"Dual MIT/GPL\0";
}

3.4 Exercise: Execution and Testing

Execute the userspace loader. To test ICMP filtering locally, target the loopback interface (lo):

RUST_LOG=info cargo xtask run -- --iface lo

Verify the mitigation by transmitting ICMP echo requests in an adjacent terminal:

ping 127.0.0.1

The packets will be dropped silently at the interface level, triggering the log statement.