Chapter 8: Combining Programs (A Simple Agent)

This final chapter demonstrates how to combine the basic concepts learned previously. While production tools are highly complex, we can learn a lot by putting together a simple, educational agent that runs two different eBPF programs from a single userspace application.

8.1 Multi-Program Structuring

Aya supports compiling and loading multiple eBPF programs in a single project. Here, we will combine the simple network filter from Chapter 3 with the process monitor from Chapter 5.

8.2 Exercise: Unified Agent Implementation

Step 1: Base Project Configuration

Initialize the workspace and provision the necessary dependencies:

cargo generate -n mini_cilium -d program_type=xdp https://github.com/aya-rs/aya-template
cd mini_cilium/mini_cilium-ebpf
cargo add network-types

Step 2: Multi-Program eBPF Integration

Modify mini_cilium-ebpf/src/main.rs to encapsulate both the #[xdp] macro and the #[tracepoint] macro within the same module.

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

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

#[xdp]
pub fn firewall(ctx: XdpContext) -> u32 {
    match try_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_firewall(ctx: XdpContext) -> Result<u32, ()> {
    let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? };
    if unsafe { (*ethhdr).ether_type() } != Ok(EtherType::Ipv4) {
        return Ok(xdp_action::XDP_PASS);
    }

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

    let proto = unsafe { (*ipv4hdr).proto() }
        .map_err(|IpError::InvalidProto(_proto)| ())?;

    let action = match proto {
        IpProto::Icmp => {
            info!(&ctx, "FIREWALL: Dropped ICMP packet.");
            return Ok(xdp_action::XDP_DROP);
        }
        _ => return Ok(xdp_action::XDP_PASS),
    };
    Ok(action)
}

#[tracepoint]
pub fn exec_monitor(ctx: TracePointContext) -> u32 {
    let pid = (bpf_get_current_pid_tgid() >> 32) as u32;
    info!(&ctx, "MONITOR: Process {} executed a command.", pid);
    0
}

#[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";
}

Step 3: Userspace Orchestration

Modify mini_cilium/src/main.rs to sequentially instantiate and attach both eBPF programs via the aya API framework.

#![allow(unused)]
fn main() {
    let mut ebpf = aya::Ebpf::load(aya::include_bytes_aligned!(concat!(
        env!("OUT_DIR"),
        "/mini_cilium"
    )))?;
    match aya_log::EbpfLogger::init(&mut ebpf) {
        Err(e) => {
            log::warn!("failed to initialize eBPF logger: {e}");
        }
        Ok(logger) => {
            let mut logger =
                tokio::io::unix::AsyncFd::with_interest(logger, tokio::io::Interest::READABLE)?;
            tokio::task::spawn(async move {
                loop {
                    let mut guard = logger.readable_mut().await.unwrap();
                    guard.get_inner_mut().flush();
                    guard.clear_ready();
                }
            });
        }
    }

    // Attach XDP Program
    let program: &mut aya::programs::Xdp = ebpf.program_mut("firewall").unwrap().try_into()?;
    program.load()?;
    program.attach(&opt.iface, aya::programs::XdpMode::default())
        .context("failed to attach the XDP program")?;
    log::info!("Attached XDP Firewall to interface: {}", opt.iface);

    // Attach Tracepoint Program
    let program: &mut aya::programs::TracePoint = ebpf.program_mut("exec_monitor").unwrap().try_into()?;
    program.load()?;
    program.attach("sched", "sched_process_exec")?;
    log::info!("Attached Tracepoint Exec Monitor");

    println!("Unified agent is operational. Waiting for SIGINT...");
    signal::ctrl_c().await?;
    println!("Exiting...");
}

Compile and execute the program to observe simultaneous enforcement of networking restrictions and telemetry collection.