A Beginner's Guide to eBPF in Rust
This tutorial is a step-by-step introduction to learning eBPF using the Rust programming language. It is designed specifically for beginners who want to explore how eBPF works under the hood without getting overwhelmed.
By following this guide, you will learn the basics of eBPF and eventually combine different concepts to build a simple, educational eBPF agent.
All the code and examples are available on GitHub. If you find this guide helpful, feel free to drop a star! â github.com/amghazanfari/ebpf-beginner
We will use the Aya framework, which allows us to write both eBPF programs and the userspace applications entirely in Rust.
đ Read it online
The full book is hosted on GitHub Pages: https://amghazanfari.github.io/ebpf-beginner/
Table of Contents
- Chapter 1: Getting Started with eBPF and Aya
- Chapter 2: Kprobes and Tracing
- Chapter 3: Basic Networking with XDP
- Chapter 4: Traffic Control (TC)
- Chapter 5: Tracepoints and Uprobes
- Chapter 6: eBPF Maps and State
- Chapter 7: Basic Security and Mitigation
- Chapter 8: Combining Programs (A Simple Agent)
Chapter 1: Getting Started with eBPF and Aya
1.1 What is eBPF?
eBPF (Extended Berkeley Packet Filter) is a technology that allows sandboxed programs to execute within the operating system kernel. It is utilized to safely and efficiently extend the capabilities of the kernel without requiring modifications to kernel source code or the loading of kernel modules.
eBPF programs are event-driven and execute when the kernel or an application passes a specific hook point. Examples of hook points include system calls, function entry and exit points, kernel tracepoints, and network events.
1.2 The Role of Rust and Aya
Traditionally, eBPF programs are authored in C and compiled using the Clang/LLVM toolchain. The userspace programs responsible for loading and interacting with them are typically written in C, Go, or Python.
Aya is an eBPF library engineered with a focus on developer experience and operability. It facilitates the development of both eBPF programs and userspace applications entirely in Rust. This approach provides memory safety, access to the Cargo package manager, and a unified development ecosystem.
1.3 Environment Setup
To develop eBPF programs in Rust, the environment must be configured with the Rust toolchain, a linker (bpf-linker), and the Aya project generator (cargo-generate).
Exercise 1.1: Dependency Installation
Execute the following commands to install the required dependencies:
# 1. Install the nightly Rust toolchain (required for compiling eBPF)
rustup toolchain install nightly --component rust-src
# 2. Install bpf-linker
cargo install bpf-linker
# 3. Install cargo-generate
cargo install cargo-generate
1.4 Project Initialization
Once the environment is prepared, the project structure can be generated using Aya templates.
Exercise 1.2: Generating the Aya Project
Execute the following command within the workspace directory:
cargo generate -n my_ebpf_agent -d program_type=kprobe https://github.com/aya-rs/aya-template
This command generates a new directory named my_ebpf_agent containing two sub-projects:
my_ebpf_agent: The userspace program responsible for loading the eBPF code into the kernel.my_ebpf_agent-ebpf: The eBPF code intended to run within the kernel space.
Review the generated file structure to understand the separation between userspace and kernel space components.
Chapter 2: Kprobes and Tracing
2.1 The Kprobe Hook Point
A Kprobe (Kernel Probe) is a mechanism that allows developers to dynamically intercept execution at almost any instruction within the Linux kernel. When the kernel reaches the specified function, the attached eBPF program executes, providing access to inspect function arguments, return values, or simply record the invocation event.
Two primary variants exist:
kprobe: Executes before the target kernel function executes.kretprobe: Executes after the target kernel function executes, facilitating inspection of its return value.
2.2 Analyzing the eBPF Architecture
An eBPF project is divided into Kernel Space and User Space components.
2.2.1 The Kernel Space Program
The code located in my_ebpf_agent-ebpf/src/main.rs represents the logic executed within the kernel.
The #[kprobe] macro instructs the compiler to format the function as a kprobe eBPF program. A typical logging implementation utilizes the aya_log_ebpf crate:
#![allow(unused)] fn main() { info!(&ctx, "function try_to_wake_up called"); }
This macro leverages a ring buffer to efficiently stream log data from the kernel to the userspace application.
2.2.2 The User Space Program
The application in my_ebpf_agent/src/main.rs manages the lifecycle of the eBPF program:
- Loading: Reads and loads the compiled eBPF bytecode via
bpf.load(). - Attaching: Binds the loaded program to the specific kernel function via
attach(). - Listening: Initializes the asynchronous logger to receive events from the kernel via
BpfLogger::init(...).
2.3 Exercise 2.1: Extracting System State
eBPF's primary utility lies in system state inspection. This exercise modifies the eBPF program to extract the Process ID (PID) of the application responsible for triggering the kernel function.
eBPF programs interact with the kernel through restricted "Helper Functions." To retrieve the PID, the bpf_get_current_pid_tgid() helper is used.
Step 1: Modify my_ebpf_agent-ebpf/src/main.rs to import the helper function:
#![allow(unused)] fn main() { use aya_ebpf::helpers::bpf_get_current_pid_tgid; }
Step 2: Extract the PID within the probe function. The helper returns a 64-bit integer where the upper 32 bits contain the Thread Group ID (which corresponds to the PID in userspace semantics).
#![allow(unused)] fn main() { fn try_my_ebpf_agent(ctx: ProbeContext) -> Result<u32, u32> { let pid_tgid = bpf_get_current_pid_tgid(); let pid = (pid_tgid >> 32) as u32; info!(&ctx, "try_to_wake_up called by PID: {}", pid); Ok(0) } }
2.4 Exercise 2.2: Compilation and Execution
After saving modifications, the userspace program must be recompiled and executed with elevated privileges to attach the probe.
cd my_ebpf_agent
RUST_LOG=info cargo xtask run
The output stream will display the active Process IDs invoking the traced kernel function.
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.
Chapter 4: Traffic Control (TC) and Network Manipulation
4.1 XDP vs Traffic Control (TC)
While XDP provides optimal performance for ingress packet filtering, Traffic Control (TC) offers a broader scope for network manipulation. The primary distinctions are:
- Directionality: XDP executes exclusively on Ingress. TC supports attachment to both Ingress and Egress paths.
- Data Structures: XDP operates on raw, unparsed byte arrays. TC executes after the kernel has parsed the packet and allocated an
sk_buff(Socket Buffer) structure, granting the eBPF program access to richer networking metadata.
TC Return Codes
Similar to XDP, a TC eBPF program determines packet flow via specific return codes:
TC_ACT_PIPE: Passes the packet to the subsequent TC program or the default network stack.TC_ACT_SHOT: Drops the packet immediately.TC_ACT_OK: Terminates the TC pipeline and permits the packet to proceed.TC_ACT_REDIRECT: Redirects the packet to an alternate network interface.TC_ACT_STOLEN: Indicates the eBPF program has consumed the packet, effectively dropping it from the kernel's perspective.
4.2 Exercise: Project Generation
Generate a TC workspace using the classifier template:
cargo generate -n tc_interceptor -d program_type=classifier https://github.com/aya-rs/aya-template
4.3 Exercise: Egress Traffic Interception
This exercise implements a TC program that monitors outbound traffic and logs connections targeting standard web ports (80 and 443).
Step 1: Dependencies
Include the network-types crate in the eBPF package:
cd tc_interceptor/tc_interceptor-ebpf
cargo add network-types
Step 2: The Classifier Implementation
Replace tc_interceptor-ebpf/src/main.rs with the following logic:
#![allow(unused)] #![no_std] #![no_main] fn main() { use aya_ebpf::{ macros::classifier, programs::TcContext, bindings::{TC_ACT_PIPE, TC_ACT_SHOT}, }; use aya_log_ebpf::info; use network_types::{ eth::{EthHdr, EtherType}, ip::{Ipv4Hdr, IpProto}, tcp::TcpHdr, }; #[classifier] pub fn tc_interceptor(ctx: TcContext) -> i32 { match try_tc_interceptor(ctx) { Ok(ret) => ret, Err(_) => TC_ACT_SHOT, } } #[inline(always)] unsafe fn ptr_at<T>(ctx: &TcContext, 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_tc_interceptor(ctx: TcContext) -> Result<i32, ()> { let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? }; match unsafe { (*ethhdr).ether_type() } { Ok(EtherType::Ipv4) => {} _ => return Ok(TC_ACT_PIPE), } let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? }; let dest_addr = u32::from_be_bytes(unsafe { (*ipv4hdr).dst_addr }); let proto = unsafe { (*ipv4hdr).proto() }; if !matches!(proto, Ok(IpProto::Tcp)) { return Ok(TC_ACT_PIPE); } let tcphdr: *const TcpHdr = unsafe { ptr_at(&ctx, EthHdr::LEN + 20)? }; let dest_port = u16::from_be_bytes(unsafe { (*tcphdr).dest }); if dest_port == 80 || dest_port == 443 { info!(&ctx, "OUTBOUND web traffic to IP: {:i} on Port: {}", dest_addr, dest_port); } Ok(TC_ACT_PIPE) } #[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"; }
4.4 Exercise: Egress Configuration
By default, the userspace loader may attach the program to the Ingress path. Modify tc_interceptor/src/main.rs to enforce Egress attachment:
#![allow(unused)] fn main() { tc::qdisc_add_clsact(&iface)?; let program: &mut dyn Program = bpf.program_mut("tc_interceptor").unwrap(); let tc_program: &mut aya::programs::tc::SchedClassifier = program.try_into()?; tc_program.load()?; // Attach to Egress tc_program.attach(&iface, aya::programs::tc::TcAttachType::Egress)?; }
Execute the agent and trigger an outbound connection via curl -I https://google.com to observe the trace logs.
Chapter 5: Advanced Observability with Tracepoints and Uprobes
While Kprobes offer significant utility for kernel tracing, their reliance on dynamic function names makes them susceptible to breakage across different Linux kernel versions. For production-grade observability and security agents, stability is paramount.
5.1 Tracepoints
Tracepoints are static instrumentation markers embedded directly within the kernel source code by Linux developers. They are explicitly maintained as a stable API across kernel releases. For standard system events (e.g., process execution, file I/O, network connections), Tracepoints are the preferred mechanism over Kprobes.
5.2 Uprobes
Uprobes (User-Space Probes) extend eBPF's tracing capabilities to userspace applications. They allow eBPF programs to attach to specific functions within running binaries or shared libraries. Common use cases include tracing bash inputs or intercepting unencrypted data structures prior to TLS encryption via libssl.so.
5.3 Exercise: Tracepoint Implementation
This exercise implements a system monitor utilizing the sched_process_exec tracepoint to log process executions.
Step 1: Project Generation
Generate a tracepoint project:
cargo generate -n exec_monitor -d program_type=tracepoint https://github.com/aya-rs/aya-template
Configure the attachment point as: sched:sched_process_exec.
Step 2: Implementation
Replace the contents of exec_monitor-ebpf/src/main.rs with the following. The implementation leverages bpf_get_current_pid_tgid to identify the invoking process.
#![allow(unused)] #![no_std] #![no_main] fn main() { use aya_ebpf::{ macros::tracepoint, programs::TracePointContext, helpers::bpf_get_current_pid_tgid, }; use aya_log_ebpf::info; #[tracepoint] pub fn exec_monitor(ctx: TracePointContext) -> u32 { match try_exec_monitor(ctx) { Ok(ret) => ret, Err(ret) => ret, } } fn try_exec_monitor(ctx: TracePointContext) -> Result<u32, u32> { let pid = (bpf_get_current_pid_tgid() >> 32) as u32; info!(&ctx, "SECURITY ALERT: Process {} executed a new program!", pid); Ok(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: Execution
Run the userspace application:
RUST_LOG=info cargo xtask run
Execute commands in a secondary terminal to observe the tracepoint triggering.
Chapter 6: eBPF Maps and State Management
Standard eBPF programs operate ephemerally; data is inspected, potentially logged, and immediately discarded. Constructing advanced systemsâsuch as rate-limiting firewalls or behavioral anomaly detectorsârequires state persistence across multiple execution cycles.
6.1 Understanding eBPF Maps
eBPF Maps provide highly efficient data structures (e.g., HashMaps, Arrays, Ring Buffers) allocated directly within kernel memory. They serve two critical functions:
- State Persistence: Permitting an eBPF program to retain and modify data across numerous invocations.
- Kernel-User Communication: Providing an API for userspace applications to read, modify, and delete entries within the kernel-allocated maps concurrently.
6.2 Exercise: Aggregating Execution Metrics
This exercise refactors the exec_monitor to aggregate execution counts per Process ID utilizing an eBPF HashMap.
Step 1: Map Declaration
Modify exec_monitor-ebpf/src/main.rs to include a globally accessible HashMap structure supporting a maximum of 1024 unique entries:
#![allow(unused)] fn main() { use aya_ebpf::macros::map; use aya_ebpf::maps::HashMap; #[map] static EXEC_COUNTS: HashMap<u32, u32> = HashMap::with_max_entries(1024, 0); }
Step 2: Map Interaction
Implement the logic to retrieve, increment, and store the execution counter within the tracepoint context:
#![allow(unused)] fn main() { fn try_exec_monitor(ctx: TracePointContext) -> Result<u32, u32> { let pid = (bpf_get_current_pid_tgid() >> 32) as u32; let current_count = unsafe { EXEC_COUNTS.get(&pid).copied().unwrap_or(0) }; let new_count = current_count + 1; unsafe { EXEC_COUNTS.insert(&pid, &new_count, 0).map_err(|_| 1u32)?; } info!(&ctx, "PID: {} has now executed {} programs", pid, new_count); Ok(0) } }
Step 3: Userspace Retrieval
Modify exec_monitor/src/main.rs to establish an asynchronous polling routine that extracts the map contents directly from the kernel interface:
#![allow(unused)] fn main() { let mut exec_counts: aya::maps::HashMap<_, u32, u32> = aya::maps::HashMap::try_from(bpf.map_mut("EXEC_COUNTS").unwrap())?; tokio::spawn(async move { loop { tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; println!("--- Current Execution Counts ---"); for item in exec_counts.iter() { if let Ok((pid, count)) = item { println!("PID {}: {} execs", pid, count); } } } }); }
Compile and execute the program to observe bidirectional data transfer.
Chapter 7: Basic Security and Mitigation
Modern Cloud Native Security solutions extend beyond passive observability into proactive system enforcement. eBPF provides the infrastructure necessary to intercept and terminate unauthorized kernel operations dynamically.
7.1 Active Mitigation Mechanisms
eBPF enforces system state via two primary vectors:
- BPF-LSM (Linux Security Modules): Allows eBPF to interface directly with the LSM framework, returning denial codes to block systemic actions. This methodology requires the
CONFIG_BPF_LSMkernel configuration. - Signal Injection: Utilizes standard tracepoints or kprobes coupled with signal injection APIs to immediately terminate an offending process. This technique maintains broader compatibility across standard Linux kernels.
7.2 Exercise: Process Mitigation via Signal Injection
This exercise constructs an agent that hooks the file access routine (do_sys_openat2) and transmits a SIGKILL to any process matching a restricted name pattern (e.g., wget).
Step 1: Project Generation
Generate a kprobe project targeted at file access operations:
cargo generate -n security_agent -d program_type=kprobe https://github.com/aya-rs/aya-template
Target the do_sys_openat2 kernel function.
Step 2: Signal Injection Implementation
Modify security_agent-ebpf/src/main.rs to evaluate the active process nomenclature and transmit a termination signal if a match is determined.
#![allow(unused)] #![no_std] #![no_main] fn main() { use aya_ebpf::{ macros::kprobe, programs::ProbeContext, helpers::{bpf_get_current_comm, bpf_send_signal}, }; use aya_log_ebpf::info; #[kprobe] pub fn security_agent(ctx: ProbeContext) -> u32 { match try_security_agent(ctx) { Ok(ret) => ret, Err(ret) => ret, } } fn try_security_agent(ctx: ProbeContext) -> Result<u32, u32> { let comm = bpf_get_current_comm().map_err(|_| 1u32)?; let malicious_comm = b"wget\0\0\0\0\0\0\0\0\0\0\0\0"; let mut is_malicious = true; for i in 0..4 { if comm[i] != malicious_comm[i] { is_malicious = false; break; } } if is_malicious { info!(&ctx, "SECURITY ALERT: Mitigating unauthorized execution of 'wget'"); unsafe { bpf_send_signal(9); } } Ok(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: Execution and Verification
Execute the binary:
RUST_LOG=info cargo xtask run
Invoke the restricted application in a secondary terminal (wget https://google.com). The process will be terminated prematurely by the kernel, yielding a Killed directive to standard output.
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.