diff --git a/TODO b/TODO index 9b3b9fa..3737c19 100644 --- a/TODO +++ b/TODO @@ -8,3 +8,5 @@ test Filter::regex conformity after setup should an ipv6-mapped ipv4 match a pattern of type ipv6? should it be normalized as ipv4 then? + +duplicate: deduplicate when loading database diff --git a/src/concepts/pattern/ip/cidr.rs b/src/concepts/pattern/ip/cidr.rs new file mode 100644 index 0000000..9e77975 --- /dev/null +++ b/src/concepts/pattern/ip/cidr.rs @@ -0,0 +1,239 @@ +use std::{ + fmt::Display, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +use super::*; + +/// Stores an IP and an associated mask. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Cidr { + IPv4((Ipv4Addr, Ipv4Addr)), + IPv6((Ipv6Addr, Ipv6Addr)), +} + +impl FromStr for Cidr { + type Err = String; + + fn from_str(cidr: &str) -> Result { + let (ip, mask) = cidr.split_once('/').ok_or(format!( + "malformed IP/MASK. '{cidr}' doesn't contain any '/'" + ))?; + let ip = normalize(ip).map_err(|err| format!("malformed IP '{ip}' in '{cidr}': {err}"))?; + let mask_count = u8::from_str(mask) + .map_err(|err| format!("malformed mask '{mask}' in '{cidr}': {err}"))?; + + // Let's accept any mask size for now, as useless as it may seem + // if mask_count < 2 { + // return Err("Can't have a network mask of 0 or 1. You're either ignoring all Internet or half of it.".into()); + // } else if mask_count + // < (match ip { + // IpAddr::V4(_) => 8, + // IpAddr::V6(_) => 16, + // }) + // { + // warn!("With a mask of {mask_count}, you're ignoring a big part of Internet. Are you sure you want to do this?"); + // } + + Self::from_ip_and_mask(ip, mask_count) + } +} + +impl Display for Cidr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.network(), self.mask()) + } +} + +impl Cidr { + fn from_ip_and_mask(ip: IpAddr, mask_count: u8) -> Result { + match ip { + IpAddr::V4(mut ipv4_addr) => { + // Create bitmask + let mask = mask_to_ipv4(mask_count)?; + // Normalize IP from mask + ipv4_addr &= mask; + + Ok(Cidr::IPv4((ipv4_addr, mask))) + } + IpAddr::V6(mut ipv6_addr) => { + let mask = mask_to_ipv6(mask_count)?; + // Normalize IP from mask + ipv6_addr &= mask; + + Ok(Cidr::IPv6((ipv6_addr, mask))) + } + } + } + + /// Whether an IP is included in this IP CIDR. + /// If IP is not the same version as CIDR, returns always false. + pub fn includes(&self, ip: &IpAddr) -> bool { + let ip = normalize_ip(*ip); + match self { + Cidr::IPv4((network_ipv4, mask)) => match ip { + IpAddr::V6(_) => false, + IpAddr::V4(ipv4_addr) => *network_ipv4 == ipv4_addr & mask, + }, + Cidr::IPv6((network_ipv6, mask)) => match ip { + IpAddr::V4(_) => false, + IpAddr::V6(ipv6_addr) => *network_ipv6 == ipv6_addr & mask, + }, + } + } + + fn network(&self) -> IpAddr { + match self { + Cidr::IPv4((network, _)) => IpAddr::from(*network), + Cidr::IPv6((network, _)) => IpAddr::from(*network), + } + } + + fn mask(&self) -> u8 { + let mut raw_mask = match self { + Cidr::IPv4((_, mask)) => mask.to_bits() as u128, + Cidr::IPv6((_, mask)) => mask.to_bits(), + }; + let mut ret = 0; + for _ in 0..128 { + if raw_mask % 2 == 1 { + ret += 1; + } + raw_mask >>= 1; + } + ret + } +} + +#[cfg(test)] +mod cidr_tests { + use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, + }; + + use super::Cidr; + + #[test] + fn cidrv4_from_str() { + assert_eq!( + Ok(Cidr::IPv4((Ipv4Addr::new(192, 168, 1, 4), u32::MAX.into()))), + Cidr::from_str("192.168.1.4/32") + ); + // Test IP normalization from mask + assert_eq!( + Ok(Cidr::IPv4(( + Ipv4Addr::new(192, 168, 1, 0), + Ipv4Addr::new(255, 255, 255, 0), + ))), + Cidr::from_str("192.168.1.4/24") + ); + // Another ok-test "pour la route" + assert_eq!( + Ok(Cidr::IPv4(( + Ipv4Addr::new(1, 1, 0, 0), + Ipv4Addr::new(255, 255, 0, 0), + ))), + Cidr::from_str("1.1.248.25/16") + ); + // Errors + assert!(Cidr::from_str("256.1.1.1/8").is_err()); + // assert!(Cidr::from_str("1.1.1.1/0").is_err()); + // assert!(Cidr::from_str("1.1.1.1/1").is_err()); + // assert!(Cidr::from_str("1.1.1.1.1").is_err()); + assert!(Cidr::from_str("1.1.1.1/16/16").is_err()); + } + + #[test] + fn cidrv6_from_str() { + assert_eq!( + Ok(Cidr::IPv6(( + Ipv6Addr::new(0xfe80, 0, 0, 0, 0xdf68, 0x2ee, 0xe4f9, 0xe68), + u128::MAX.into() + ))), + Cidr::from_str("fe80::df68:2ee:e4f9:e68/128") + ); + // Test IP normalization from mask + assert_eq!( + Ok(Cidr::IPv6(( + Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x9de5, 0, 0, 0, 0), + Ipv6Addr::new(u16::MAX, u16::MAX, u16::MAX, u16::MAX, 0, 0, 0, 0), + ))), + Cidr::from_str("2001:db8:85a3:9de5::8a2e:370:7334/64") + ); + // Another ok-test "pour la route" + assert_eq!( + Ok(Cidr::IPv6(( + Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x9d00, 0, 0, 0, 0), + Ipv6Addr::new( + u16::MAX, + u16::MAX, + u16::MAX, + u16::MAX - u8::MAX as u16, + 0, + 0, + 0, + 0 + ), + ))), + Cidr::from_str("2001:db8:85a3:9d00::8a2e:370:7334/56") + ); + assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334/56").is_ok()); + assert!(Cidr::from_str("2001:DB8:85A3:0:0:8A2E:370:7334/56").is_ok()); + // Errors + assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:g334/56").is_err()); + // assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334/0").is_err()); + // assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334/1").is_err()); + assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334:11/56").is_err()); + assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334/11/56").is_err()); + } + + #[test] + fn cidrv4_includes() { + let cidr = Cidr::from_str("192.168.1.0/24").unwrap(); + assert!(cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)))); + assert!(cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); + assert!(cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 234)))); + assert!(!cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)))); + assert!(!cidr.includes(&IpAddr::V6(Ipv6Addr::new( + 0xfe80, 0, 0, 0, 0xdf68, 0x2ee, 0xe4f9, 0xe68 + ),))); + } + + #[test] + fn cidrv6_includes() { + let cidr = Cidr::from_str("2001:db8:85a3:9d00:0:8a2e:370:7334/56").unwrap(); + assert!(cidr.includes(&IpAddr::V6(Ipv6Addr::new( + 0x2001, 0x0db8, 0x85a3, 0x9d00, 0, 0, 0, 0 + )))); + assert!(cidr.includes(&IpAddr::V6(Ipv6Addr::new( + 0x2001, 0x0db8, 0x85a3, 0x9da4, 0x34fc, 0x0d8b, 0xffff, 0x1111 + )))); + assert!(!cidr.includes(&IpAddr::V6(Ipv6Addr::new( + 0x2001, 0x0db8, 0x85a3, 0xad00, 0, 0, 0, 1 + )))); + assert!(!cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)))); + } + + #[test] + fn cidr_display() { + let cidrs = [ + ("192.168.1.4/32", "192.168.1.4/32"), + ("192.168.1.4/24", "192.168.1.0/24"), + ("1.1.248.25/16", "1.1.0.0/16"), + ("fe80::df68:2ee:e4f9:e68/128", "fe80::df68:2ee:e4f9:e68/128"), + ( + "2001:db8:85a3:9de5::8a2e:370:7334/64", + "2001:db8:85a3:9de5::/64", + ), + ( + "2001:db8:85a3:9d00::8a2e:370:7334/56", + "2001:db8:85a3:9d00::/56", + ), + ]; + for (from, to) in cidrs { + assert_eq!(Cidr::from_str(from).unwrap().to_string(), to); + } + } +} diff --git a/src/concepts/pattern/ip.rs b/src/concepts/pattern/ip/mod.rs similarity index 68% rename from src/concepts/pattern/ip.rs rename to src/concepts/pattern/ip/mod.rs index 15a3319..77f525d 100644 --- a/src/concepts/pattern/ip.rs +++ b/src/concepts/pattern/ip/mod.rs @@ -1,12 +1,17 @@ use std::{ - fmt::Display, - net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, str::FromStr, }; use serde::{Deserialize, Serialize}; use tracing::warn; +use cidr::Cidr; +use utils::*; + +mod cidr; +mod utils; + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub enum PatternType { #[default] @@ -283,11 +288,11 @@ mod patternip_tests { use tokio::{fs::read_to_string, task::JoinSet}; use crate::{ - concepts::{pattern::ip::Cidr, Action, Duplicate, Filter, Pattern}, + concepts::{Action, Duplicate, Filter, Pattern}, daemon::{tests::TestBed, React}, }; - use super::{PatternIp, PatternType}; + use super::{Cidr, PatternIp, PatternType}; #[test] fn test_setup_type_regex() { @@ -716,351 +721,3 @@ mod patternip_tests { join_set.join_all().await; } } - -/// Normalize a string as an IP address. -/// IPv6-mapped IPv4 addresses are casted to IPv4. -fn normalize(ip: &str) -> Result { - IpAddr::from_str(ip).map(normalize_ip) -} - -/// Normalize a string as an IP address. -/// IPv6-mapped IPv4 addresses are casted to IPv4. -fn normalize_ip(ip: IpAddr) -> IpAddr { - match ip { - IpAddr::V4(_) => ip, - IpAddr::V6(ipv6) => match ipv6.to_ipv4_mapped() { - Some(ipv4) => IpAddr::V4(ipv4), - None => ip, - }, - } -} - -/// Creates an [`Ipv4Addr`] from a mask -fn mask_to_ipv4(mask_count: u8) -> Result { - if mask_count > 32 { - Err(format!( - "an IPv4 mask must be 32 max. {mask_count} is too big." - )) - } else { - let mask = match mask_count { - 0 => 0u32, - n => u32::MAX << (32 - n), - }; - let mask = Ipv4Addr::from_bits(mask); - Ok(mask) - } -} - -/// Creates an [`Ipv4Addr`] from a mask -fn mask_to_ipv6(mask_count: u8) -> Result { - if mask_count > 128 { - Err(format!( - "an IPv4 mask must be 128 max. {mask_count} is too big." - )) - } else { - let mask = match mask_count { - 0 => 0u128, - n => u128::MAX << (128 - n), - }; - let mask = Ipv6Addr::from_bits(mask); - Ok(mask) - } -} - -#[cfg(test)] -mod utils_tests { - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - - use super::{mask_to_ipv4, mask_to_ipv6, normalize}; - - #[test] - fn test_normalize_ip() { - assert_eq!( - normalize("83.44.23.14"), - Ok(IpAddr::V4(Ipv4Addr::new(83, 44, 23, 14))) - ); - assert_eq!( - normalize("2001:db8:85a3::8a2e:370:7334"), - Ok(IpAddr::V6(Ipv6Addr::new( - 0x2001, 0xdb8, 0x85a3, 0x0, 0x0, 0x8a2e, 0x370, 0x7334 - ))) - ); - assert_eq!( - normalize("::ffff:192.168.1.34"), - Ok(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 34))) - ); - assert_eq!( - normalize("::ffff:1.2.3.4"), - Ok(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))) - ); - } - - #[test] - fn test_mask_to_ipv4() { - assert!(mask_to_ipv4(33).is_err()); - assert!(mask_to_ipv4(100).is_err()); - assert_eq!(mask_to_ipv4(16), Ok(Ipv4Addr::new(255, 255, 0, 0))); - assert_eq!(mask_to_ipv4(24), Ok(Ipv4Addr::new(255, 255, 255, 0))); - assert_eq!(mask_to_ipv4(25), Ok(Ipv4Addr::new(255, 255, 255, 128))); - assert_eq!(mask_to_ipv4(26), Ok(Ipv4Addr::new(255, 255, 255, 192))); - assert_eq!(mask_to_ipv4(32), Ok(Ipv4Addr::new(255, 255, 255, 255))); - } - - #[test] - fn test_mask_to_ipv6() { - assert!(mask_to_ipv6(129).is_err()); - assert!(mask_to_ipv6(254).is_err()); - assert_eq!( - mask_to_ipv6(56), - Ok(Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xff00, 0, 0, 0, 0)) - ); - assert_eq!( - mask_to_ipv6(64), - Ok(Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xffff, 0, 0, 0, 0)) - ); - assert_eq!( - mask_to_ipv6(112), - Ok(Ipv6Addr::new( - 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0 - )) - ); - assert_eq!( - mask_to_ipv6(128), - Ok(Ipv6Addr::new( - 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff - )) - ); - } -} - -/// Stores an IP and an associated mask. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Cidr { - IPv4((Ipv4Addr, Ipv4Addr)), - IPv6((Ipv6Addr, Ipv6Addr)), -} - -impl FromStr for Cidr { - type Err = String; - - fn from_str(cidr: &str) -> Result { - let (ip, mask) = cidr.split_once('/').ok_or(format!( - "malformed IP/MASK. '{cidr}' doesn't contain any '/'" - ))?; - let ip = normalize(ip).map_err(|err| format!("malformed IP '{ip}' in '{cidr}': {err}"))?; - let mask_count = u8::from_str(mask) - .map_err(|err| format!("malformed mask '{mask}' in '{cidr}': {err}"))?; - - // Let's accept any mask size for now, as useless as it may seem - // if mask_count < 2 { - // return Err("Can't have a network mask of 0 or 1. You're either ignoring all Internet or half of it.".into()); - // } else if mask_count - // < (match ip { - // IpAddr::V4(_) => 8, - // IpAddr::V6(_) => 16, - // }) - // { - // warn!("With a mask of {mask_count}, you're ignoring a big part of Internet. Are you sure you want to do this?"); - // } - - Self::from_ip_and_mask(ip, mask_count) - } -} - -impl Display for Cidr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}/{}", self.network(), self.mask()) - } -} - -impl Cidr { - fn from_ip_and_mask(ip: IpAddr, mask_count: u8) -> Result { - match ip { - IpAddr::V4(mut ipv4_addr) => { - // Create bitmask - let mask = mask_to_ipv4(mask_count)?; - // Normalize IP from mask - ipv4_addr &= mask; - - Ok(Cidr::IPv4((ipv4_addr, mask))) - } - IpAddr::V6(mut ipv6_addr) => { - let mask = mask_to_ipv6(mask_count)?; - // Normalize IP from mask - ipv6_addr &= mask; - - Ok(Cidr::IPv6((ipv6_addr, mask))) - } - } - } - - /// Whether an IP is included in this IP CIDR. - /// If IP is not the same version as CIDR, returns always false. - fn includes(&self, ip: &IpAddr) -> bool { - let ip = normalize_ip(*ip); - match self { - Cidr::IPv4((network_ipv4, mask)) => match ip { - IpAddr::V6(_) => false, - IpAddr::V4(ipv4_addr) => *network_ipv4 == ipv4_addr & mask, - }, - Cidr::IPv6((network_ipv6, mask)) => match ip { - IpAddr::V4(_) => false, - IpAddr::V6(ipv6_addr) => *network_ipv6 == ipv6_addr & mask, - }, - } - } - - fn network(&self) -> IpAddr { - match self { - Cidr::IPv4((network, _)) => IpAddr::from(*network), - Cidr::IPv6((network, _)) => IpAddr::from(*network), - } - } - - fn mask(&self) -> u8 { - let mut raw_mask = match self { - Cidr::IPv4((_, mask)) => mask.to_bits() as u128, - Cidr::IPv6((_, mask)) => mask.to_bits(), - }; - let mut ret = 0; - for _ in 0..128 { - if raw_mask % 2 == 1 { - ret += 1; - } - raw_mask >>= 1; - } - ret - } -} - -#[cfg(test)] -mod cidr_tests { - use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr}, - str::FromStr, - }; - - use super::Cidr; - - #[test] - fn cidrv4_from_str() { - assert_eq!( - Ok(Cidr::IPv4((Ipv4Addr::new(192, 168, 1, 4), u32::MAX.into()))), - Cidr::from_str("192.168.1.4/32") - ); - // Test IP normalization from mask - assert_eq!( - Ok(Cidr::IPv4(( - Ipv4Addr::new(192, 168, 1, 0), - Ipv4Addr::new(255, 255, 255, 0), - ))), - Cidr::from_str("192.168.1.4/24") - ); - // Another ok-test "pour la route" - assert_eq!( - Ok(Cidr::IPv4(( - Ipv4Addr::new(1, 1, 0, 0), - Ipv4Addr::new(255, 255, 0, 0), - ))), - Cidr::from_str("1.1.248.25/16") - ); - // Errors - assert!(Cidr::from_str("256.1.1.1/8").is_err()); - // assert!(Cidr::from_str("1.1.1.1/0").is_err()); - // assert!(Cidr::from_str("1.1.1.1/1").is_err()); - // assert!(Cidr::from_str("1.1.1.1.1").is_err()); - assert!(Cidr::from_str("1.1.1.1/16/16").is_err()); - } - - #[test] - fn cidrv6_from_str() { - assert_eq!( - Ok(Cidr::IPv6(( - Ipv6Addr::new(0xfe80, 0, 0, 0, 0xdf68, 0x2ee, 0xe4f9, 0xe68), - u128::MAX.into() - ))), - Cidr::from_str("fe80::df68:2ee:e4f9:e68/128") - ); - // Test IP normalization from mask - assert_eq!( - Ok(Cidr::IPv6(( - Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x9de5, 0, 0, 0, 0), - Ipv6Addr::new(u16::MAX, u16::MAX, u16::MAX, u16::MAX, 0, 0, 0, 0), - ))), - Cidr::from_str("2001:db8:85a3:9de5::8a2e:370:7334/64") - ); - // Another ok-test "pour la route" - assert_eq!( - Ok(Cidr::IPv6(( - Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x9d00, 0, 0, 0, 0), - Ipv6Addr::new( - u16::MAX, - u16::MAX, - u16::MAX, - u16::MAX - u8::MAX as u16, - 0, - 0, - 0, - 0 - ), - ))), - Cidr::from_str("2001:db8:85a3:9d00::8a2e:370:7334/56") - ); - assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334/56").is_ok()); - assert!(Cidr::from_str("2001:DB8:85A3:0:0:8A2E:370:7334/56").is_ok()); - // Errors - assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:g334/56").is_err()); - // assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334/0").is_err()); - // assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334/1").is_err()); - assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334:11/56").is_err()); - assert!(Cidr::from_str("2001:db8:85a3:0:0:8a2e:370:7334/11/56").is_err()); - } - - #[test] - fn cidrv4_includes() { - let cidr = Cidr::from_str("192.168.1.0/24").unwrap(); - assert!(cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)))); - assert!(cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); - assert!(cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 234)))); - assert!(!cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1)))); - assert!(!cidr.includes(&IpAddr::V6(Ipv6Addr::new( - 0xfe80, 0, 0, 0, 0xdf68, 0x2ee, 0xe4f9, 0xe68 - ),))); - } - - #[test] - fn cidrv6_includes() { - let cidr = Cidr::from_str("2001:db8:85a3:9d00:0:8a2e:370:7334/56").unwrap(); - assert!(cidr.includes(&IpAddr::V6(Ipv6Addr::new( - 0x2001, 0x0db8, 0x85a3, 0x9d00, 0, 0, 0, 0 - )))); - assert!(cidr.includes(&IpAddr::V6(Ipv6Addr::new( - 0x2001, 0x0db8, 0x85a3, 0x9da4, 0x34fc, 0x0d8b, 0xffff, 0x1111 - )))); - assert!(!cidr.includes(&IpAddr::V6(Ipv6Addr::new( - 0x2001, 0x0db8, 0x85a3, 0xad00, 0, 0, 0, 1 - )))); - assert!(!cidr.includes(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)))); - } - - #[test] - fn cidr_display() { - let cidrs = [ - ("192.168.1.4/32", "192.168.1.4/32"), - ("192.168.1.4/24", "192.168.1.0/24"), - ("1.1.248.25/16", "1.1.0.0/16"), - ("fe80::df68:2ee:e4f9:e68/128", "fe80::df68:2ee:e4f9:e68/128"), - ( - "2001:db8:85a3:9de5::8a2e:370:7334/64", - "2001:db8:85a3:9de5::/64", - ), - ( - "2001:db8:85a3:9d00::8a2e:370:7334/56", - "2001:db8:85a3:9d00::/56", - ), - ]; - for (from, to) in cidrs { - assert_eq!(Cidr::from_str(from).unwrap().to_string(), to); - } - } -} diff --git a/src/concepts/pattern/ip/utils.rs b/src/concepts/pattern/ip/utils.rs new file mode 100644 index 0000000..a294c55 --- /dev/null +++ b/src/concepts/pattern/ip/utils.rs @@ -0,0 +1,120 @@ +use std::{ + net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +/// Normalize a string as an IP address. +/// IPv6-mapped IPv4 addresses are casted to IPv4. +pub fn normalize(ip: &str) -> Result { + IpAddr::from_str(ip).map(normalize_ip) +} + +/// Normalize a string as an IP address. +/// IPv6-mapped IPv4 addresses are casted to IPv4. +pub fn normalize_ip(ip: IpAddr) -> IpAddr { + match ip { + IpAddr::V4(_) => ip, + IpAddr::V6(ipv6) => match ipv6.to_ipv4_mapped() { + Some(ipv4) => IpAddr::V4(ipv4), + None => ip, + }, + } +} + +/// Creates an [`Ipv4Addr`] from a mask +pub fn mask_to_ipv4(mask_count: u8) -> Result { + if mask_count > 32 { + Err(format!( + "an IPv4 mask must be 32 max. {mask_count} is too big." + )) + } else { + let mask = match mask_count { + 0 => 0u32, + n => u32::MAX << (32 - n), + }; + let mask = Ipv4Addr::from_bits(mask); + Ok(mask) + } +} + +/// Creates an [`Ipv4Addr`] from a mask +pub fn mask_to_ipv6(mask_count: u8) -> Result { + if mask_count > 128 { + Err(format!( + "an IPv4 mask must be 128 max. {mask_count} is too big." + )) + } else { + let mask = match mask_count { + 0 => 0u128, + n => u128::MAX << (128 - n), + }; + let mask = Ipv6Addr::from_bits(mask); + Ok(mask) + } +} + +#[cfg(test)] +mod utils_tests { + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + use super::{mask_to_ipv4, mask_to_ipv6, normalize}; + + #[test] + fn test_normalize_ip() { + assert_eq!( + normalize("83.44.23.14"), + Ok(IpAddr::V4(Ipv4Addr::new(83, 44, 23, 14))) + ); + assert_eq!( + normalize("2001:db8:85a3::8a2e:370:7334"), + Ok(IpAddr::V6(Ipv6Addr::new( + 0x2001, 0xdb8, 0x85a3, 0x0, 0x0, 0x8a2e, 0x370, 0x7334 + ))) + ); + assert_eq!( + normalize("::ffff:192.168.1.34"), + Ok(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 34))) + ); + assert_eq!( + normalize("::ffff:1.2.3.4"), + Ok(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))) + ); + } + + #[test] + fn test_mask_to_ipv4() { + assert!(mask_to_ipv4(33).is_err()); + assert!(mask_to_ipv4(100).is_err()); + assert_eq!(mask_to_ipv4(16), Ok(Ipv4Addr::new(255, 255, 0, 0))); + assert_eq!(mask_to_ipv4(24), Ok(Ipv4Addr::new(255, 255, 255, 0))); + assert_eq!(mask_to_ipv4(25), Ok(Ipv4Addr::new(255, 255, 255, 128))); + assert_eq!(mask_to_ipv4(26), Ok(Ipv4Addr::new(255, 255, 255, 192))); + assert_eq!(mask_to_ipv4(32), Ok(Ipv4Addr::new(255, 255, 255, 255))); + } + + #[test] + fn test_mask_to_ipv6() { + assert!(mask_to_ipv6(129).is_err()); + assert!(mask_to_ipv6(254).is_err()); + assert_eq!( + mask_to_ipv6(56), + Ok(Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xff00, 0, 0, 0, 0)) + ); + assert_eq!( + mask_to_ipv6(64), + Ok(Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xffff, 0, 0, 0, 0)) + ); + assert_eq!( + mask_to_ipv6(112), + Ok(Ipv6Addr::new( + 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0 + )) + ); + assert_eq!( + mask_to_ipv6(128), + Ok(Ipv6Addr::new( + 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff + )) + ); + } +}