use crate::error::MapError;
use libbpf_rs::{MapCore, MapFlags};
use std::rc::Rc;
pub struct BpfMaps<'obj> {
pub xsk_map: Rc<libbpf_rs::Map<'obj>>,
pub stats_map: Rc<libbpf_rs::Map<'obj>>,
pub config_map: Rc<libbpf_rs::Map<'obj>>,
pub expectation_map: Rc<libbpf_rs::Map<'obj>>,
}
impl<'obj> BpfMaps<'obj> {
pub fn from_object(obj: &'obj libbpf_rs::Object) -> Result<Self, MapError> {
let mut xsk_map = None;
let mut stats_map = None;
let mut config_map = None;
let mut expectation_map = None;
for map in obj.maps() {
let name = map.info().map(|i| {
let bytes = i.info.name.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as u8)
.collect::<Vec<_>>();
String::from_utf8_lossy(&bytes).to_string()
}).unwrap_or_default();
match name.as_str() {
"xsk_map" => xsk_map = Some(Rc::new(map)),
"stats_map" => stats_map = Some(Rc::new(map)),
"config_map" => config_map = Some(Rc::new(map)),
"expectation_map" => expectation_map = Some(Rc::new(map)),
_ => {}
}
}
Ok(Self {
xsk_map: xsk_map.ok_or_else(|| MapError::NotFound("xsk_map".to_string()))?,
stats_map: stats_map.ok_or_else(|| MapError::NotFound("stats_map".to_string()))?,
config_map: config_map.ok_or_else(|| MapError::NotFound("config_map".to_string()))?,
expectation_map: expectation_map
.ok_or_else(|| MapError::NotFound("expectation_map".to_string()))?,
})
}
pub fn update_xsk(&self, queue_id: u32, xsk_fd: u32) -> Result<(), MapError> {
let key = &queue_id.to_ne_bytes();
let value = &xsk_fd.to_ne_bytes();
self.xsk_map
.update(key, value, MapFlags::ANY)
.map_err(|e| MapError::Libbpf(format!("Failed to update xsk_map: {}", e)))
}
pub fn delete_xsk(&self, queue_id: u32) -> Result<(), MapError> {
let key = &queue_id.to_ne_bytes();
self.xsk_map
.delete(key)
.map_err(|e| MapError::Libbpf(format!("Failed to delete from xsk_map: {}", e)))
}
pub fn lookup_xsk(&self, queue_id: u32) -> Result<Option<u32>, MapError> {
let key = &queue_id.to_ne_bytes();
let value = self
.xsk_map
.lookup(key, MapFlags::ANY)
.map_err(|e| MapError::Libbpf(format!("Failed to lookup xsk_map: {}", e)))?;
match value {
Some(v) => {
let arr: [u8; 4] = v.as_slice().try_into().map_err(|_| {
MapError::SizeMismatch {
expected: 4,
actual: v.len(),
}
})?;
Ok(Some(u32::from_ne_bytes(arr)))
}
None => Ok(None),
}
}
pub fn lookup_stat(&self, stat_id: u32) -> Result<Option<u64>, MapError> {
let key = &stat_id.to_ne_bytes();
let value = self
.stats_map
.lookup(key, MapFlags::ANY)
.map_err(|e| MapError::Libbpf(format!("Failed to lookup stats_map: {}", e)))?;
match value {
Some(v) => {
if v.len() < 8 || v.len() % 8 != 0 {
return Ok(None);
}
let mut total: u64 = 0;
for chunk in v.chunks_exact(8) {
let n = u64::from_ne_bytes(chunk.try_into().map_err(|_| {
MapError::SizeMismatch {
expected: 8,
actual: chunk.len(),
}
})?);
total = total.checked_add(n).ok_or(MapError::CounterOverflow)?;
}
Ok(Some(total))
}
None => Ok(None),
}
}
pub const CONFIG_KEY_MTU: u32 = 0;
pub const CONFIG_KEY_PROTO_WHITELIST: u32 = 1;
pub const CONFIG_KEY_FAIL_CLOSED: u32 = 2;
pub const CONFIG_KEY_WHITELIST_ENABLED: u32 = 3;
pub const CONFIG_KEY_EXPECTATION: u32 = 10;
pub fn update_config(&self, key: u32, value: u64) -> Result<(), MapError> {
let key_bytes = &key.to_ne_bytes();
let val_bytes = &value.to_ne_bytes();
self.config_map
.update(key_bytes, val_bytes, MapFlags::ANY)
.map_err(|e| MapError::Libbpf(format!("Failed to update config_map: {}", e)))
}
pub fn lookup_config(&self, key: u32) -> Result<Option<u64>, MapError> {
let key_bytes = &key.to_ne_bytes();
let value = self
.config_map
.lookup(key_bytes, MapFlags::ANY)
.map_err(|e| MapError::Libbpf(format!("Failed to lookup config_map: {}", e)))?;
match value {
Some(v) => {
if v.len() < 8 {
return Ok(None);
}
let arr: [u8; 8] = v[..8].try_into().map_err(|_| MapError::SizeMismatch {
expected: 8,
actual: v.len(),
})?;
Ok(Some(u64::from_ne_bytes(arr)))
}
None => Ok(None),
}
}
pub fn set_mtu(&self, mtu: u32) -> Result<(), MapError> {
self.update_config(Self::CONFIG_KEY_MTU, mtu as u64)
}
pub fn get_mtu(&self) -> Result<Option<u32>, MapError> {
self.lookup_config(Self::CONFIG_KEY_MTU)
.map(|v| v.map(|val| val as u32))
}
pub fn set_proto_whitelist(&self, bitmap: u64) -> Result<(), MapError> {
self.update_config(Self::CONFIG_KEY_PROTO_WHITELIST, bitmap)
}
pub fn get_proto_whitelist(&self) -> Result<Option<u64>, MapError> {
self.lookup_config(Self::CONFIG_KEY_PROTO_WHITELIST)
}
pub fn set_proto_whitelist_for(&self, protocols: &[u8]) -> Result<(), MapError> {
let bitmap = Self::proto_whitelist_bitmap(protocols)?;
self.set_proto_whitelist(bitmap)
}
pub fn proto_whitelist_bitmap(protocols: &[u8]) -> Result<u64, MapError> {
let mut bitmap: u64 = 0;
for &proto in protocols {
if proto >= 64 {
return Err(MapError::UnsupportedOperation(format!(
"协议号 {} 超出白名单位图表示范围(>= 64)",
proto
)));
}
bitmap |= 1u64 << proto;
}
Ok(bitmap)
}
pub fn set_fail_closed(&self, enabled: bool) -> Result<(), MapError> {
self.update_config(Self::CONFIG_KEY_FAIL_CLOSED, enabled as u64)
}
pub fn get_fail_closed(&self) -> Result<bool, MapError> {
self.lookup_config(Self::CONFIG_KEY_FAIL_CLOSED)
.map(|v| v.is_some_and(|val| val != 0))
}
pub fn set_whitelist_enabled(&self, enabled: bool) -> Result<(), MapError> {
self.update_config(Self::CONFIG_KEY_WHITELIST_ENABLED, enabled as u64)
}
pub fn get_whitelist_enabled(&self) -> Result<bool, MapError> {
self.lookup_config(Self::CONFIG_KEY_WHITELIST_ENABLED)
.map(|v| v.is_some_and(|val| val != 0))
}
pub const IPPROTO_TCP: u8 = zenith_foundation::net::IPPROTO_TCP;
pub const IPPROTO_UDP: u8 = zenith_foundation::net::IPPROTO_UDP;
pub const IPPROTO_ICMP: u8 = zenith_foundation::net::IPPROTO_ICMP;
pub const IPPROTO_ICMPV6: u8 = zenith_foundation::net::IPPROTO_ICMPV6;
pub const DEFAULT_PROTO_WHITELIST: u64 = zenith_foundation::net::DEFAULT_PROTO_WHITELIST;
pub fn update_expectation(&self, config: &ExpectationConfig) -> Result<(), MapError> {
let key_bytes = &0u32.to_ne_bytes();
let val_bytes = config.to_bytes();
self.expectation_map
.update(key_bytes, &val_bytes, MapFlags::ANY)
.map_err(|e| MapError::Libbpf(format!("Failed to update expectation_map: {}", e)))
}
pub fn lookup_expectation(&self) -> Result<Option<ExpectationConfig>, MapError> {
let key_bytes = &0u32.to_ne_bytes();
let value = self
.expectation_map
.lookup(key_bytes, MapFlags::ANY)
.map_err(|e| MapError::Libbpf(format!("Failed to lookup expectation_map: {}", e)))?;
match value {
Some(v) => Ok(ExpectationConfig::from_bytes(&v)),
None => Ok(None),
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ExpectationConfig {
pub allowed_protocols: u64,
pub fragment_policy: u8,
pub min_ttl: u8,
pub _pad: [u8; 2],
pub tcp_port_count: u16,
pub tcp_ports: [u16; 16],
pub udp_port_count: u16,
pub udp_ports: [u16; 16],
}
impl ExpectationConfig {
pub const SIZE: usize = 80;
pub fn default_allow_all() -> Self {
Self {
allowed_protocols: zenith_foundation::net::DEFAULT_PROTO_WHITELIST,
fragment_policy: 0,
min_ttl: 0,
_pad: [0, 0],
tcp_port_count: 0,
tcp_ports: [0; 16],
udp_port_count: 0,
udp_ports: [0; 16],
}
}
pub fn to_bytes(&self) -> [u8; Self::SIZE] {
let mut buf = [0u8; Self::SIZE];
buf[0..8].copy_from_slice(&self.allowed_protocols.to_ne_bytes());
buf[8] = self.fragment_policy;
buf[9] = self.min_ttl;
buf[12..14].copy_from_slice(&self.tcp_port_count.to_ne_bytes());
for (i, &port) in self.tcp_ports.iter().enumerate() {
let offset = 14 + i * 2;
buf[offset..offset + 2].copy_from_slice(&port.to_ne_bytes());
}
let udp_count_offset = 14 + 16 * 2; buf[udp_count_offset..udp_count_offset + 2]
.copy_from_slice(&self.udp_port_count.to_ne_bytes());
for (i, &port) in self.udp_ports.iter().enumerate() {
let offset = udp_count_offset + 2 + i * 2;
buf[offset..offset + 2].copy_from_slice(&port.to_ne_bytes());
}
buf
}
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
if buf.len() < Self::SIZE {
return None;
}
let allowed_protocols = u64::from_ne_bytes(buf[0..8].try_into().ok()?);
let fragment_policy = buf[8];
let min_ttl = buf[9];
let tcp_port_count = u16::from_ne_bytes(buf[12..14].try_into().ok()?);
let mut tcp_ports = [0u16; 16];
for i in 0..16 {
let offset = 14 + i * 2;
tcp_ports[i] = u16::from_ne_bytes(buf[offset..offset + 2].try_into().ok()?);
}
let udp_count_offset = 46;
let udp_port_count = u16::from_ne_bytes(
buf[udp_count_offset..udp_count_offset + 2].try_into().ok()?,
);
let mut udp_ports = [0u16; 16];
for i in 0..16 {
let offset = udp_count_offset + 2 + i * 2;
udp_ports[i] = u16::from_ne_bytes(buf[offset..offset + 2].try_into().ok()?);
}
Some(Self {
allowed_protocols,
fragment_policy,
min_ttl,
_pad: [0, 0],
tcp_port_count,
tcp_ports,
udp_port_count,
udp_ports,
})
}
}
impl Default for ExpectationConfig {
fn default() -> Self {
Self::default_allow_all()
}
}
impl<'obj> std::fmt::Debug for BpfMaps<'obj> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let get_name = |map: &Rc<libbpf_rs::Map<'_>>| -> String {
map.info()
.map(|i| {
let bytes = i.info.name.iter()
.take_while(|&&b| b != 0)
.map(|&b| b as u8)
.collect::<Vec<_>>();
String::from_utf8_lossy(&bytes).to_string()
})
.unwrap_or_else(|_| "unknown".to_string())
};
f.debug_struct("BpfMaps")
.field("xsk_map", &get_name(&self.xsk_map))
.field("stats_map", &get_name(&self.stats_map))
.field("config_map", &get_name(&self.config_map))
.field("expectation_map", &get_name(&self.expectation_map))
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum StatId {
RxPackets = 0,
RxValid = 1,
DropShort = 2,
DropBadEth = 3,
DropBadIp = 4,
DropNoXsk = 5,
Redirected = 6,
DropProto = 7,
}
impl StatId {
pub fn as_u32(&self) -> u32 {
*self as u32
}
pub fn from_u32(value: u32) -> Option<Self> {
match value {
0 => Some(Self::RxPackets),
1 => Some(Self::RxValid),
2 => Some(Self::DropShort),
3 => Some(Self::DropBadEth),
4 => Some(Self::DropBadIp),
5 => Some(Self::DropNoXsk),
6 => Some(Self::Redirected),
7 => Some(Self::DropProto),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::RxPackets => "rx_packets",
Self::RxValid => "rx_valid",
Self::DropShort => "drop_short",
Self::DropBadEth => "drop_bad_eth",
Self::DropBadIp => "drop_bad_ip",
Self::DropNoXsk => "drop_no_xsk",
Self::Redirected => "redirected",
Self::DropProto => "drop_proto",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stat_id_matches_kernel_layout() {
const KERNEL_STATS_RX_PACKETS: u32 = 0;
const KERNEL_STATS_RX_VALID: u32 = 1;
const KERNEL_STATS_RX_DROP_SHORT: u32 = 2;
const KERNEL_STATS_RX_DROP_BAD_ETH: u32 = 3;
const KERNEL_STATS_RX_DROP_BAD_IP: u32 = 4;
const KERNEL_STATS_RX_DROP_NO_XSK: u32 = 5;
const KERNEL_STATS_REDIRECTED: u32 = 6;
const KERNEL_STATS_RX_DROP_PROTO: u32 = 7;
assert_eq!(StatId::RxPackets.as_u32(), KERNEL_STATS_RX_PACKETS);
assert_eq!(StatId::RxValid.as_u32(), KERNEL_STATS_RX_VALID);
assert_eq!(StatId::DropShort.as_u32(), KERNEL_STATS_RX_DROP_SHORT);
assert_eq!(StatId::DropBadEth.as_u32(), KERNEL_STATS_RX_DROP_BAD_ETH);
assert_eq!(StatId::DropBadIp.as_u32(), KERNEL_STATS_RX_DROP_BAD_IP);
assert_eq!(StatId::DropNoXsk.as_u32(), KERNEL_STATS_RX_DROP_NO_XSK);
assert_eq!(StatId::Redirected.as_u32(), KERNEL_STATS_REDIRECTED);
assert_eq!(StatId::DropProto.as_u32(), KERNEL_STATS_RX_DROP_PROTO);
}
#[test]
fn test_stat_id_count() {
let stats = [StatId::RxPackets,
StatId::RxValid,
StatId::DropShort,
StatId::DropBadEth,
StatId::DropBadIp,
StatId::DropNoXsk,
StatId::Redirected,
StatId::DropProto];
assert_eq!(stats.len(), 8);
}
#[test]
fn test_stat_id_clone_copy() {
let s1 = StatId::RxPackets;
let s2 = s1;
assert_eq!(s1, s2);
assert_eq!(s1.as_u32(), s2.as_u32());
}
#[test]
fn test_stat_id_debug() {
let s = StatId::DropShort;
let debug_str = format!("{:?}", s);
assert!(!debug_str.is_empty());
}
#[test]
fn test_stat_id_unique() {
let stats = [
StatId::RxPackets,
StatId::RxValid,
StatId::DropShort,
StatId::DropBadEth,
StatId::DropBadIp,
StatId::DropNoXsk,
StatId::Redirected,
StatId::DropProto,
];
for (i, a) in stats.iter().enumerate() {
for (j, b) in stats.iter().enumerate() {
if i != j {
assert_ne!(a, b);
} else {
assert_eq!(a, b);
}
}
}
}
#[test]
fn test_stat_id_eq() {
assert_eq!(StatId::RxPackets, StatId::RxPackets);
assert_ne!(StatId::RxPackets, StatId::RxValid);
}
#[test]
fn test_map_error_display() {
use crate::error::MapError;
let e = MapError::NotFound("test_map".to_string());
assert!(format!("{}", e).contains("Map not found"));
let e = MapError::SizeMismatch { expected: 8, actual: 4 };
let msg = format!("{}", e);
assert!(msg.contains("Size mismatch"));
assert!(msg.contains("8"));
}
#[test]
fn test_u32_byte_conversion() {
let values: Vec<u32> = vec![0, 1, 255, 256, 65535, 65536, u32::MAX];
for v in values {
let bytes = v.to_ne_bytes();
let back = u32::from_ne_bytes(bytes);
assert_eq!(v, back);
}
}
#[test]
fn test_u64_byte_conversion() {
let values: Vec<u64> = vec![0, 1, 255, 256, 65535, 65536, u64::MAX];
for v in values {
let bytes = v.to_ne_bytes();
let back = u64::from_ne_bytes(bytes);
assert_eq!(v, back);
}
}
#[test]
fn test_stat_id_as_u32_consistency() {
let pairs = [
(StatId::RxPackets, 0u32),
(StatId::RxValid, 1u32),
(StatId::DropShort, 2u32),
(StatId::DropBadEth, 3u32),
(StatId::DropBadIp, 4u32),
(StatId::DropNoXsk, 5u32),
(StatId::Redirected, 6u32),
(StatId::DropProto, 7u32),
];
for (stat_id, expected) in pairs.iter() {
assert_eq!(stat_id.as_u32(), *expected);
}
}
#[test]
fn test_stat_id_ordering() {
assert!(StatId::RxPackets.as_u32() < StatId::RxValid.as_u32());
assert!(StatId::RxValid.as_u32() < StatId::DropShort.as_u32());
assert!(StatId::DropProto.as_u32() > StatId::Redirected.as_u32());
}
#[test]
fn test_map_error_is_error() {
use crate::error::MapError;
fn assert_error<T: std::error::Error>() {}
assert_error::<MapError>();
}
#[test]
fn test_stat_id_variants_coverage() {
let all = [
StatId::RxPackets,
StatId::RxValid,
StatId::DropShort,
StatId::DropBadEth,
StatId::DropBadIp,
StatId::DropNoXsk,
StatId::Redirected,
StatId::DropProto,
];
for (i, stat) in all.iter().enumerate() {
assert_eq!(stat.as_u32(), i as u32);
}
}
#[test]
fn test_stat_id_as_str_semantics() {
assert_eq!(StatId::RxPackets.as_str(), "rx_packets");
assert_eq!(StatId::RxValid.as_str(), "rx_valid");
assert_eq!(StatId::DropShort.as_str(), "drop_short");
assert_eq!(StatId::DropBadEth.as_str(), "drop_bad_eth");
assert_eq!(StatId::DropBadIp.as_str(), "drop_bad_ip");
assert_eq!(StatId::DropNoXsk.as_str(), "drop_no_xsk");
assert_eq!(StatId::Redirected.as_str(), "redirected");
assert_eq!(StatId::DropProto.as_str(), "drop_proto");
}
#[test]
fn test_stat_id_from_u32_roundtrip() {
let all = [
StatId::RxPackets,
StatId::RxValid,
StatId::DropShort,
StatId::DropBadEth,
StatId::DropBadIp,
StatId::DropNoXsk,
StatId::Redirected,
StatId::DropProto,
];
for stat in all {
let back = StatId::from_u32(stat.as_u32());
assert_eq!(back, Some(stat));
assert!(!stat.as_str().is_empty());
}
assert_eq!(StatId::from_u32(8), None);
assert_eq!(StatId::from_u32(u32::MAX), None);
}
#[test]
fn test_proto_whitelist_bitmap() {
let bitmap = BpfMaps::proto_whitelist_bitmap(&[
BpfMaps::IPPROTO_TCP,
BpfMaps::IPPROTO_UDP,
BpfMaps::IPPROTO_ICMP,
BpfMaps::IPPROTO_ICMPV6,
]);
assert!(bitmap.is_ok());
let bitmap = bitmap.unwrap_or(0);
assert_eq!(bitmap, BpfMaps::DEFAULT_PROTO_WHITELIST);
assert_ne!(bitmap & (1u64 << BpfMaps::IPPROTO_TCP), 0);
assert_ne!(bitmap & (1u64 << BpfMaps::IPPROTO_UDP), 0);
assert_ne!(bitmap & (1u64 << BpfMaps::IPPROTO_ICMP), 0);
assert_ne!(bitmap & (1u64 << BpfMaps::IPPROTO_ICMPV6), 0);
assert_eq!(bitmap & (1u64 << 47u8), 0);
}
#[test]
fn test_proto_whitelist_bitmap_empty() {
let bitmap = BpfMaps::proto_whitelist_bitmap(&[]);
assert_eq!(bitmap.ok(), Some(0u64));
}
#[test]
fn test_proto_whitelist_bitmap_overflow_fails() {
let result = BpfMaps::proto_whitelist_bitmap(&[64]);
assert!(result.is_err());
let result = BpfMaps::proto_whitelist_bitmap(&[BpfMaps::IPPROTO_TCP, 255]);
assert!(result.is_err());
}
#[test]
fn test_default_proto_whitelist_value() {
let expected = (1u64 << 6) | (1u64 << 17) | (1u64 << 1) | (1u64 << 58);
assert_eq!(BpfMaps::DEFAULT_PROTO_WHITELIST, expected);
}
#[test]
fn test_expectation_default_matches_unified_whitelist() {
let cfg = ExpectationConfig::default_allow_all();
assert_eq!(
cfg.allowed_protocols,
BpfMaps::DEFAULT_PROTO_WHITELIST,
"ExpectationConfig 默认 allowed_protocols 必须与统一白名单一致(含 ICMP)"
);
assert_eq!(cfg.allowed_protocols, zenith_foundation::net::DEFAULT_PROTO_WHITELIST);
}
#[test]
fn test_whitelist_enabled_config_key_value() {
assert_eq!(BpfMaps::CONFIG_KEY_WHITELIST_ENABLED, 3);
}
}