zenith-ebpf 0.1.0

Zenith eBPF 程序管理:预编译字节码嵌入(include_bytes!)、libbpf-rs 高性能加载、bpf_link 原子挂载、双 Bank 热更新
//! XDP attach 模式管理(DRV / SKB / HW 自动兼容切换)
//!
//! # 设计原则(极端极限极致严格标准)
//!
//! 内核 XDP 有三种 attach 模式,性能与兼容性递减:
//! 1. **HW_MODE**(硬件卸载)— 最快,网卡硬件执行 eBPF,需网卡支持
//! 2. **DRV_MODE**(驱动层)— 次快,零拷贝,需驱动支持 XDP hooks
//! 3. **SKB_MODE**(通用 SKB)— 最慢但兼容性最好,所有网卡均支持
//!
//! 本模块实现自动检测最优模式并回退的机制:
//! - `attach_xdp_optimal()` 按 HW → DRV → SKB 顺序尝试,首次成功即锁定
//! - 失败时自动降级,保证在任意 Linux 环境可用(WSL2/容器/裸机)
//! - 通过 `bpf_xdp_attach()` 系统调用指定 flags,而非 libbpf-rs 默认模式
//!
//! # unsafe 使用
//!
//! 本模块封装 `bpf_xdp_attach` / `bpf_xdp_detach` / `mount` 等 C 系统调用。
//! 是 crate 内唯一被精确放开 `#[allow(unsafe_code)]` 的模块,每个 unsafe 块
//! 均附带详细 SAFETY 论证。

#![allow(unsafe_code)]
#![deny(missing_debug_implementations)]
#![warn(missing_docs)]

use crate::error::AttachError;
use std::os::fd::{AsFd, AsRawFd};

/// XDP attach 模式
///
/// 性能递减顺序:Hw > Drv > Skb
/// 兼容性递增顺序:Hw < Drv < Skb
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum XdpAttachMode {
    /// 自动选择最优模式(HW → DRV → SKB 逐级回退)
    Auto,
    /// 驱动层模式(零拷贝,最快通用模式)
    Drv,
    /// 通用 SKB 模式(兼容性最好,所有网卡支持)
    Skb,
    /// 硬件卸载模式(最快,需网卡硬件支持)
    Hw,
}

impl XdpAttachMode {
    /// 转为 libbpf-sys 的 XDP_FLAGS_* 常量
    ///
    /// `Auto` 返回 0,由调用方逐级尝试
    #[inline]
    fn to_flags(self) -> u32 {
        match self {
            XdpAttachMode::Auto => 0,
            XdpAttachMode::Drv => libbpf_sys::XDP_FLAGS_DRV_MODE,
            XdpAttachMode::Skb => libbpf_sys::XDP_FLAGS_SKB_MODE,
            XdpAttachMode::Hw => libbpf_sys::XDP_FLAGS_HW_MODE,
        }
    }

    /// 获取模式名称(用于日志)
    pub fn as_str(self) -> &'static str {
        match self {
            XdpAttachMode::Auto => "auto",
            XdpAttachMode::Drv => "drv",
            XdpAttachMode::Skb => "skb",
            XdpAttachMode::Hw => "hw",
        }
    }

    /// 按性能从高到低返回具体模式列表(用于 Auto 逐级尝试)
    fn fallback_chain() -> &'static [XdpAttachMode] {
        &[
            XdpAttachMode::Hw,  // 最快:硬件卸载
            XdpAttachMode::Drv, // 次快:驱动层零拷贝
            XdpAttachMode::Skb, // 兜底:通用兼容
        ]
    }
}

/// 尝试按回退链挂载 XDP 程序(Hw → Drv → Skb)。

/// 获取已加载 XDP 程序的 attach 模式(查询网卡当前 XDP 状态)
///
/// 返回当前网卡上已 attach 的 XDP 程序的模式。如果没有 XDP 程序,返回 None。
/// 可用于运行时检测网卡 XDP 状态,或验证 attach 是否成功。
///
/// # 参数
/// * `ifindex` - 网卡接口索引
///
/// # 返回
/// * `Ok(Some(mode))` - 网卡上已 attach 的 XDP 模式
/// * `Ok(None)` - 网卡上无 XDP 程序
/// * `Err(_)` - 查询失败
///
/// # SAFETY
/// `bpf_xdp_query_id` 是 libbpf-sys 函数,参数为有效 ifindex 和输出指针。
/// prog_id 是栈变量,指针在调用期间有效。
pub fn query_xdp_mode(ifindex: i32) -> Result<Option<XdpAttachMode>, AttachError> {
    // 逐个模式查询,返回第一个匹配的模式
    for mode in XdpAttachMode::fallback_chain() {
        let mut prog_id: u32 = 0;
        // SAFETY: bpf_xdp_query_id 是纯查询函数,prog_id 为栈变量,
        // 指针在调用期间有效。ifindex 由调用方保证有效。
        let ret = unsafe {
            libbpf_sys::bpf_xdp_query_id(
                ifindex,
                mode.to_flags() as i32,
                &mut prog_id as *mut u32,
            )
        };
        if ret == 0 && prog_id != 0 {
            return Ok(Some(*mode));
        }
    }
    Ok(None)
}

/// 用指定模式 attach XDP 程序
///
/// 通过 `bpf_xdp_attach()` 系统调用指定 XDP_FLAGS_* 模式 attach。
/// 与 libbpf-rs 的 `attach_xdp()` 不同,本函数支持 DRV/SKB/HW 三种模式。
///
/// # 参数
/// * `ifindex` - 网卡接口索引
/// * `prog_fd` - 已加载的 BPF 程序文件描述符
/// * `mode` - attach 模式(Auto 会逐级尝试)
///
/// # 返回
/// 成功时返回实际使用的模式(Auto 解析为具体模式)
pub(crate) fn attach_xdp_raw(
    ifindex: i32,
    prog_fd: i32,
    mode: XdpAttachMode,
) -> Result<XdpAttachMode, AttachError> {
    let modes_to_try: Vec<XdpAttachMode> = if mode == XdpAttachMode::Auto {
        XdpAttachMode::fallback_chain().to_vec()
    } else {
        vec![mode]
    };

    let mut last_err = String::new();

    for &m in &modes_to_try {
        let flags = m.to_flags();
        // SAFETY: bpf_xdp_attach 是 libbpf-sys 函数。
        // - ifindex: 由调用方保证有效(来自 getifindex)
        // - prog_fd: 来自 OwnedFd::as_raw_fd(),保证有效
        // - flags: 编译期常量 XDP_FLAGS_*
        // - opts: NULL(bpf_xdp_attach_opts 可选)
        let ret = unsafe {
            libbpf_sys::bpf_xdp_attach(
                ifindex,
                prog_fd,
                flags,
                std::ptr::null(),
            )
        };

        if ret == 0 {
            return Ok(m);
        }

        let err = std::io::Error::last_os_error();
        last_err = format!("{} 模式失败: {} (errno={})", m.as_str(), err, err.raw_os_error().unwrap_or(0));

        // ENOTSUP = 网卡/驱动不支持此模式,继续尝试下一模式
        // EPERM = 无权限,继续尝试(可能其他模式权限不同)
        // EBUSY = 已有程序 attach,需要先 detach
    }

    Err(AttachError::Libbpf(format!(
        "所有 XDP attach 模式均失败。最后错误: {}",
        last_err
    )))
}

/// 用指定模式 detach XDP 程序
///
/// # SAFETY
/// `bpf_xdp_detach` 是 libbpf-sys 函数,参数为有效 ifindex 和 flags。
pub(crate) fn detach_xdp_raw(ifindex: i32, mode: XdpAttachMode) -> Result<(), AttachError> {
    let flags = mode.to_flags();
    // SAFETY: bpf_xdp_detach 是 libbpf-sys 函数。
    // - ifindex: 由调用方保证有效
    // - flags: 与 attach 时相同的模式 flags
    // - opts: NULL
    let ret = unsafe {
        libbpf_sys::bpf_xdp_detach(
            ifindex,
            flags,
            std::ptr::null(),
        )
    };

    if ret == 0 {
        Ok(())
    } else {
        // detach 时如果没有程序 attach,返回 0(libbpf 内部处理)
        // 只有真正的错误才返回非零
        let err = std::io::Error::last_os_error();
        Err(AttachError::Libbpf(format!(
            "detach 失败 (mode={}): {} (errno={})",
            mode.as_str(),
            err,
            err.raw_os_error().unwrap_or(0)
        )))
    }
}

/// 获取已加载 XDP 程序的 fd(直接获取,避免 pin 的 TOCTOU 风险)
///
/// 通过 `prog.as_fd().as_raw_fd()` 直接获取程序 fd,
/// 无需 pin 到 bpffs,消除 pin + fd_from_pinned_path 的 TOCTOU 竞争。
///
/// # 安全
/// `prog_name` 仅允许 `[A-Za-z0-9_]` 字符,防止路径注入。
///
/// # 参数
/// * `obj` - 已加载的 libbpf-rs Object
/// * `prog_name` - BPF 程序名称(C 函数名,如 "zenith_xdp_main")
///
/// # 返回
/// 成功时返回程序 fd(raw fd,由 Object 持有所有权,
/// `bpf_xdp_attach` 内核会 dup fd,调用方无需管理 fd 生命周期)
pub(crate) fn get_prog_fd(
    obj: &mut libbpf_rs::Object,
    prog_name: &str,
) -> Result<i32, AttachError> {
    // 校验 prog_name 仅含 [A-Za-z0-9_],防止路径注入
    if prog_name.is_empty()
        || !prog_name
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '_')
    {
        return Err(AttachError::Libbpf(format!(
            "无效的程序名称(仅允许 [A-Za-z0-9_]):{}",
            prog_name
        )));
    }

    // 查找指定名称的程序
    let prog = obj
        .progs_mut()
        .find(|p| p.name().to_str().unwrap_or("") == prog_name)
        .ok_or_else(|| {
            AttachError::Libbpf(format!("程序 {} 未找到", prog_name))
        })?;

    // 直接获取 fd(由 Object 持有所有权,bpf_xdp_attach 内核会 dup fd,无 TOCTOU)
    Ok(prog.as_fd().as_raw_fd())
}

/// Unpin 程序(清理 bpffs 上的 pin 文件)
pub(crate) fn unpin(pin_path: &std::path::Path) {
    let _ = std::fs::remove_file(pin_path);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_xdp_attach_mode_flags() {
        assert_eq!(XdpAttachMode::Drv.to_flags(), libbpf_sys::XDP_FLAGS_DRV_MODE);
        assert_eq!(XdpAttachMode::Skb.to_flags(), libbpf_sys::XDP_FLAGS_SKB_MODE);
        assert_eq!(XdpAttachMode::Hw.to_flags(), libbpf_sys::XDP_FLAGS_HW_MODE);
        assert_eq!(XdpAttachMode::Auto.to_flags(), 0);
    }

    #[test]
    fn test_xdp_attach_mode_as_str() {
        assert_eq!(XdpAttachMode::Auto.as_str(), "auto");
        assert_eq!(XdpAttachMode::Drv.as_str(), "drv");
        assert_eq!(XdpAttachMode::Skb.as_str(), "skb");
        assert_eq!(XdpAttachMode::Hw.as_str(), "hw");
    }

    #[test]
    fn test_fallback_chain_order() {
        let chain = XdpAttachMode::fallback_chain();
        // 性能从高到低:Hw → Drv → Skb
        assert_eq!(chain[0], XdpAttachMode::Hw);
        assert_eq!(chain[1], XdpAttachMode::Drv);
        assert_eq!(chain[2], XdpAttachMode::Skb);
    }
}