use crate::error::{NetError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdmissionAction {
Allow,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IpAddr {
V4([u8; 4]),
V6([u8; 16]),
Any,
}
impl IpAddr {
pub const V4_WILDCARD: Self = Self::V4([0, 0, 0, 0]);
pub const V6_WILDCARD: Self = Self::V6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
pub const ANY: Self = Self::Any;
#[inline]
pub fn is_v4(&self) -> bool {
matches!(self, Self::V4(_))
}
#[inline]
pub fn is_v6(&self) -> bool {
matches!(self, Self::V6(_))
}
#[inline]
pub fn as_v4(&self) -> Option<[u8; 4]> {
match self {
Self::V4(bytes) => Some(*bytes),
Self::V6(_) | Self::Any => None,
}
}
#[inline]
pub fn as_v6(&self) -> Option<[u8; 16]> {
match self {
Self::V6(bytes) => Some(*bytes),
Self::V4(_) | Self::Any => None,
}
}
#[inline]
pub fn is_wildcard(&self) -> bool {
match self {
Self::V4(b) => *b == [0, 0, 0, 0],
Self::V6(b) => *b == [0u8; 16],
Self::Any => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtoMatch {
Tcp,
Udp,
Icmp,
Any,
}
impl ProtoMatch {
#[inline]
pub fn matches(&self, proto: u8) -> bool {
match self {
Self::Tcp => proto == 6,
Self::Udp => proto == 17,
Self::Icmp => proto == 1 || proto == 58,
Self::Any => true,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct AdmissionRule {
pub id: u32,
pub src_ip: IpAddr,
pub prefix_len: u8,
pub src_port: u16,
pub dst_port: u16,
pub proto: ProtoMatch,
pub action: AdmissionAction,
pub enabled: bool,
}
#[inline]
fn ip_subnet_matches(rule_ip: IpAddr, prefix_len: u8, src_ip: IpAddr) -> bool {
match rule_ip {
IpAddr::Any => true,
IpAddr::V4(rb) => match src_ip {
IpAddr::V4(sb) => {
if rb == [0, 0, 0, 0] {
return true;
}
let plen = prefix_len.min(32) as u32;
if plen == 0 {
return rb == sb;
}
let r = u32::from_be_bytes(rb);
let s = u32::from_be_bytes(sb);
r >> (32 - plen) == s >> (32 - plen)
}
IpAddr::V6(_) | IpAddr::Any => false,
},
IpAddr::V6(rb) => match src_ip {
IpAddr::V6(sb) => {
if rb == [0u8; 16] {
return true;
}
let plen = prefix_len.min(128) as u32;
if plen == 0 {
return rb == sb;
}
let r = u128::from_be_bytes(rb);
let s = u128::from_be_bytes(sb);
r >> (128 - plen) == s >> (128 - plen)
}
IpAddr::V4(_) | IpAddr::Any => false,
},
}
}
impl AdmissionRule {
#[inline]
pub const fn default_allow(id: u32) -> Self {
Self {
id,
src_ip: IpAddr::V4_WILDCARD,
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
}
}
#[inline]
pub const fn default_deny(id: u32) -> Self {
Self {
id,
src_ip: IpAddr::V4_WILDCARD,
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Deny,
enabled: true,
}
}
#[inline]
pub fn matches(
&self,
src_ip: IpAddr,
src_port: u16,
dst_port: u16,
proto: u8,
) -> bool {
if !self.enabled {
return false;
}
if !self.proto.matches(proto) {
return false;
}
if !ip_subnet_matches(self.src_ip, self.prefix_len, src_ip) {
return false;
}
if self.src_port != 0 && self.src_port != src_port {
return false;
}
if self.dst_port != 0 && self.dst_port != dst_port {
return false;
}
true
}
}
const MAX_RULES: usize = 64;
#[derive(Debug)]
pub struct SourceAdmissionEngine {
rules: [AdmissionRule; MAX_RULES],
rule_count: usize,
default_action: AdmissionAction,
}
impl SourceAdmissionEngine {
pub fn new(default_action: AdmissionAction) -> Self {
Self {
rules: [AdmissionRule::default_allow(0); MAX_RULES],
rule_count: 0,
default_action,
}
}
pub fn add_rule(&mut self, rule: AdmissionRule) -> Result<()> {
if self.rule_count >= MAX_RULES {
return Err(NetError::Internal("admission rule table full"));
}
self.rules[self.rule_count] = rule;
self.rule_count += 1;
Ok(())
}
pub fn add_rules(&mut self, rules: &[AdmissionRule]) -> Result<()> {
let new_count = self
.rule_count
.checked_add(rules.len())
.ok_or(NetError::Internal("admission rule count overflow"))?;
if new_count > MAX_RULES {
return Err(NetError::Internal("admission rule table overflow"));
}
for rule in rules {
self.rules[self.rule_count] = *rule;
self.rule_count += 1;
}
Ok(())
}
pub fn remove_rule(&mut self, rule_id: u32) -> Result<()> {
for i in 0..self.rule_count {
if self.rules[i].id == rule_id {
self.rules[i..self.rule_count].rotate_left(1);
self.rule_count -= 1;
return Ok(());
}
}
Err(NetError::Internal("rule not found"))
}
#[inline]
pub fn evaluate(
&self,
src_ip: IpAddr,
src_port: u16,
dst_port: u16,
proto: u8,
) -> AdmissionAction {
for i in 0..self.rule_count {
let rule = &self.rules[i];
if rule.matches(src_ip, src_port, dst_port, proto) {
return rule.action;
}
}
self.default_action
}
#[inline]
pub fn rule_count(&self) -> usize {
self.rule_count
}
#[inline]
pub fn max_rules(&self) -> usize {
MAX_RULES
}
#[inline]
pub fn default_action(&self) -> AdmissionAction {
self.default_action
}
pub fn clear(&mut self) {
self.rule_count = 0;
}
pub fn allow_all() -> Self {
Self::new(AdmissionAction::Allow)
}
pub fn deny_all() -> Self {
Self::new(AdmissionAction::Deny)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_allow_all() {
let engine = SourceAdmissionEngine::allow_all();
let action = engine.evaluate(
IpAddr::V4([192, 168, 1, 1]),
8080,
443,
6,
);
assert_eq!(action, AdmissionAction::Allow);
}
#[test]
fn test_deny_all() {
let engine = SourceAdmissionEngine::deny_all();
let action = engine.evaluate(
IpAddr::V4([10, 0, 0, 1]),
0,
0,
17,
);
assert_eq!(action, AdmissionAction::Deny);
}
#[test]
fn test_ip_whitelist() {
let mut engine = SourceAdmissionEngine::deny_all();
engine
.add_rule(AdmissionRule {
id: 1,
src_ip: IpAddr::V4([192, 168, 1, 100]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
let action = engine.evaluate(
IpAddr::V4([192, 168, 1, 100]),
0,
0,
6,
);
assert_eq!(action, AdmissionAction::Allow);
let action = engine.evaluate(
IpAddr::V4([10, 0, 0, 1]),
0,
0,
6,
);
assert_eq!(action, AdmissionAction::Deny);
}
#[test]
fn test_port_rule() {
let mut engine = SourceAdmissionEngine::deny_all();
engine
.add_rule(AdmissionRule {
id: 1,
src_ip: IpAddr::V4_WILDCARD,
prefix_len: 0,
src_port: 0,
dst_port: 8080,
proto: ProtoMatch::Tcp,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
let action = engine.evaluate(
IpAddr::V4([10, 0, 0, 1]),
0,
8080,
6,
);
assert_eq!(action, AdmissionAction::Allow);
let action = engine.evaluate(
IpAddr::V4([10, 0, 0, 1]),
0,
8080,
17,
);
assert_eq!(action, AdmissionAction::Deny);
let action = engine.evaluate(
IpAddr::V4([10, 0, 0, 1]),
0,
443,
6,
);
assert_eq!(action, AdmissionAction::Deny);
}
#[test]
fn test_rule_priority() {
let mut engine = SourceAdmissionEngine::deny_all();
engine
.add_rule(AdmissionRule {
id: 1,
src_ip: IpAddr::V4_WILDCARD,
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
engine
.add_rule(AdmissionRule {
id: 2,
src_ip: IpAddr::V4([10, 0, 0, 1]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Deny,
enabled: true,
})
.unwrap();
let action = engine.evaluate(
IpAddr::V4([10, 0, 0, 1]),
0,
0,
6,
);
assert_eq!(action, AdmissionAction::Allow);
}
#[test]
fn test_remove_rule() {
let mut engine = SourceAdmissionEngine::deny_all();
engine
.add_rule(AdmissionRule {
id: 1,
src_ip: IpAddr::V4([10, 0, 0, 1]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
assert_eq!(engine.rule_count(), 1);
engine.remove_rule(1).unwrap();
assert_eq!(engine.rule_count(), 0);
let action = engine.evaluate(
IpAddr::V4([10, 0, 0, 1]),
0,
0,
6,
);
assert_eq!(action, AdmissionAction::Deny);
}
#[test]
fn test_max_rules() {
let mut engine = SourceAdmissionEngine::new(AdmissionAction::Deny);
for i in 0..MAX_RULES {
engine
.add_rule(AdmissionRule {
id: i as u32,
src_ip: IpAddr::V4([127, 0, 0, (i % 256) as u8]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
}
let result = engine.add_rule(AdmissionRule::default_allow(100));
assert!(result.is_err());
}
#[test]
fn test_add_rules_batch_overflow() {
let mut engine = SourceAdmissionEngine::new(AdmissionAction::Deny);
let rules: Vec<AdmissionRule> = (0..MAX_RULES + 1)
.map(|i| AdmissionRule {
id: i as u32,
src_ip: IpAddr::V4([127, 0, 0, (i % 256) as u8]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.collect();
let result = engine.add_rules(&rules);
assert!(result.is_err(), "应拒绝超出容量的批量规则");
assert_eq!(engine.rule_count(), 0, "失败后不应部分插入");
}
#[test]
fn test_add_rules_batch_success() {
let mut engine = SourceAdmissionEngine::new(AdmissionAction::Deny);
let rules: Vec<AdmissionRule> = (0..10)
.map(|i| AdmissionRule {
id: i as u32,
src_ip: IpAddr::V4([127, 0, 0, (i % 256) as u8]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.collect();
assert!(engine.add_rules(&rules).is_ok());
assert_eq!(engine.rule_count(), 10);
}
#[test]
fn test_disabled_rule() {
let mut engine = SourceAdmissionEngine::deny_all();
engine
.add_rule(AdmissionRule {
id: 1,
src_ip: IpAddr::V4([10, 0, 0, 1]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: false, })
.unwrap();
let action = engine.evaluate(
IpAddr::V4([10, 0, 0, 1]),
0,
0,
6,
);
assert_eq!(action, AdmissionAction::Deny);
}
#[test]
fn test_empty_ruleset_default_allow() {
let engine = SourceAdmissionEngine::allow_all();
assert_eq!(engine.rule_count(), 0);
assert_eq!(
engine.evaluate(IpAddr::V4([1, 2, 3, 4]), 1234, 80, 6),
AdmissionAction::Allow
);
}
#[test]
fn test_empty_ruleset_default_deny() {
let engine = SourceAdmissionEngine::deny_all();
assert_eq!(engine.rule_count(), 0);
assert_eq!(
engine.evaluate(IpAddr::V4([1, 2, 3, 4]), 1234, 80, 6),
AdmissionAction::Deny
);
}
#[test]
fn test_clear_rules() {
let mut engine = SourceAdmissionEngine::deny_all();
for i in 0..10 {
engine
.add_rule(AdmissionRule {
id: i,
src_ip: IpAddr::V4_WILDCARD,
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
}
assert_eq!(engine.rule_count(), 10);
engine.clear();
assert_eq!(engine.rule_count(), 0);
assert_eq!(
engine.evaluate(IpAddr::V4([10, 0, 0, 1]), 0, 0, 6),
AdmissionAction::Deny
);
}
#[test]
fn test_ipv6_addresses() {
let mut engine = SourceAdmissionEngine::deny_all();
engine
.add_rule(AdmissionRule {
id: 1,
src_ip: IpAddr::V6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
assert_eq!(
engine.evaluate(
IpAddr::V6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
0,
0,
6
),
AdmissionAction::Allow
);
assert_eq!(
engine.evaluate(
IpAddr::V6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]),
0,
0,
6
),
AdmissionAction::Deny
);
}
#[test]
fn test_boundary_ipv4_addresses() {
let mut engine = SourceAdmissionEngine::deny_all();
engine
.add_rule(AdmissionRule {
id: 1,
src_ip: IpAddr::V4([0, 0, 0, 0]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
engine
.add_rule(AdmissionRule {
id: 2,
src_ip: IpAddr::V4([255, 255, 255, 255]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
assert_eq!(
engine.evaluate(IpAddr::V4([0, 0, 0, 0]), 0, 0, 6),
AdmissionAction::Allow
);
assert_eq!(
engine.evaluate(IpAddr::V4([255, 255, 255, 255]), 0, 0, 6),
AdmissionAction::Allow
);
}
#[test]
fn test_ip_addr_methods() {
let v4 = IpAddr::V4([192, 168, 1, 1]);
assert!(v4.is_v4());
assert!(!v4.is_v6());
assert_eq!(v4.as_v4(), Some([192, 168, 1, 1]));
assert_eq!(v4.as_v6(), None);
assert!(!v4.is_wildcard());
let v4_wild = IpAddr::V4_WILDCARD;
assert!(v4_wild.is_wildcard());
let v6 = IpAddr::V6([0u8; 16]);
assert!(v6.is_v6());
assert!(!v6.is_v4());
assert!(v6.is_wildcard());
assert_eq!(v6.as_v6(), Some([0u8; 16]));
}
#[test]
fn test_proto_match_variants() {
assert!(ProtoMatch::Tcp.matches(6));
assert!(!ProtoMatch::Tcp.matches(17));
assert!(ProtoMatch::Udp.matches(17));
assert!(!ProtoMatch::Udp.matches(6));
assert!(ProtoMatch::Icmp.matches(1));
assert!(ProtoMatch::Icmp.matches(58));
assert!(!ProtoMatch::Icmp.matches(6));
assert!(ProtoMatch::Any.matches(6));
assert!(ProtoMatch::Any.matches(17));
assert!(ProtoMatch::Any.matches(99));
}
#[test]
fn test_port_boundaries() {
let mut engine = SourceAdmissionEngine::deny_all();
engine
.add_rule(AdmissionRule {
id: 1,
src_ip: IpAddr::V4_WILDCARD,
prefix_len: 0,
src_port: 0,
dst_port: 80,
proto: ProtoMatch::Tcp,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
assert_eq!(
engine.evaluate(IpAddr::V4([10, 0, 0, 1]), 12345, 80, 6),
AdmissionAction::Allow
);
assert_eq!(
engine.evaluate(IpAddr::V4([10, 0, 0, 1]), 0, 80, 6),
AdmissionAction::Allow
);
assert_eq!(
engine.evaluate(IpAddr::V4([10, 0, 0, 1]), 12345, 81, 6),
AdmissionAction::Deny
);
}
#[test]
fn test_max_rules_boundary() {
let mut engine = SourceAdmissionEngine::new(AdmissionAction::Deny);
assert_eq!(engine.max_rules(), MAX_RULES);
for i in 0..MAX_RULES {
engine
.add_rule(AdmissionRule {
id: i as u32,
src_ip: IpAddr::V4([127, 0, 0, (i % 256) as u8]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.unwrap();
}
assert_eq!(engine.rule_count(), MAX_RULES);
let result = engine.add_rule(AdmissionRule::default_allow(9999));
assert!(result.is_err());
}
#[test]
fn test_remove_nonexistent_rule() {
let mut engine = SourceAdmissionEngine::allow_all();
let result = engine.remove_rule(999);
assert!(result.is_err());
}
#[test]
fn test_add_rules_batch_partial_overflow() {
let mut engine = SourceAdmissionEngine::new(AdmissionAction::Deny);
let rules: Vec<AdmissionRule> = (0..MAX_RULES - 5)
.map(|i| AdmissionRule {
id: i as u32,
src_ip: IpAddr::V4([127, 0, 0, (i % 256) as u8]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.collect();
assert!(engine.add_rules(&rules).is_ok());
assert_eq!(engine.rule_count(), MAX_RULES - 5);
let extra: Vec<AdmissionRule> = (0..10)
.map(|i| AdmissionRule {
id: (MAX_RULES + i) as u32,
src_ip: IpAddr::V4([10, 0, 0, i as u8]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
})
.collect();
let result = engine.add_rules(&extra);
assert!(result.is_err());
assert_eq!(engine.rule_count(), MAX_RULES - 5);
}
#[test]
fn test_cidr_subnet_matching() {
let rule = AdmissionRule {
id: 1,
src_ip: IpAddr::V4([192, 168, 1, 0]),
prefix_len: 24,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
};
assert!(rule.matches(IpAddr::V4([192, 168, 1, 200]), 0, 0, 6));
assert!(!rule.matches(IpAddr::V4([192, 168, 2, 1]), 0, 0, 6));
assert!(!rule.matches(
IpAddr::V6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
0,
0,
6
));
let exact = AdmissionRule {
id: 2,
src_ip: IpAddr::V4([10, 0, 0, 1]),
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
};
assert!(exact.matches(IpAddr::V4([10, 0, 0, 1]), 0, 0, 6));
assert!(!exact.matches(IpAddr::V4([10, 0, 0, 2]), 0, 0, 6));
}
#[test]
fn test_cross_family_wildcard_any() {
let rule = AdmissionRule {
id: 1,
src_ip: IpAddr::Any,
prefix_len: 0,
src_port: 0,
dst_port: 0,
proto: ProtoMatch::Any,
action: AdmissionAction::Allow,
enabled: true,
};
assert!(rule.matches(IpAddr::V4([1, 2, 3, 4]), 0, 0, 6));
assert!(rule.matches(
IpAddr::V6([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
0,
0,
6
));
assert!(IpAddr::Any.is_wildcard());
}
}