use std::net::IpAddr;
fn canonicalize_ip(ip: IpAddr) -> IpAddr {
match ip {
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
IpAddr::V4(v4)
} else {
ip
}
}
_ => ip,
}
}
#[derive(Debug, Clone)]
struct CidrEntry {
addr: IpAddr,
prefix_len: u8,
}
impl CidrEntry {
fn contains(&self, ip: IpAddr) -> bool {
let ip = canonicalize_ip(ip);
match (self.addr, ip) {
(IpAddr::V4(net), IpAddr::V4(addr)) => {
if self.prefix_len == 0 {
return true;
}
let mask = u32::MAX
.checked_shl(32 - self.prefix_len as u32)
.unwrap_or(0);
u32::from(net) & mask == u32::from(addr) & mask
}
(IpAddr::V6(net), IpAddr::V6(addr)) => {
if self.prefix_len == 0 {
return true;
}
let mask = u128::MAX
.checked_shl(128 - self.prefix_len as u32)
.unwrap_or(0);
u128::from(net) & mask == u128::from(addr) & mask
}
_ => false, }
}
}
#[derive(Debug, Clone)]
pub struct RecursionAcl {
entries: Vec<CidrEntry>,
}
impl RecursionAcl {
pub fn from_cidrs(cidrs: &[String]) -> Self {
let entries = cidrs
.iter()
.filter_map(|s| {
let s = s.trim();
if s.is_empty() {
return None;
}
let (addr_str, prefix_len) = if let Some(idx) = s.find('/') {
let prefix: u8 = s[idx + 1..].parse().ok()?;
(&s[..idx], prefix)
} else {
let addr: IpAddr = s.parse().ok()?;
let max_prefix = if addr.is_ipv4() { 32 } else { 128 };
return Some(CidrEntry {
addr,
prefix_len: max_prefix,
});
};
let addr: IpAddr = addr_str.parse().ok()?;
let max_prefix = if addr.is_ipv4() { 32 } else { 128 };
if prefix_len > max_prefix {
tracing::warn!(cidr = %s, "Invalid prefix length {} for {} address (max {})", prefix_len, if addr.is_ipv4() { "IPv4" } else { "IPv6" }, max_prefix);
return None;
}
Some(CidrEntry { addr, prefix_len })
})
.collect();
Self { entries }
}
#[inline]
pub fn is_allowed(&self, ip: IpAddr) -> bool {
if self.entries.is_empty() {
return true;
}
let ip = canonicalize_ip(ip);
self.entries.iter().any(|e| e.contains(ip))
}
pub fn is_configured(&self) -> bool {
!self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn test_empty_acl_allows_all() {
let acl = RecursionAcl::from_cidrs(&[]);
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
assert!(acl.is_allowed(IpAddr::V6(Ipv6Addr::LOCALHOST)));
}
#[test]
fn test_cidr_matching() {
let acl = RecursionAcl::from_cidrs(&[
"10.0.0.0/8".to_string(),
"192.168.1.0/24".to_string(),
"::1/128".to_string(),
]);
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 255, 255, 255))));
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))));
assert!(acl.is_allowed(IpAddr::V6(Ipv6Addr::LOCALHOST)));
assert!(!acl.is_allowed(IpAddr::V4(Ipv4Addr::new(11, 0, 0, 1))));
assert!(!acl.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 2, 1))));
assert!(!acl.is_allowed(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));
}
#[test]
fn test_host_entry() {
let acl = RecursionAcl::from_cidrs(&["127.0.0.1".to_string()]);
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
assert!(!acl.is_allowed(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2))));
}
#[test]
fn test_ipv6_cidr() {
let acl = RecursionAcl::from_cidrs(&["fd00::/8".to_string()]);
assert!(acl.is_allowed("fd00::1".parse().unwrap()));
assert!(acl.is_allowed("fdff::1".parse().unwrap()));
assert!(!acl.is_allowed("fe80::1".parse().unwrap()));
}
}