use ipnet::IpNet;
use rhai::{Engine, EvalAltResult, ImmutableString};
use std::net::IpAddr;
use std::str::FromStr;
pub fn is_ipv4(ip: ImmutableString) -> bool {
if let Ok(addr) = IpAddr::from_str(ip.as_str()) {
matches!(addr, IpAddr::V4(_))
} else {
false
}
}
pub fn is_ipv6(ip: ImmutableString) -> bool {
if let Ok(addr) = IpAddr::from_str(ip.as_str()) {
matches!(addr, IpAddr::V6(_))
} else {
false
}
}
pub fn is_in_cidr(ip: ImmutableString, cidr: ImmutableString) -> Result<bool, Box<EvalAltResult>> {
let addr = IpAddr::from_str(ip.as_str()).map_err(|_| {
EvalAltResult::ErrorRuntime(
format!("Invalid IP address: '{}'", ip).into(),
rhai::Position::NONE,
)
})?;
let network = IpNet::from_str(cidr.as_str()).map_err(|_| {
EvalAltResult::ErrorRuntime(
format!("Invalid CIDR format: '{}'", cidr).into(),
rhai::Position::NONE,
)
})?;
Ok(network.contains(&addr))
}
fn mask_ip_impl(ip: &str, octets_to_mask: usize) -> String {
let parts: Vec<&str> = ip.split('.').collect();
if parts.len() != 4 {
return ip.to_string(); }
for part in &parts {
if part.parse::<u8>().is_err() {
return ip.to_string(); }
}
let mut result = parts.clone();
let mask_count = octets_to_mask.clamp(1, 4);
for item in result.iter_mut().skip(4 - mask_count) {
*item = "X";
}
result.join(".")
}
fn is_private_ip_impl(ip: &str) -> bool {
let parts: Vec<&str> = ip.split('.').collect();
if parts.len() != 4 {
return false; }
let octets: Result<Vec<u8>, _> = parts.iter().map(|s| s.parse::<u8>()).collect();
let octets = match octets {
Ok(o) => o,
Err(_) => return false,
};
match octets[0] {
10 => true, 172 => octets[1] >= 16 && octets[1] <= 31, 192 => octets[1] == 168, 127 => true, _ => false,
}
}
pub fn register_functions(engine: &mut Engine) {
engine.register_fn("is_ipv4", |ip: ImmutableString| -> bool { is_ipv4(ip) });
engine.register_fn("is_ipv6", |ip: ImmutableString| -> bool { is_ipv6(ip) });
engine.register_fn(
"is_in_cidr",
|ip: ImmutableString, cidr: ImmutableString| -> Result<bool, Box<EvalAltResult>> {
is_in_cidr(ip, cidr)
},
);
engine.register_fn("mask_ip", |ip: &str| -> String {
mask_ip_impl(ip, 1) });
engine.register_fn("mask_ip", |ip: &str, octets: i64| -> String {
mask_ip_impl(ip, octets.clamp(1, 4) as usize) });
engine.register_fn("is_private_ip", |ip: &str| -> bool {
is_private_ip_impl(ip)
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_ipv4() {
assert!(is_ipv4("192.168.1.1".into()));
assert!(is_ipv4("10.0.0.1".into()));
assert!(is_ipv4("0.0.0.0".into()));
assert!(is_ipv4("255.255.255.255".into()));
assert!(!is_ipv4("not-an-ip".into()));
assert!(!is_ipv4("256.1.1.1".into()));
assert!(!is_ipv4("".into()));
assert!(!is_ipv4("2001:db8::1".into())); }
#[test]
fn test_is_ipv6() {
assert!(is_ipv6("2001:db8::1".into()));
assert!(is_ipv6("::1".into()));
assert!(is_ipv6("fe80::1".into()));
assert!(is_ipv6("2001:0db8:0000:0000:0000:0000:0000:0001".into()));
assert!(!is_ipv6("not-an-ip".into()));
assert!(!is_ipv6("".into()));
assert!(!is_ipv6("192.168.1.1".into())); }
#[test]
fn test_is_in_cidr_ipv4() {
assert!(is_in_cidr("192.168.1.1".into(), "192.168.1.0/24".into()).unwrap());
assert!(is_in_cidr("10.0.0.1".into(), "10.0.0.0/8".into()).unwrap());
assert!(is_in_cidr("172.16.5.4".into(), "172.16.0.0/16".into()).unwrap());
assert!(!is_in_cidr("192.168.2.1".into(), "192.168.1.0/24".into()).unwrap());
assert!(!is_in_cidr("11.0.0.1".into(), "10.0.0.0/8".into()).unwrap());
assert!(is_in_cidr("192.168.1.1".into(), "192.168.1.1/32".into()).unwrap());
assert!(is_in_cidr("0.0.0.0".into(), "0.0.0.0/0".into()).unwrap()); }
#[test]
fn test_is_in_cidr_ipv6() {
assert!(is_in_cidr("2001:db8::1".into(), "2001:db8::/32".into()).unwrap());
assert!(is_in_cidr("fe80::1".into(), "fe80::/10".into()).unwrap());
assert!(!is_in_cidr("2001:db9::1".into(), "2001:db8::/32".into()).unwrap());
}
#[test]
fn test_is_in_cidr_errors() {
assert!(is_in_cidr("not-an-ip".into(), "192.168.1.0/24".into()).is_err());
assert!(is_in_cidr("192.168.1.1".into(), "not-a-cidr".into()).is_err());
assert!(is_in_cidr("192.168.1.1".into(), "192.168.1.0/33".into()).is_err());
}
#[test]
fn test_is_in_cidr_mixed() {
assert!(!is_in_cidr("192.168.1.1".into(), "2001:db8::/32".into()).unwrap());
assert!(!is_in_cidr("2001:db8::1".into(), "192.168.1.0/24".into()).unwrap());
}
#[test]
fn test_mask_ip_function() {
use rhai::Scope;
let mut engine = Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("ip", "192.168.1.100");
scope.push("invalid", "not-an-ip");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip()"##)
.unwrap();
assert_eq!(result, "192.168.1.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(2)"##)
.unwrap();
assert_eq!(result, "192.168.X.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(3)"##)
.unwrap();
assert_eq!(result, "192.X.X.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(4)"##)
.unwrap();
assert_eq!(result, "X.X.X.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"invalid.mask_ip()"##)
.unwrap();
assert_eq!(result, "not-an-ip");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(0)"##)
.unwrap();
assert_eq!(result, "192.168.1.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(10)"##)
.unwrap();
assert_eq!(result, "X.X.X.X");
}
#[test]
fn test_is_private_ip_function() {
use rhai::Scope;
let mut engine = Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("private1", "10.0.0.1");
scope.push("private2", "172.16.0.1");
scope.push("private3", "192.168.1.1");
scope.push("loopback", "127.0.0.1");
scope.push("public1", "8.8.8.8");
scope.push("public2", "1.1.1.1");
scope.push("edge1", "172.15.0.1"); scope.push("edge2", "172.32.0.1"); scope.push("invalid", "not-an-ip");
let result: bool = engine
.eval_with_scope(&mut scope, r##"private1.is_private_ip()"##)
.unwrap();
assert!(result);
let result: bool = engine
.eval_with_scope(&mut scope, r##"private2.is_private_ip()"##)
.unwrap();
assert!(result);
let result: bool = engine
.eval_with_scope(&mut scope, r##"private3.is_private_ip()"##)
.unwrap();
assert!(result);
let result: bool = engine
.eval_with_scope(&mut scope, r##"loopback.is_private_ip()"##)
.unwrap();
assert!(result);
let result: bool = engine
.eval_with_scope(&mut scope, r##"public1.is_private_ip()"##)
.unwrap();
assert!(!result);
let result: bool = engine
.eval_with_scope(&mut scope, r##"public2.is_private_ip()"##)
.unwrap();
assert!(!result);
let result: bool = engine
.eval_with_scope(&mut scope, r##"edge1.is_private_ip()"##)
.unwrap();
assert!(!result);
let result: bool = engine
.eval_with_scope(&mut scope, r##"edge2.is_private_ip()"##)
.unwrap();
assert!(!result);
let result: bool = engine
.eval_with_scope(&mut scope, r##"invalid.is_private_ip()"##)
.unwrap();
assert!(!result);
}
}