use std::net::IpAddr;
use std::path::Path;
use ipnetwork::IpNetwork;
fn normalize_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, Default)]
pub struct Acl {
allow: Vec<IpNetwork>,
deny: Vec<IpNetwork>,
}
impl Acl {
pub fn new() -> Self {
Self::default()
}
pub fn from_rules(allow: Vec<String>, deny: Vec<String>) -> anyhow::Result<Self> {
let allow = allow
.into_iter()
.map(|s| s.parse::<IpNetwork>())
.collect::<Result<Vec<_>, _>>()?;
let deny = deny
.into_iter()
.map(|s| s.parse::<IpNetwork>())
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { allow, deny })
}
pub fn from_file(path: &Path) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut allow = Vec::new();
let mut deny = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() != 2 {
anyhow::bail!("Invalid ACL line: {}", line);
}
let network: IpNetwork = parts[1].parse()?;
match parts[0].to_lowercase().as_str() {
"allow" => allow.push(network),
"deny" => deny.push(network),
_ => anyhow::bail!("Unknown ACL directive: {}", parts[0]),
}
}
Ok(Self { allow, deny })
}
pub fn allow(&mut self, network: IpNetwork) {
self.allow.push(network);
}
pub fn deny(&mut self, network: IpNetwork) {
self.deny.push(network);
}
pub fn is_allowed(&self, ip: IpAddr) -> bool {
let ip = normalize_ip(ip);
for network in &self.deny {
if network.contains(ip) {
return false;
}
}
if self.allow.is_empty() {
return true;
}
for network in &self.allow {
if network.contains(ip) {
return true;
}
}
false
}
pub fn is_configured(&self) -> bool {
!self.allow.is_empty() || !self.deny.is_empty()
}
pub fn matched_rule(&self, ip: IpAddr) -> Option<String> {
for network in &self.deny {
if network.contains(ip) {
return Some(format!("deny {}", network));
}
}
for network in &self.allow {
if network.contains(ip) {
return Some(format!("allow {}", network));
}
}
None
}
}
#[derive(Debug, Clone, Default)]
pub struct AclConfig {
pub allow: Vec<String>,
pub deny: Vec<String>,
pub file: Option<String>,
}
impl AclConfig {
pub fn build(&self) -> anyhow::Result<Acl> {
let mut acl = if let Some(file) = &self.file {
Acl::from_file(Path::new(file))?
} else {
Acl::new()
};
for allow in &self.allow {
acl.allow(allow.parse()?);
}
for deny in &self.deny {
acl.deny(deny.parse()?);
}
Ok(acl)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn test_empty_acl_allows_all() {
let acl = Acl::new();
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));
}
#[test]
fn test_deny_rule() {
let mut acl = Acl::new();
acl.deny("192.168.0.0/16".parse().unwrap());
assert!(!acl.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
}
#[test]
fn test_allow_rule_creates_default_deny() {
let mut acl = Acl::new();
acl.allow("192.168.0.0/16".parse().unwrap());
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
assert!(!acl.is_allowed(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
}
#[test]
fn test_deny_takes_precedence() {
let mut acl = Acl::new();
acl.allow("192.168.0.0/16".parse().unwrap());
acl.deny("192.168.1.0/24".parse().unwrap());
assert!(acl.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 2, 1))));
assert!(!acl.is_allowed(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
}
#[test]
fn test_ipv6() {
let mut acl = Acl::new();
acl.allow("::1/128".parse().unwrap());
assert!(acl.is_allowed(IpAddr::V6(Ipv6Addr::LOCALHOST)));
assert!(!acl.is_allowed(IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1))));
}
}