zenith-linux 0.1.0

Zenith Linux 平台抽象层:AF_XDP Socket、UMEM 内存管理、四环操作(Fill/RX/TX/Completion)、描述符安全校验引擎
Documentation
//! 网络接口抽象(Linux 平台)
//!
//! 封装 `libc::if_nametoindex` 等 Linux 特有系统调用为 safe API,
//! 严格遵守 AGENT.md §1.1 "所有 unsafe 必须封装在 zenith-linux 内部"。
//!
//! # 安全保证
//! 本模块所有 `unsafe` 块均集中在 `libc` FFI 调用,对外提供 `Option` 接口。
//! 调用方零 unsafe 即可完成网卡名→ifindex 的转换。

#![allow(unsafe_code)]

use std::ffi::CString;
use std::io;

/// 网卡名最大长度(含 NUL 终止符)
///
/// Linux 限制 `IFNAMSIZ` = 16,包含 NUL 终止符,故有效网卡名 ≤ 15 字节。
const IFNAMSIZ: usize = 16;

/// 错误类型
#[derive(Debug, thiserror::Error)]
pub enum NetIfError {
    /// 网卡名包含内部 NUL 字节
    #[error("interface name contains NUL byte")]
    InvalidName,
    /// 网卡名过长(超过 IFNAMSIZ-1)
    #[error("interface name too long (max {} bytes)", IFNAMSIZ - 1)]
    NameTooLong,
    /// `if_nametoindex` 系统调用失败(网卡不存在或权限不足)
    #[error("if_nametoindex failed for {name:?}: {source}")]
    NotFound {
        /// 出错的网卡名
        name: String,
        /// 底层 IO 错误(errno 转换)
        #[source]
        source: io::Error,
    },
}

/// 将网卡名转换为内核 ifindex
///
/// # 参数
/// * `name` - 网卡名(如 `"eth0"`、`"lo"`)
///
/// # 返回
/// * `Ok(u32)` - 网卡对应的 ifindex(≥1,1 通常为 lo)
/// * `Err(NetIfError)` - 名称非法或网卡不存在
///
/// # 安全
/// 内部 `unsafe` 块仅用于 `libc::if_nametoindex` FFI 调用:
/// - `CString::new` 已确保 NUL 终止
/// - `as_ptr()` 返回的指针在 FFI 调用期间保持有效
/// - `libc::if_nametoindex` 不持有指针所有权,仅读取
///
/// # 示例
/// ```no_run
/// use zenith_linux::if_nametoindex;
/// let idx = if_nametoindex("lo").expect("lo must exist");
/// assert!(idx >= 1);
/// ```
#[cfg(target_os = "linux")]
pub fn if_nametoindex(name: &str) -> Result<u32, NetIfError> {
    // 边界检查:网卡名 ≤ IFNAMSIZ-1(不含 NUL 终止符)
    if name.len() >= IFNAMSIZ {
        return Err(NetIfError::NameTooLong);
    }

    // CString::new 在名称含内部 NUL 时返回 Err
    let c_name = CString::new(name).map_err(|_| NetIfError::InvalidName)?;

    // SAFETY:
    // 1. c_name 是 CString,保证 NUL 终止且无内部 NUL;
    // 2. as_ptr() 返回的 *const c_str 在 CString 存活期间保持有效;
    // 3. libc::if_nametoindex 是 POSIX 标准函数,仅读取字符串,不持有指针所有权;
    // 4. 返回值 0 表示失败(errno 已设置),非 0 即合法 ifindex。
    let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
    if idx == 0 {
        // SAFETY: libc::__errno_location 在 Linux glibc/musl 上返回线程局部 errno 指针
        let err = io::Error::last_os_error();
        return Err(NetIfError::NotFound {
            name: name.to_string(),
            source: err,
        });
    }
    Ok(idx)
}

/// 非 Linux 平台占位实现
#[cfg(not(target_os = "linux"))]
pub fn if_nametoindex(_name: &str) -> Result<u32, NetIfError> {
    Err(NetIfError::NotFound {
        name: _name.to_string(),
        source: io::Error::new(io::ErrorKind::Unsupported, "non-Linux platform"),
    })
}

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

    #[test]
    fn test_lo_ifindex() {
        // lo 在所有 Linux 系统上必须存在且 ifindex=1
        match if_nametoindex("lo") {
            Ok(idx) => {
                assert_eq!(idx, 1, "lo must have ifindex=1");
            }
            Err(NetIfError::NotFound { .. }) => {
                // 极少数容器环境可能无 lo,但 errno 必须正确分类
            }
            Err(e) => panic!("unexpected error for lo: {:?}", e),
        }
    }

    #[test]
    fn test_nonexistent_iface() {
        // 网卡名必须 ≤ IFNAMSIZ-1=15 字节,否则会先返回 NameTooLong
        let result = if_nametoindex("zz_nx99");
        assert!(matches!(result, Err(NetIfError::NotFound { .. })));
    }

    #[test]
    fn test_name_too_long() {
        // 15 字节合法,16 字节超限
        let long_name = "a".repeat(IFNAMSIZ);
        assert_eq!(long_name.len(), IFNAMSIZ);
        let result = if_nametoindex(&long_name);
        assert!(matches!(result, Err(NetIfError::NameTooLong)));
    }

    #[test]
    fn test_name_with_nul() {
        let result = if_nametoindex("eth\0");
        assert!(matches!(result, Err(NetIfError::InvalidName)));
    }

    #[test]
    fn test_boundary_name_length() {
        // 15 字节网卡名是合法上限(虽然通常不存在)
        let max_name = "a".repeat(IFNAMSIZ - 1);
        // 不应返回 NameTooLong(即便网卡不存在也应返回 NotFound)
        let result = if_nametoindex(&max_name);
        assert!(!matches!(result, Err(NetIfError::NameTooLong)));
    }
}