use crate::error::{AttachError, LoadError};
use crate::xdp_attach::{
XdpAttachMode, attach_xdp_raw, detach_xdp_raw, get_prog_fd, unpin,
};
use libbpf_rs::{ObjectBuilder, ProgramType, Link};
use ring::digest::{Context, SHA256};
use std::path::PathBuf;
use zenith_foundation::ct_compare::constant_time_eq;
const XDP_MAIN_BYTES: &[u8] = include_bytes!("../../../bpf/prebuilt/xdp_main.o");
const XDP_REDIRECT_BYTES: &[u8] = include_bytes!("../../../bpf/prebuilt/xdp_redirect.o");
const XDP_STATS_BYTES: &[u8] = include_bytes!("../../../bpf/prebuilt/xdp_stats.o");
include!("generated/bpf_hashes.rs");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XdpProgramName {
Main,
Redirect,
Stats,
}
impl XdpProgramName {
pub fn as_str(&self) -> &'static str {
match self {
XdpProgramName::Main => "xdp_main",
XdpProgramName::Redirect => "xdp_redirect",
XdpProgramName::Stats => "xdp_stats",
}
}
fn get_bytecode(&self) -> &'static [u8] {
match self {
XdpProgramName::Main => XDP_MAIN_BYTES,
XdpProgramName::Redirect => XDP_REDIRECT_BYTES,
XdpProgramName::Stats => XDP_STATS_BYTES,
}
}
pub fn verify_integrity(&self) -> Result<(), LoadError> {
let bytecode = self.get_bytecode();
if bytecode.is_empty() {
return Ok(());
}
verify_bytes_hash(self.as_str(), bytecode)
}
}
fn verify_bytes_hash(prog_key: &str, bytecode: &[u8]) -> Result<(), LoadError> {
let mut ctx = Context::new(&SHA256);
ctx.update(bytecode);
let digest = ctx.finish();
let hash_str = hex_encode(digest.as_ref());
let expected = EXPECTED_HASHES
.iter()
.find(|(name, _)| *name == prog_key)
.map(|(_, hash)| *hash);
match expected {
Some(expected_hash) => {
if constant_time_eq(hash_str.as_bytes(), expected_hash.as_bytes()) {
Ok(())
} else {
Err(LoadError::VerificationFailed(format!(
"SHA-256 hash mismatch for {}: expected {}, got {}",
prog_key, expected_hash, hash_str
)))
}
}
None => Err(LoadError::VerificationFailed(format!(
"no expected SHA-256 hash registered for program key '{}'",
prog_key
))),
}
}
use zenith_foundation::hex_encode;
pub struct XdpLoader {
program_name: XdpProgramName,
obj: libbpf_rs::Object,
active_link: Option<Link>,
attach_mode: Option<XdpAttachMode>,
attached_ifindex: Option<i32>,
prog_fd: Option<std::os::fd::OwnedFd>,
pin_path: Option<PathBuf>,
}
impl XdpLoader {
pub fn load(name: XdpProgramName) -> Result<Self, LoadError> {
let bytecode = name.get_bytecode();
if bytecode.is_empty() {
return Err(LoadError::EmptyBytecode);
}
name.verify_integrity()?;
let obj = ObjectBuilder::default()
.open_memory(bytecode)
.map_err(|e| LoadError::Libbpf(format!("Failed to open ELF: {}", e)))?
.load()
.map_err(|e| LoadError::Libbpf(format!("Failed to load object: {}", e)))?;
Ok(Self {
program_name: name,
obj,
active_link: None,
attach_mode: None,
attached_ifindex: None,
prog_fd: None,
pin_path: None,
})
}
pub fn program_name(&self) -> XdpProgramName {
self.program_name
}
#[inline]
pub fn is_full_filter_chain(&self) -> bool {
self.program_name == XdpProgramName::Main
}
pub fn object(&self) -> &libbpf_rs::Object {
&self.obj
}
pub fn object_mut(&mut self) -> &mut libbpf_rs::Object {
&mut self.obj
}
pub fn xdp_program(&self) -> Option<libbpf_rs::Program<'_>> {
self.obj.progs().find(|p| {
matches!(p.prog_type(), ProgramType::Xdp)
})
}
pub fn xdp_program_by_name(&self, name: &str) -> Option<libbpf_rs::Program<'_>> {
self.obj
.progs()
.find(|p| p.name() == name && matches!(p.prog_type(), ProgramType::Xdp))
}
pub fn xdp_program_mut(&mut self) -> Option<libbpf_rs::ProgramMut<'_>> {
self.obj.progs_mut().find(|p| {
matches!(p.prog_type(), ProgramType::Xdp)
})
}
pub fn xdp_program_mut_by_name(&mut self, name: &str) -> Option<libbpf_rs::ProgramMut<'_>> {
self.obj
.progs_mut()
.find(|p| p.name() == name && matches!(p.prog_type(), ProgramType::Xdp))
}
pub fn prog_count(&self) -> usize {
self.obj.progs().count()
}
pub fn map_count(&self) -> usize {
self.obj.maps().count()
}
pub fn active_link(&self) -> Option<&Link> {
self.active_link.as_ref()
}
pub fn attach_xdp(&mut self, ifindex: i32) -> Result<(), AttachError> {
let prog = self
.xdp_program_mut()
.ok_or_else(|| AttachError::InterfaceNotFound("XDP program not found".to_string()))?;
let link = prog
.attach_xdp(ifindex)
.map_err(|e| AttachError::Libbpf(format!("Failed to attach XDP: {}", e)))?;
self.active_link = Some(link);
Ok(())
}
pub fn attach_xdp_replace(&mut self, ifindex: i32) -> Result<(), AttachError> {
let mut link_opt = self.active_link.take();
if let Some(link) = link_opt.as_mut() {
let prog = self
.xdp_program_mut()
.ok_or_else(|| AttachError::InterfaceNotFound("XDP program not found".to_string()))?;
link.update_prog(&prog)
.map_err(|e| AttachError::Libbpf(format!("Failed to atomically replace XDP: {}", e)))?;
} else {
let prog = self
.xdp_program_mut()
.ok_or_else(|| AttachError::InterfaceNotFound("XDP program not found".to_string()))?;
let link = prog
.attach_xdp(ifindex)
.map_err(|e| AttachError::Libbpf(format!("Failed to attach XDP: {}", e)))?;
link_opt = Some(link);
}
self.active_link = link_opt;
Ok(())
}
pub fn update_link_prog(&mut self, new_prog_owner: &mut XdpLoader) -> Result<(), AttachError> {
let prog = new_prog_owner.xdp_program().ok_or_else(|| {
AttachError::InterfaceNotFound("standby XDP program not found".to_string())
})?;
let link = self.active_link.as_mut().ok_or_else(|| {
AttachError::InterfaceNotFound("no active bpf_link to update".to_string())
})?;
link.update_prog(&prog).map_err(|e| {
AttachError::Libbpf(format!(
"bpf_link update_prog failed (old program still online): {}",
e
))
})
}
pub fn take_link(&mut self) -> Option<Link> {
self.active_link.take()
}
pub fn set_link(&mut self, link: Option<Link>) {
self.active_link = link;
}
pub fn detach_xdp(&mut self) -> Result<(), AttachError> {
if let Some(link) = self.active_link.take() {
link.detach()
.map_err(|e| AttachError::Libbpf(format!("Failed to detach XDP (bpf_link): {}", e)))?;
return Ok(());
}
if let Some(mode) = self.attach_mode.take()
&& let Some(ifindex) = self.attached_ifindex.take() {
detach_xdp_raw(ifindex, mode)?;
}
Ok(())
}
pub fn attach_xdp_optimal(&mut self, ifindex: i32) -> Result<XdpAttachMode, AttachError> {
self.attach_xdp_with_mode(ifindex, XdpAttachMode::Auto)
}
pub fn attach_xdp_with_mode(
&mut self,
ifindex: i32,
mode: XdpAttachMode,
) -> Result<XdpAttachMode, AttachError> {
let prog_name = self
.obj
.progs()
.find(|p| matches!(p.prog_type(), ProgramType::Xdp))
.map(|p| p.name().to_str().unwrap_or("").to_string())
.filter(|n| !n.is_empty())
.ok_or_else(|| {
AttachError::InterfaceNotFound("XDP program not found".to_string())
})?;
let prog_fd = get_prog_fd(&mut self.obj, &prog_name)?;
let actual_mode = attach_xdp_raw(ifindex, prog_fd, mode)?;
self.attach_mode = Some(actual_mode);
self.attached_ifindex = Some(ifindex);
Ok(actual_mode)
}
pub fn current_attach_mode(&self) -> Option<XdpAttachMode> {
self.attach_mode
}
pub fn destroy_link(&mut self) -> Result<(), AttachError> {
self.active_link = None;
self.attach_mode = None;
self.attached_ifindex = None;
self.prog_fd = None;
if let Some(path) = self.pin_path.take() {
unpin(&path);
}
Ok(())
}
}
impl std::fmt::Debug for XdpLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("XdpLoader")
.field("program_name", &self.program_name)
.field("prog_count", &self.prog_count())
.field("map_count", &self.map_count())
.field("has_active_link", &self.active_link.is_some())
.field("attach_mode", &self.attach_mode)
.field("attached_ifindex", &self.attached_ifindex)
.finish()
}
}
impl Drop for XdpLoader {
fn drop(&mut self) {
let _ = self.detach_xdp();
if let Some(path) = self.pin_path.take() {
unpin(&path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn should_skip_bpf_load() -> bool {
if cfg!(not(target_os = "linux")) {
eprintln!("[SKIP] 非 Linux 平台,跳过 eBPF 加载测试");
return true;
}
if std::env::var("ZENITH_SKIP_BPF_LOAD_TEST")
.map(|v| v == "1" || v == "true")
.unwrap_or(false)
{
eprintln!("[SKIP] ZENITH_SKIP_BPF_LOAD_TEST=1,跳过 eBPF 加载测试");
return true;
}
false
}
macro_rules! require_load {
($name:expr) => {{
if should_skip_bpf_load() {
return;
}
match XdpLoader::load($name) {
Ok(loader) => loader,
Err(e) => panic!(
"XdpLoader::load({:?}) 必须成功。eBPF 加载失败说明字节码无法通过内核验证器,\
可能原因:vmlinux.h 枚举错误、map_type 数值与内核不匹配、\
clang 版本不兼容。错误: {:?}",
$name, e
),
}
}};
}
#[test]
fn test_xdp_program_name_as_str() {
assert_eq!(XdpProgramName::Main.as_str(), "xdp_main");
assert_eq!(XdpProgramName::Redirect.as_str(), "xdp_redirect");
assert_eq!(XdpProgramName::Stats.as_str(), "xdp_stats");
}
#[test]
fn test_hash_lookup_keys_match_expected_hashes() {
for name in [
XdpProgramName::Main,
XdpProgramName::Redirect,
XdpProgramName::Stats,
] {
let found = EXPECTED_HASHES.iter().any(|(k, _)| *k == name.as_str());
assert!(
found,
"EXPECTED_HASHES 必须包含键 '{}'(bpf_hashes.rs 由 build.rs 生成)",
name.as_str()
);
}
}
#[test]
fn test_tampered_bytecode_fails_verification() {
let tampered = b"tampered-bytecode-not-the-real-elf";
let result = verify_bytes_hash(XdpProgramName::Main.as_str(), tampered);
assert!(result.is_err(), "篡改字节码必须校验失败");
let err = result.err();
assert!(
matches!(err, Some(LoadError::VerificationFailed(_))),
"错误类型必须为 VerificationFailed,实际: {:?}",
err
);
}
#[test]
fn test_unknown_program_key_fails_verification() {
let result = verify_bytes_hash("xdp_nonexistent", b"whatever");
assert!(result.is_err(), "未注册键必须校验失败");
assert!(
matches!(result.err(), Some(LoadError::VerificationFailed(_))),
"错误类型必须为 VerificationFailed"
);
}
#[test]
fn test_embedded_bytecode_passes_verification() {
let allow_missing = std::env::var("ZENITH_ALLOW_MISSING_BPF")
.map(|v| v == "1" || v == "true")
.unwrap_or(false);
for name in [
XdpProgramName::Main,
XdpProgramName::Redirect,
XdpProgramName::Stats,
] {
if allow_missing && name.get_bytecode().is_empty() {
continue;
}
assert!(
name.verify_integrity().is_ok(),
"嵌入字节码 {:?} 必须通过 SHA-256 完整性校验",
name
);
}
}
#[test]
fn test_xdp_program_name_get_bytecode() {
let main_bytes = XdpProgramName::Main.get_bytecode();
let redirect_bytes = XdpProgramName::Redirect.get_bytecode();
let stats_bytes = XdpProgramName::Stats.get_bytecode();
let allow_missing = std::env::var("ZENITH_ALLOW_MISSING_BPF")
.map(|v| v == "1" || v == "true")
.unwrap_or(false);
if !allow_missing {
assert!(!main_bytes.is_empty(), "xdp_main.o 字节码不能为空");
assert!(!redirect_bytes.is_empty(), "xdp_redirect.o 字节码不能为空");
assert!(!stats_bytes.is_empty(), "xdp_stats.o 字节码不能为空");
}
let _ = (main_bytes, redirect_bytes, stats_bytes);
}
#[test]
fn test_xdp_loader_creation() {
let loader = require_load!(XdpProgramName::Main);
assert!(
loader.xdp_program().is_some(),
"加载后必须包含 XDP 类型程序"
);
assert!(loader.prog_count() >= 1, "程序数必须 >= 1");
}
#[test]
fn test_xdp_loader_program_name() {
let loader = require_load!(XdpProgramName::Main);
assert_eq!(loader.program_name(), XdpProgramName::Main);
assert!(
loader.is_full_filter_chain(),
"xdp_main 必须被视为完整过滤链程序"
);
}
#[test]
fn test_xdp_loader_link_management() {
let mut loader = require_load!(XdpProgramName::Redirect);
assert!(loader.active_link().is_none());
let result = loader.destroy_link();
assert!(result.is_ok());
assert!(loader.active_link().is_none());
}
#[test]
fn test_xdp_loader_detach_without_attach() {
let mut loader = require_load!(XdpProgramName::Stats);
let result = loader.detach_xdp();
assert!(result.is_ok());
}
#[test]
fn test_xdp_program_mut_access() {
let mut loader = require_load!(XdpProgramName::Redirect);
let prog = loader.xdp_program_mut();
assert!(prog.is_some(), "xdp_program_mut() 必须返回 Some");
}
#[test]
fn test_xdp_program_name_debug() {
let debug_str = format!("{:?}", XdpProgramName::Main);
assert!(!debug_str.is_empty());
let debug_str = format!("{:?}", XdpProgramName::Redirect);
assert!(!debug_str.is_empty());
let debug_str = format!("{:?}", XdpProgramName::Stats);
assert!(!debug_str.is_empty());
}
#[test]
fn test_xdp_program_name_clone_copy() {
let a = XdpProgramName::Main;
let b = a;
assert_eq!(a, b);
assert_eq!(a.as_str(), b.as_str());
}
#[test]
fn test_xdp_program_name_eq() {
assert_eq!(XdpProgramName::Main, XdpProgramName::Main);
assert_eq!(XdpProgramName::Redirect, XdpProgramName::Redirect);
assert_eq!(XdpProgramName::Stats, XdpProgramName::Stats);
assert_ne!(XdpProgramName::Main, XdpProgramName::Redirect);
assert_ne!(XdpProgramName::Main, XdpProgramName::Stats);
assert_ne!(XdpProgramName::Redirect, XdpProgramName::Stats);
}
#[test]
fn test_xdp_program_name_unique() {
let names = [
XdpProgramName::Main,
XdpProgramName::Redirect,
XdpProgramName::Stats,
];
for (i, a) in names.iter().enumerate() {
for (j, b) in names.iter().enumerate() {
if i != j {
assert_ne!(a, b);
} else {
assert_eq!(a, b);
}
}
}
}
#[test]
fn test_xdp_loader_debug() {
let loader = require_load!(XdpProgramName::Main);
let debug_str = format!("{:?}", loader);
assert!(!debug_str.is_empty());
assert!(debug_str.contains("XdpLoader"));
}
#[test]
fn test_xdp_loader_object_access() {
let loader = require_load!(XdpProgramName::Main);
let _obj = loader.object();
let _ = (loader.prog_count(), loader.map_count());
}
#[test]
fn test_xdp_loader_object_mut_access() {
let mut loader = require_load!(XdpProgramName::Main);
let _obj = loader.object_mut();
}
#[test]
fn test_xdp_program_name_all_variants() {
let variants = [
XdpProgramName::Main,
XdpProgramName::Redirect,
XdpProgramName::Stats,
];
assert_eq!(variants.len(), 3);
for v in variants.iter() {
assert!(!v.as_str().is_empty());
}
}
#[test]
fn test_attach_error_display() {
use crate::error::AttachError;
let e = AttachError::InterfaceNotFound("eth0".to_string());
assert!(format!("{}", e).contains("Interface not found"));
}
#[test]
fn test_load_error_display() {
use crate::error::LoadError;
let e = LoadError::EmptyBytecode;
assert!(format!("{}", e).contains("Bytecode is empty"));
}
#[test]
fn test_all_programs_load_into_kernel() {
if should_skip_bpf_load() {
return;
}
for name in [
XdpProgramName::Main,
XdpProgramName::Redirect,
XdpProgramName::Stats,
] {
let result = XdpLoader::load(name);
assert!(
result.is_ok(),
"eBPF 程序 {:?} 必须能加载到内核。错误: {:?}\
\n可能原因:vmlinux.h 枚举与内核不匹配、clang 编译参数错误、\
map_type 数值错误、内核版本不兼容",
name,
result.err()
);
let loader = result.unwrap();
assert!(
loader.prog_count() >= 1,
"程序 {:?} 加载后必须包含至少 1 个 BPF 程序",
name
);
}
}
}