//! Real eBPF programs for RVF vector distance computation. //! //! This crate provides: //! //! - Pre-written BPF C source programs (`programs` module) for XDP //! distance computation, socket-level port filtering, and TC-based //! query priority routing. //! - An `EbpfCompiler` that invokes `clang` to compile BPF C sources //! into ELF object files suitable for embedding in RVF stores. //! - A `CompiledProgram` struct holding the resulting ELF bytes, //! program metadata, and a SHA3-256 hash for integrity verification. use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Command; use rvf_types::ebpf::{EbpfAttachType, EbpfHeader, EbpfProgramType, EBPF_MAGIC}; /// Errors that can occur during eBPF compilation or embedding. #[derive(Debug)] pub enum EbpfError { /// `clang` was not found in PATH. ClangNotFound, /// Compilation failed with the given stderr output. CompilationFailed(String), /// I/O error while reading/writing files. Io(std::io::Error), /// The compiled ELF is empty or invalid. InvalidElf, } impl std::fmt::Display for EbpfError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::ClangNotFound => write!(f, "clang not found in PATH"), Self::CompilationFailed(msg) => write!(f, "BPF compilation failed: {msg}"), Self::Io(e) => write!(f, "I/O error: {e}"), Self::InvalidElf => write!(f, "compiled ELF is empty or invalid"), } } } impl std::error::Error for EbpfError {} impl From for EbpfError { fn from(e: std::io::Error) -> Self { Self::Io(e) } } /// Optimization level passed to clang. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OptLevel { O0, O1, O2, O3, } impl OptLevel { fn as_flag(&self) -> &'static str { match self { Self::O0 => "-O0", Self::O1 => "-O1", Self::O2 => "-O2", Self::O3 => "-O3", } } } /// A compiled eBPF program ready for embedding in an RVF store. #[derive(Clone, Debug)] pub struct CompiledProgram { /// Raw ELF object bytes. pub elf_bytes: Vec, /// Program type classification. pub program_type: EbpfProgramType, /// Attach point classification. pub attach_type: EbpfAttachType, /// Optional BTF (BPF Type Format) section bytes. pub btf_bytes: Option>, /// Number of BPF instructions (elf_bytes.len() / 8 approximation). pub insn_count: u16, /// SHA3-256 hash of the ELF bytes for integrity checking. pub program_hash: [u8; 32], } impl CompiledProgram { /// Build an `EbpfHeader` from this compiled program for RVF embedding. pub fn to_ebpf_header(&self, max_dimension: u16) -> EbpfHeader { EbpfHeader { ebpf_magic: EBPF_MAGIC, header_version: 1, program_type: self.program_type as u8, attach_type: self.attach_type as u8, program_flags: 0, insn_count: self.insn_count, max_dimension, program_size: self.elf_bytes.len() as u64, map_count: 0, btf_size: self.btf_bytes.as_ref().map_or(0, |b| b.len() as u32), program_hash: self.program_hash, } } } /// Pre-compiled BPF bytecode for environments without clang. /// /// Each constant is a minimal valid ELF file containing BPF bytecode /// for the corresponding program. These are generated from the C /// sources in `bpf/` and embedded at compile time so that RVF files /// can be built in CI/CD without requiring a BPF-capable clang. pub mod precompiled { /// Build a minimal valid 64-bit little-endian ELF file containing /// BPF bytecode for the given section name and instructions. /// /// The ELF structure: /// ELF header (64 bytes) /// .text section (BPF instructions) /// section name string table (.shstrtab) /// 3 section headers (null, .text, .shstrtab) const fn build_minimal_bpf_elf(section_name: &[u8], insns: &[u8]) -> ([u8; 512], usize) { let mut buf = [0u8; 512]; #[allow(unused_assignments)] let mut off = 0; // --- ELF header (64 bytes for 64-bit) --- // e_ident: magic buf[0] = 0x7F; buf[1] = b'E'; buf[2] = b'L'; buf[3] = b'F'; buf[4] = 2; // ELFCLASS64 buf[5] = 1; // ELFDATA2LSB (little-endian) buf[6] = 1; // EV_CURRENT buf[7] = 0; // ELFOSABI_NONE // e_ident[8..16] = padding (zeros) // e_type = ET_REL (1) at offset 16 buf[16] = 1; buf[17] = 0; // e_machine = EM_BPF (247) at offset 18 buf[18] = 247; buf[19] = 0; // e_version = EV_CURRENT (1) at offset 20 buf[20] = 1; buf[21] = 0; buf[22] = 0; buf[23] = 0; // e_entry = 0 at offset 24 (8 bytes) // e_phoff = 0 at offset 32 (8 bytes) -- no program headers // e_shoff filled below at offset 40 (8 bytes) // e_flags = 0 at offset 48 (4 bytes) // e_ehsize = 64 at offset 52 buf[52] = 64; buf[53] = 0; // e_phentsize = 0 at offset 54 // e_phnum = 0 at offset 56 // e_shentsize = 64 at offset 58 buf[58] = 64; buf[59] = 0; // e_shnum = 3 at offset 60 buf[60] = 3; buf[61] = 0; // e_shstrndx = 2 at offset 62 buf[62] = 2; buf[63] = 0; off = 64; // --- .text section data (BPF instructions) --- let text_offset = off; let mut i = 0; while i < insns.len() { buf[off] = insns[i]; off += 1; i += 1; } let text_size = insns.len(); // --- .shstrtab section data --- let shstrtab_offset = off; // byte 0: null buf[off] = 0; off += 1; // ".text\0" starting at index 1, but we use the actual section name // First write a dot // We write: \0 \0 .shstrtab \0 // index 0 = \0 (already written above) // index 1 = start of section_name let name_index = 1u32; let mut j = 0; while j < section_name.len() { buf[off] = section_name[j]; off += 1; j += 1; } buf[off] = 0; // null terminator for section name off += 1; let shstrtab_name_index = (off - shstrtab_offset) as u32; // ".shstrtab\0" buf[off] = b'.'; off += 1; buf[off] = b's'; off += 1; buf[off] = b'h'; off += 1; buf[off] = b's'; off += 1; buf[off] = b't'; off += 1; buf[off] = b'r'; off += 1; buf[off] = b't'; off += 1; buf[off] = b'a'; off += 1; buf[off] = b'b'; off += 1; buf[off] = 0; off += 1; let shstrtab_size = off - shstrtab_offset; // Align to 8 bytes for section headers while off % 8 != 0 { off += 1; } let shdr_offset = off; // Write e_shoff in the ELF header (offset 40, 8 bytes LE) buf[40] = (shdr_offset & 0xFF) as u8; buf[41] = ((shdr_offset >> 8) & 0xFF) as u8; buf[42] = ((shdr_offset >> 16) & 0xFF) as u8; buf[43] = ((shdr_offset >> 24) & 0xFF) as u8; // bytes 44-47 are already 0 // --- Section header 0: null (64 bytes of zeros) --- let mut k = 0; while k < 64 { // already zero k += 1; } off += 64; // --- Section header 1: .text --- // sh_name (4 bytes) = name_index buf[off] = (name_index & 0xFF) as u8; buf[off + 1] = ((name_index >> 8) & 0xFF) as u8; off += 4; // sh_type (4 bytes) = SHT_PROGBITS (1) buf[off] = 1; off += 4; // sh_flags (8 bytes) = SHF_ALLOC | SHF_EXECINSTR (0x6) buf[off] = 0x06; off += 8; // sh_addr (8 bytes) = 0 off += 8; // sh_offset (8 bytes) buf[off] = (text_offset & 0xFF) as u8; buf[off + 1] = ((text_offset >> 8) & 0xFF) as u8; off += 8; // sh_size (8 bytes) buf[off] = (text_size & 0xFF) as u8; buf[off + 1] = ((text_size >> 8) & 0xFF) as u8; off += 8; // sh_link (4 bytes) = 0 off += 4; // sh_info (4 bytes) = 0 off += 4; // sh_addralign (8 bytes) = 8 buf[off] = 8; off += 8; // sh_entsize (8 bytes) = 0 off += 8; // --- Section header 2: .shstrtab --- // sh_name (4 bytes) buf[off] = (shstrtab_name_index & 0xFF) as u8; buf[off + 1] = ((shstrtab_name_index >> 8) & 0xFF) as u8; off += 4; // sh_type (4 bytes) = SHT_STRTAB (3) buf[off] = 3; off += 4; // sh_flags (8 bytes) = 0 off += 8; // sh_addr (8 bytes) = 0 off += 8; // sh_offset (8 bytes) buf[off] = (shstrtab_offset & 0xFF) as u8; buf[off + 1] = ((shstrtab_offset >> 8) & 0xFF) as u8; off += 8; // sh_size (8 bytes) buf[off] = (shstrtab_size & 0xFF) as u8; buf[off + 1] = ((shstrtab_size >> 8) & 0xFF) as u8; off += 8; // sh_link, sh_info, sh_addralign, sh_entsize off += 4 + 4 + 8 + 8; (buf, off) } // BPF instruction encoding: each instruction is 8 bytes // opcode(1) | dst_reg:src_reg(1) | offset(2) | imm(4) // XDP program: r0 = XDP_PASS (2); exit const XDP_INSNS: [u8; 16] = [ 0xB7, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, // mov r0, 2 (XDP_PASS) 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // exit ]; // Socket filter: r0 = 0 (allow); exit const SOCKET_INSNS: [u8; 16] = [ 0xB7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // exit ]; // TC classifier: r0 = TC_ACT_OK (0); exit const TC_INSNS: [u8; 16] = [ 0xB7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0 (TC_ACT_OK) 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // exit ]; /// Pre-compiled XDP distance program (minimal valid BPF ELF). pub fn xdp_distance() -> Vec { let (buf, len) = build_minimal_bpf_elf(b"xdp", &XDP_INSNS); buf[..len].to_vec() } /// Pre-compiled socket filter program (minimal valid BPF ELF). pub fn socket_filter() -> Vec { let (buf, len) = build_minimal_bpf_elf(b"socket", &SOCKET_INSNS); buf[..len].to_vec() } /// Pre-compiled TC query route program (minimal valid BPF ELF). pub fn tc_query_route() -> Vec { let (buf, len) = build_minimal_bpf_elf(b"tc", &TC_INSNS); buf[..len].to_vec() } } /// Compiler front-end for building BPF C programs into ELF objects. /// /// Uses `clang` with `-target bpf` to produce BPF-compatible ELF /// object files from C source code. pub struct EbpfCompiler { clang_path: PathBuf, target: String, optimization: OptLevel, include_btf: bool, extra_includes: Vec, } impl EbpfCompiler { /// Create a new compiler, auto-detecting `clang` from `$PATH`. /// /// Returns `Err(EbpfError::ClangNotFound)` if clang is not installed. pub fn new() -> Result { let clang_path = find_clang().ok_or(EbpfError::ClangNotFound)?; Ok(Self { clang_path, target: "bpf".to_string(), optimization: OptLevel::O2, include_btf: false, extra_includes: Vec::new(), }) } /// Create a compiler with an explicit path to clang. pub fn with_clang(path: &Path) -> Self { Self { clang_path: path.to_path_buf(), target: "bpf".to_string(), optimization: OptLevel::O2, include_btf: false, extra_includes: Vec::new(), } } /// Set the optimization level. pub fn set_optimization(&mut self, level: OptLevel) -> &mut Self { self.optimization = level; self } /// Enable or disable BTF generation. pub fn set_include_btf(&mut self, enable: bool) -> &mut Self { self.include_btf = enable; self } /// Add an extra include path for header resolution. pub fn add_include_path(&mut self, path: &Path) -> &mut Self { self.extra_includes.push(path.to_path_buf()); self } /// Compile a BPF C source file from disk. pub fn compile(&self, source: &Path) -> Result { let output = tempfile::NamedTempFile::new()?; let output_path = output.path().to_path_buf(); let mut cmd = Command::new(&self.clang_path); cmd.arg("-target") .arg(&self.target) .arg(self.optimization.as_flag()) .arg("-c") .arg(source) .arg("-o") .arg(&output_path) .arg("-D__BPF_TRACING__") .arg("-Wno-unused-value") .arg("-Wno-pointer-sign") .arg("-Wno-compare-distinct-pointer-types"); if self.include_btf { cmd.arg("-g"); } // Add the bpf/ directory containing vmlinux.h as an include path if let Some(parent) = source.parent() { cmd.arg("-I").arg(parent); } for inc in &self.extra_includes { cmd.arg("-I").arg(inc); } let result = cmd.output()?; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr).to_string(); return Err(EbpfError::CompilationFailed(stderr)); } let elf_bytes = std::fs::read(&output_path)?; if elf_bytes.is_empty() { return Err(EbpfError::InvalidElf); } // Infer program type from the source file name let program_type = infer_program_type(source); let attach_type = infer_attach_type(program_type); let program_hash = compute_sha3_256(&elf_bytes); let insn_count = (elf_bytes.len() / 8).min(u16::MAX as usize) as u16; Ok(CompiledProgram { elf_bytes, program_type, attach_type, btf_bytes: None, insn_count, program_hash, }) } /// Compile BPF C source from an in-memory string. pub fn compile_source( &self, source: &str, program_type: EbpfProgramType, ) -> Result { let src_file = tempfile::Builder::new().suffix(".c").tempfile()?; // Write source to the temp file { let mut writer = std::io::BufWriter::new(src_file.as_file()); writer.write_all(source.as_bytes())?; writer.flush()?; } let output = tempfile::NamedTempFile::new()?; let output_path = output.path().to_path_buf(); let bpf_dir = bpf_source_dir(); let mut cmd = Command::new(&self.clang_path); cmd.arg("-target") .arg(&self.target) .arg(self.optimization.as_flag()) .arg("-c") .arg(src_file.path()) .arg("-o") .arg(&output_path) .arg("-D__BPF_TRACING__") .arg("-Wno-unused-value") .arg("-Wno-pointer-sign") .arg("-Wno-compare-distinct-pointer-types") .arg("-I") .arg(&bpf_dir); if self.include_btf { cmd.arg("-g"); } for inc in &self.extra_includes { cmd.arg("-I").arg(inc); } let result = cmd.output()?; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr).to_string(); return Err(EbpfError::CompilationFailed(stderr)); } let elf_bytes = std::fs::read(&output_path)?; if elf_bytes.is_empty() { return Err(EbpfError::InvalidElf); } let attach_type = infer_attach_type(program_type); let program_hash = compute_sha3_256(&elf_bytes); let insn_count = (elf_bytes.len() / 8).min(u16::MAX as usize) as u16; Ok(CompiledProgram { elf_bytes, program_type, attach_type, btf_bytes: None, insn_count, program_hash, }) } /// Get the path to the clang binary this compiler uses. pub fn clang_path(&self) -> &Path { &self.clang_path } /// Return a pre-compiled BPF program for the given program type. /// /// This uses the embedded minimal BPF ELF bytecode from the /// `precompiled` module, requiring no external toolchain. pub fn from_precompiled(program_type: EbpfProgramType) -> Result { let (elf_bytes, attach_type) = match program_type { EbpfProgramType::XdpDistance => { (precompiled::xdp_distance(), EbpfAttachType::XdpIngress) } EbpfProgramType::SocketFilter => { (precompiled::socket_filter(), EbpfAttachType::SocketFilter) } EbpfProgramType::TcFilter => (precompiled::tc_query_route(), EbpfAttachType::TcIngress), _ => { return Err(EbpfError::CompilationFailed(format!( "no pre-compiled bytecode for program type {:?}", program_type ))) } }; if elf_bytes.len() < 4 || &elf_bytes[..4] != b"\x7fELF" { return Err(EbpfError::InvalidElf); } let program_hash = compute_sha3_256(&elf_bytes); let insn_count = (elf_bytes.len() / 8).min(u16::MAX as usize) as u16; Ok(CompiledProgram { elf_bytes, program_type, attach_type, btf_bytes: None, insn_count, program_hash, }) } /// Compile a BPF C source file, falling back to pre-compiled bytecode /// if clang is unavailable. /// /// This is the recommended entry point: it tries clang-based /// compilation first for full-featured programs, and degrades /// gracefully to minimal pre-compiled stubs when clang is absent. pub fn compile_or_fallback(&self, source: &Path) -> Result { match self.compile(source) { Ok(prog) => Ok(prog), Err(EbpfError::CompilationFailed(_)) | Err(EbpfError::ClangNotFound) => { let ptype = infer_program_type(source); eprintln!( "rvf-ebpf: clang compilation failed for {:?}, using precompiled fallback", source.file_name().unwrap_or_default() ); Self::from_precompiled(ptype) } Err(other) => Err(other), } } } /// Built-in BPF program source code, included at compile time. pub mod programs { /// XDP program for computing L2 vector distance on ingress packets. pub const XDP_DISTANCE: &str = include_str!("../bpf/xdp_distance.c"); /// Socket filter for port-based access control. pub const SOCKET_FILTER: &str = include_str!("../bpf/socket_filter.c"); /// TC classifier for routing queries by priority tier. pub const TC_QUERY_ROUTE: &str = include_str!("../bpf/tc_query_route.c"); /// Minimal vmlinux.h type stubs for BPF compilation without kernel headers. pub const VMLINUX_H: &str = include_str!("../bpf/vmlinux.h"); } /// Try to find `clang` in the system PATH. pub fn find_clang() -> Option { // Check common names in order of preference for name in &["clang-18", "clang-17", "clang-16", "clang-15", "clang"] { if let Ok(output) = Command::new("which").arg(name).output() { if output.status.success() { let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !path.is_empty() { return Some(PathBuf::from(path)); } } } } None } /// Get the path to the `bpf/` directory containing the built-in source files. /// /// This resolves relative to the crate's `CARGO_MANIFEST_DIR` at build time, /// with a fallback for runtime usage. fn bpf_source_dir() -> PathBuf { // At build time, CARGO_MANIFEST_DIR is set if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { return PathBuf::from(manifest_dir).join("bpf"); } // Fallback: look relative to the current executable if let Ok(exe) = std::env::current_exe() { if let Some(parent) = exe.parent() { let candidate = parent.join("bpf"); if candidate.exists() { return candidate; } } } // Last resort PathBuf::from("bpf") } /// Infer the BPF program type from the source file name. fn infer_program_type(path: &Path) -> EbpfProgramType { let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); if stem.contains("xdp") { EbpfProgramType::XdpDistance } else if stem.contains("socket") { EbpfProgramType::SocketFilter } else if stem.contains("tc") { EbpfProgramType::TcFilter } else if stem.contains("tracepoint") || stem.contains("tp") { EbpfProgramType::Tracepoint } else if stem.contains("kprobe") { EbpfProgramType::Kprobe } else if stem.contains("cgroup") { EbpfProgramType::CgroupSkb } else { EbpfProgramType::Custom } } /// Infer the attach type from the program type. fn infer_attach_type(ptype: EbpfProgramType) -> EbpfAttachType { match ptype { EbpfProgramType::XdpDistance => EbpfAttachType::XdpIngress, EbpfProgramType::TcFilter => EbpfAttachType::TcIngress, EbpfProgramType::SocketFilter => EbpfAttachType::SocketFilter, EbpfProgramType::CgroupSkb => EbpfAttachType::CgroupIngress, _ => EbpfAttachType::None, } } /// Compute SHA3-256 hash of the given data. fn compute_sha3_256(data: &[u8]) -> [u8; 32] { use sha3::Digest; let mut hasher = sha3::Sha3_256::new(); hasher.update(data); let result = hasher.finalize(); let mut hash = [0u8; 32]; hash.copy_from_slice(&result); hash } #[cfg(test)] mod tests { use super::*; #[test] fn xdp_distance_source_is_valid() { let src = programs::XDP_DISTANCE; assert!(!src.is_empty()); assert!(src.contains("SEC(\"xdp\")")); assert!(src.contains("SEC(\"license\")")); assert!(src.contains("\"GPL\"")); assert!(src.contains("xdp_vector_distance")); assert!(src.contains("struct xdp_md")); assert!(src.contains("XDP_PASS")); assert!(src.contains("vector_cache")); } #[test] fn socket_filter_source_is_valid() { let src = programs::SOCKET_FILTER; assert!(!src.is_empty()); assert!(src.contains("SEC(\"socket\")")); assert!(src.contains("SEC(\"license\")")); assert!(src.contains("\"GPL\"")); assert!(src.contains("rvf_port_filter")); assert!(src.contains("allowed_ports")); assert!(src.contains("__sk_buff")); } #[test] fn tc_query_route_source_is_valid() { let src = programs::TC_QUERY_ROUTE; assert!(!src.is_empty()); assert!(src.contains("SEC(\"tc\")")); assert!(src.contains("SEC(\"license\")")); assert!(src.contains("\"GPL\"")); assert!(src.contains("rvf_query_classify")); assert!(src.contains("tc_classid")); assert!(src.contains("CLASS_HOT")); assert!(src.contains("CLASS_COLD")); } #[test] fn vmlinux_h_has_essential_types() { let src = programs::VMLINUX_H; assert!(src.contains("struct xdp_md")); assert!(src.contains("struct __sk_buff")); assert!(src.contains("struct ethhdr")); assert!(src.contains("struct iphdr")); assert!(src.contains("struct udphdr")); assert!(src.contains("bpf_map_lookup_elem")); assert!(src.contains("XDP_PASS")); assert!(src.contains("TC_ACT_OK")); } #[test] fn find_clang_detection() { // This test verifies the clang detection logic runs without // panicking. It may or may not find clang depending on the // environment. let result = find_clang(); if let Some(path) = &result { assert!(path.exists(), "detected clang path should exist"); } // Not asserting result.is_some() since clang may not be installed } #[test] fn infer_program_type_from_filename() { assert_eq!( infer_program_type(Path::new("xdp_distance.c")), EbpfProgramType::XdpDistance, ); assert_eq!( infer_program_type(Path::new("socket_filter.c")), EbpfProgramType::SocketFilter, ); assert_eq!( infer_program_type(Path::new("tc_query_route.c")), EbpfProgramType::TcFilter, ); assert_eq!( infer_program_type(Path::new("unknown.c")), EbpfProgramType::Custom, ); } #[test] fn infer_attach_type_from_program_type() { assert_eq!( infer_attach_type(EbpfProgramType::XdpDistance), EbpfAttachType::XdpIngress, ); assert_eq!( infer_attach_type(EbpfProgramType::TcFilter), EbpfAttachType::TcIngress, ); assert_eq!( infer_attach_type(EbpfProgramType::SocketFilter), EbpfAttachType::SocketFilter, ); assert_eq!( infer_attach_type(EbpfProgramType::Custom), EbpfAttachType::None, ); } #[test] fn compiled_program_to_ebpf_header() { let program = CompiledProgram { elf_bytes: vec![0u8; 1024], program_type: EbpfProgramType::XdpDistance, attach_type: EbpfAttachType::XdpIngress, btf_bytes: Some(vec![0u8; 256]), insn_count: 128, program_hash: [0xAB; 32], }; let header = program.to_ebpf_header(2048); assert_eq!(header.ebpf_magic, EBPF_MAGIC); assert_eq!(header.header_version, 1); assert_eq!(header.program_type, EbpfProgramType::XdpDistance as u8); assert_eq!(header.attach_type, EbpfAttachType::XdpIngress as u8); assert_eq!(header.insn_count, 128); assert_eq!(header.max_dimension, 2048); assert_eq!(header.program_size, 1024); assert_eq!(header.btf_size, 256); assert_eq!(header.program_hash, [0xAB; 32]); } #[test] fn ebpf_header_round_trip_from_compiled() { let program = CompiledProgram { elf_bytes: vec![0u8; 512], program_type: EbpfProgramType::TcFilter, attach_type: EbpfAttachType::TcIngress, btf_bytes: None, insn_count: 64, program_hash: [0xCD; 32], }; let header = program.to_ebpf_header(1536); let bytes = header.to_bytes(); let decoded = EbpfHeader::from_bytes(&bytes).expect("round-trip should succeed"); assert_eq!(decoded.ebpf_magic, EBPF_MAGIC); assert_eq!(decoded.program_type, EbpfProgramType::TcFilter as u8); assert_eq!(decoded.attach_type, EbpfAttachType::TcIngress as u8); assert_eq!(decoded.insn_count, 64); assert_eq!(decoded.max_dimension, 1536); assert_eq!(decoded.program_size, 512); assert_eq!(decoded.btf_size, 0); assert_eq!(decoded.program_hash, [0xCD; 32]); } #[test] fn opt_level_flags() { assert_eq!(OptLevel::O0.as_flag(), "-O0"); assert_eq!(OptLevel::O1.as_flag(), "-O1"); assert_eq!(OptLevel::O2.as_flag(), "-O2"); assert_eq!(OptLevel::O3.as_flag(), "-O3"); } #[test] fn from_precompiled_xdp_returns_valid_elf() { let prog = EbpfCompiler::from_precompiled(EbpfProgramType::XdpDistance).unwrap(); assert!(!prog.elf_bytes.is_empty()); assert_eq!(&prog.elf_bytes[..4], b"\x7fELF"); assert_eq!(prog.program_type, EbpfProgramType::XdpDistance); assert_eq!(prog.attach_type, EbpfAttachType::XdpIngress); assert!(prog.insn_count > 0); // ELF class should be ELFCLASS64 assert_eq!(prog.elf_bytes[4], 2); // Data encoding should be little-endian assert_eq!(prog.elf_bytes[5], 1); // e_machine should be EM_BPF (247) assert_eq!(prog.elf_bytes[18], 247); } #[test] fn from_precompiled_socket_filter_returns_valid_elf() { let prog = EbpfCompiler::from_precompiled(EbpfProgramType::SocketFilter).unwrap(); assert_eq!(&prog.elf_bytes[..4], b"\x7fELF"); assert_eq!(prog.program_type, EbpfProgramType::SocketFilter); assert_eq!(prog.attach_type, EbpfAttachType::SocketFilter); assert_eq!(prog.elf_bytes[18], 247); // EM_BPF } #[test] fn from_precompiled_tc_returns_valid_elf() { let prog = EbpfCompiler::from_precompiled(EbpfProgramType::TcFilter).unwrap(); assert_eq!(&prog.elf_bytes[..4], b"\x7fELF"); assert_eq!(prog.program_type, EbpfProgramType::TcFilter); assert_eq!(prog.attach_type, EbpfAttachType::TcIngress); assert_eq!(prog.elf_bytes[18], 247); // EM_BPF } #[test] fn from_precompiled_unknown_type_returns_error() { let result = EbpfCompiler::from_precompiled(EbpfProgramType::Custom); assert!(result.is_err()); } #[test] fn precompiled_elf_has_valid_structure() { // Verify all three precompiled programs have valid ELF structure for (name, elf) in [ ("xdp", precompiled::xdp_distance()), ("socket", precompiled::socket_filter()), ("tc", precompiled::tc_query_route()), ] { // ELF magic assert_eq!(&elf[..4], b"\x7fELF", "{name}: ELF magic"); // 64-bit, little-endian assert_eq!(elf[4], 2, "{name}: ELFCLASS64"); assert_eq!(elf[5], 1, "{name}: little-endian"); // ET_REL assert_eq!(elf[16], 1, "{name}: ET_REL"); // EM_BPF assert_eq!(elf[18], 247, "{name}: EM_BPF"); // e_shnum = 3 (null + .text + .shstrtab) assert_eq!(elf[60], 3, "{name}: 3 section headers"); // Size is reasonable assert!( elf.len() > 64 && elf.len() < 1024, "{name}: reasonable size" ); } } #[test] fn ebpf_error_display() { let err = EbpfError::ClangNotFound; assert_eq!(format!("{err}"), "clang not found in PATH"); let err = EbpfError::CompilationFailed("syntax error".into()); assert!(format!("{err}").contains("syntax error")); let err = EbpfError::InvalidElf; assert!(format!("{err}").contains("empty or invalid")); } #[test] fn compiler_new_returns_error_or_ok() { // This tests that the constructor doesn't panic. // It returns Ok if clang is found, Err otherwise. let result = EbpfCompiler::new(); match result { Ok(compiler) => { assert!(compiler.clang_path().exists()); } Err(EbpfError::ClangNotFound) => { // Expected on systems without clang } Err(e) => panic!("unexpected error: {e}"), } } #[test] fn compiler_with_explicit_clang() { let compiler = EbpfCompiler::with_clang(Path::new("/usr/bin/clang")); assert_eq!(compiler.clang_path(), Path::new("/usr/bin/clang")); } /// If clang is available, test actual compilation of the socket_filter program. #[test] fn compile_socket_filter_if_clang_available() { let compiler = match EbpfCompiler::new() { Ok(c) => c, Err(EbpfError::ClangNotFound) => { eprintln!("skipping: clang not found"); return; } Err(e) => panic!("unexpected error: {e}"), }; // Write vmlinux.h + socket_filter.c to a temp dir let dir = tempfile::tempdir().unwrap(); let vmlinux_path = dir.path().join("vmlinux.h"); let source_path = dir.path().join("socket_filter.c"); std::fs::write(&vmlinux_path, programs::VMLINUX_H).unwrap(); std::fs::write(&source_path, programs::SOCKET_FILTER).unwrap(); let result = compiler.compile(&source_path); match result { Ok(program) => { assert!(!program.elf_bytes.is_empty()); assert_eq!(program.program_type, EbpfProgramType::SocketFilter); assert_eq!(program.attach_type, EbpfAttachType::SocketFilter); assert!(program.insn_count > 0); // Verify ELF magic bytes assert_eq!(&program.elf_bytes[..4], b"\x7fELF"); } Err(EbpfError::CompilationFailed(msg)) => { // Compilation may fail if system lacks BPF-capable clang eprintln!("compilation failed (may need BPF-capable clang): {msg}"); } Err(e) => panic!("unexpected error: {e}"), } } /// If clang is available, test compile_source with in-memory source. #[test] fn compile_source_if_clang_available() { let compiler = match EbpfCompiler::new() { Ok(c) => c, Err(EbpfError::ClangNotFound) => { eprintln!("skipping: clang not found"); return; } Err(e) => panic!("unexpected error: {e}"), }; let result = compiler.compile_source(programs::SOCKET_FILTER, EbpfProgramType::SocketFilter); match result { Ok(program) => { assert!(!program.elf_bytes.is_empty()); assert_eq!(program.program_type, EbpfProgramType::SocketFilter); } Err(EbpfError::CompilationFailed(msg)) => { eprintln!("compilation failed (may need BPF-capable clang): {msg}"); } Err(e) => panic!("unexpected error: {e}"), } } /// If clang is available, test compilation of the XDP distance program. #[test] fn compile_xdp_distance_if_clang_available() { let compiler = match EbpfCompiler::new() { Ok(c) => c, Err(EbpfError::ClangNotFound) => { eprintln!("skipping: clang not found"); return; } Err(e) => panic!("unexpected error: {e}"), }; let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("vmlinux.h"), programs::VMLINUX_H).unwrap(); std::fs::write(dir.path().join("xdp_distance.c"), programs::XDP_DISTANCE).unwrap(); let result = compiler.compile(&dir.path().join("xdp_distance.c")); match result { Ok(program) => { assert!(!program.elf_bytes.is_empty()); assert_eq!(program.program_type, EbpfProgramType::XdpDistance); assert_eq!(program.attach_type, EbpfAttachType::XdpIngress); assert_eq!(&program.elf_bytes[..4], b"\x7fELF"); } Err(EbpfError::CompilationFailed(msg)) => { eprintln!("XDP compilation failed: {msg}"); } Err(e) => panic!("unexpected error: {e}"), } } /// If clang is available, test compilation of the TC query route program. #[test] fn compile_tc_query_route_if_clang_available() { let compiler = match EbpfCompiler::new() { Ok(c) => c, Err(EbpfError::ClangNotFound) => { eprintln!("skipping: clang not found"); return; } Err(e) => panic!("unexpected error: {e}"), }; let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("vmlinux.h"), programs::VMLINUX_H).unwrap(); std::fs::write( dir.path().join("tc_query_route.c"), programs::TC_QUERY_ROUTE, ) .unwrap(); let result = compiler.compile(&dir.path().join("tc_query_route.c")); match result { Ok(program) => { assert!(!program.elf_bytes.is_empty()); assert_eq!(program.program_type, EbpfProgramType::TcFilter); assert_eq!(program.attach_type, EbpfAttachType::TcIngress); assert_eq!(&program.elf_bytes[..4], b"\x7fELF"); } Err(EbpfError::CompilationFailed(msg)) => { eprintln!("TC compilation failed: {msg}"); } Err(e) => panic!("unexpected error: {e}"), } } }