127 lines
3.3 KiB
Rust
127 lines
3.3 KiB
Rust
|
|
use std::net::IpAddr;
|
||
|
|
use std::str::FromStr;
|
||
|
|
use ipnet::IpNet;
|
||
|
|
|
||
|
|
/// Match an IP address against a pattern.
|
||
|
|
///
|
||
|
|
/// Supported patterns:
|
||
|
|
/// - `*` matches any IP
|
||
|
|
/// - `192.168.1.0/24` CIDR range
|
||
|
|
/// - `192.168.1.100` exact match
|
||
|
|
/// - `192.168.1.*` wildcard (converted to CIDR)
|
||
|
|
/// - `::ffff:192.168.1.100` IPv6-mapped IPv4
|
||
|
|
pub fn ip_matches(pattern: &str, ip: &str) -> bool {
|
||
|
|
let pattern = pattern.trim();
|
||
|
|
|
||
|
|
if pattern == "*" {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Normalize IPv4-mapped IPv6
|
||
|
|
let normalized_ip = normalize_ip_str(ip);
|
||
|
|
|
||
|
|
// Try CIDR match
|
||
|
|
if pattern.contains('/') {
|
||
|
|
if let Ok(net) = IpNet::from_str(pattern) {
|
||
|
|
if let Ok(addr) = IpAddr::from_str(&normalized_ip) {
|
||
|
|
return net.contains(&addr);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle wildcard patterns like 192.168.1.*
|
||
|
|
if pattern.contains('*') {
|
||
|
|
let pattern_cidr = wildcard_to_cidr(pattern);
|
||
|
|
if let Some(cidr) = pattern_cidr {
|
||
|
|
if let Ok(net) = IpNet::from_str(&cidr) {
|
||
|
|
if let Ok(addr) = IpAddr::from_str(&normalized_ip) {
|
||
|
|
return net.contains(&addr);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Exact match
|
||
|
|
let normalized_pattern = normalize_ip_str(pattern);
|
||
|
|
normalized_ip == normalized_pattern
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if an IP matches any of the given patterns.
|
||
|
|
pub fn ip_matches_any(patterns: &[String], ip: &str) -> bool {
|
||
|
|
patterns.iter().any(|p| ip_matches(p, ip))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Normalize IPv4-mapped IPv6 addresses.
|
||
|
|
fn normalize_ip_str(ip: &str) -> String {
|
||
|
|
let ip = ip.trim();
|
||
|
|
if ip.starts_with("::ffff:") {
|
||
|
|
return ip[7..].to_string();
|
||
|
|
}
|
||
|
|
ip.to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Convert a wildcard IP pattern to CIDR notation.
|
||
|
|
/// e.g., "192.168.1.*" -> "192.168.1.0/24"
|
||
|
|
fn wildcard_to_cidr(pattern: &str) -> Option<String> {
|
||
|
|
let parts: Vec<&str> = pattern.split('.').collect();
|
||
|
|
if parts.len() != 4 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut octets = [0u8; 4];
|
||
|
|
let mut prefix_len = 0;
|
||
|
|
|
||
|
|
for (i, part) in parts.iter().enumerate() {
|
||
|
|
if *part == "*" {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if let Ok(n) = part.parse::<u8>() {
|
||
|
|
octets[i] = n;
|
||
|
|
prefix_len += 8;
|
||
|
|
} else {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(format!("{}.{}.{}.{}/{}", octets[0], octets[1], octets[2], octets[3], prefix_len))
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_wildcard_all() {
|
||
|
|
assert!(ip_matches("*", "192.168.1.100"));
|
||
|
|
assert!(ip_matches("*", "::1"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_exact_match() {
|
||
|
|
assert!(ip_matches("192.168.1.100", "192.168.1.100"));
|
||
|
|
assert!(!ip_matches("192.168.1.100", "192.168.1.101"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_cidr() {
|
||
|
|
assert!(ip_matches("192.168.1.0/24", "192.168.1.100"));
|
||
|
|
assert!(ip_matches("192.168.1.0/24", "192.168.1.1"));
|
||
|
|
assert!(!ip_matches("192.168.1.0/24", "192.168.2.1"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_wildcard_pattern() {
|
||
|
|
assert!(ip_matches("192.168.1.*", "192.168.1.100"));
|
||
|
|
assert!(ip_matches("192.168.1.*", "192.168.1.1"));
|
||
|
|
assert!(!ip_matches("192.168.1.*", "192.168.2.1"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_ipv6_mapped() {
|
||
|
|
assert!(ip_matches("192.168.1.100", "::ffff:192.168.1.100"));
|
||
|
|
assert!(ip_matches("192.168.1.0/24", "::ffff:192.168.1.50"));
|
||
|
|
}
|
||
|
|
}
|