use std::net::IpAddr;
use tracing::warn;
#[derive(Debug, Clone, PartialEq)]
pub enum AclAction {
Allow,
Deny,
Refuse,
}
struct CidrBlock {
prefix: IpAddr,
prefix_len: u8,
}
impl CidrBlock {
fn parse(s: &str) -> Option<Self> {
let (ip_str, prefix_len) = if let Some(pos) = s.find('/') {
let len: u8 = s[pos + 1..].parse().ok()?;
(&s[..pos], len)
} else {
let ip: IpAddr = s.parse().ok()?;
let len = match ip { IpAddr::V4(_) => 32, IpAddr::V6(_) => 128 };
(s, len)
};
let prefix: IpAddr = ip_str.parse().ok()?;
Some(CidrBlock { prefix, prefix_len })
}
#[inline]
fn contains(&self, ip: IpAddr) -> bool {
match (self.prefix, ip) {
(IpAddr::V4(net), IpAddr::V4(addr)) => {
if self.prefix_len == 0 { return true; }
let shift = 32u8.saturating_sub(self.prefix_len);
let mask = !0u32 << shift;
u32::from(net) & mask == u32::from(addr) & mask
}
(IpAddr::V6(net), IpAddr::V6(addr)) => {
if self.prefix_len == 0 { return true; }
let shift = 128u8.saturating_sub(self.prefix_len);
let mask = !0u128 << shift;
u128::from(net) & mask == u128::from(addr) & mask
}
_ => false,
}
}
}
pub struct PrivateAddressSet(Vec<CidrBlock>);
impl PrivateAddressSet {
pub fn from_config(cidrs: &[String]) -> Self {
let parsed = cidrs.iter()
.filter_map(|s| {
CidrBlock::parse(s.trim()).or_else(|| {
warn!(cidr=%s, "private-address: parse error — ignored");
None
})
})
.collect();
Self(parsed)
}
pub fn is_empty(&self) -> bool { self.0.is_empty() }
#[inline]
pub fn contains(&self, ip: IpAddr) -> bool {
self.0.iter().any(|b| b.contains(ip))
}
}
struct AclEntry {
cidr: CidrBlock,
action: AclAction,
}
impl AclEntry {
fn parse(s: &str) -> Option<Self> {
let mut parts = s.split_whitespace();
let net_str = parts.next()?;
let action_str = parts.next()?;
let action = match action_str {
"allow" | "allow_snoop" | "allow_setrd" => AclAction::Allow,
"deny" | "deny_non_local" => AclAction::Deny,
"refuse"| "refuse_non_local" => AclAction::Refuse,
_ => return None,
};
let cidr = CidrBlock::parse(net_str)?;
Some(AclEntry { cidr, action })
}
#[inline]
fn matches(&self, ip: IpAddr) -> bool { self.cidr.contains(ip) }
}
pub struct Acl(Vec<AclEntry>);
impl Acl {
pub fn from_config(entries: &[String]) -> Self {
let parsed = entries.iter()
.filter_map(|s| {
AclEntry::parse(s).or_else(|| {
warn!(entry=%s, "access-control: parse error — ignored");
None
})
})
.collect();
Self(parsed)
}
pub fn is_empty(&self) -> bool { self.0.is_empty() }
pub fn len(&self) -> usize { self.0.len() }
#[inline]
pub fn check(&self, ip: IpAddr) -> AclAction {
if self.0.is_empty() { return AclAction::Allow; }
let ip = match ip {
IpAddr::V6(v6) => v6.to_ipv4_mapped()
.map(IpAddr::V4)
.unwrap_or(IpAddr::V6(v6)),
_ => ip,
};
for entry in &self.0 {
if entry.matches(ip) {
return entry.action.clone();
}
}
AclAction::Refuse }
}