use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr}; use thiserror::Error; #[derive(Debug, Error)] pub enum CalcError { #[error("invalid input: {0}")] InvalidInput(String), #[error("parse error: {0}")] Parse(String), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SubnetInfo { pub input: String, pub version: String, pub cidr: String, pub network: String, pub broadcast: Option, pub netmask: String, pub hostmask: String, pub prefix_len: u8, pub total_addresses: String, pub usable_addresses: String, pub first_host: Option, pub last_host: Option, } pub fn subnet_info(input: &str) -> Result { let net = parse_net(input)?; match net { IpNet::V4(v4) => Ok(subnet_info_v4(input, v4)), IpNet::V6(v6) => Ok(subnet_info_v6(input, v6)), } } pub fn contains(a: &str, b: &str) -> Result { let net_a = parse_net(a)?; let net_b = parse_net(b)?; Ok(net_a.contains(&net_b)) } pub fn overlap(a: &str, b: &str) -> Result { let net_a = parse_net(a)?; let net_b = parse_net(b)?; match (net_a, net_b) { (IpNet::V4(a), IpNet::V4(b)) => Ok(overlap_v4(a, b)), (IpNet::V6(a), IpNet::V6(b)) => Ok(overlap_v6(a, b)), _ => Ok(false), } } pub fn summarize(inputs: &[String]) -> Result, CalcError> { if inputs.is_empty() { return Err(CalcError::InvalidInput( "at least one CIDR required".to_string(), )); } let mut nets = Vec::with_capacity(inputs.len()); for value in inputs { nets.push(parse_net(value)?); } Ok(IpNet::aggregate(&nets)) } fn subnet_info_v4(input: &str, net: Ipv4Net) -> SubnetInfo { let total = total_addresses_v4(net.prefix_len()); let usable = usable_addresses_v4(net.prefix_len()); let (first, last) = first_last_v4(net); SubnetInfo { input: input.to_string(), version: "ipv4".to_string(), cidr: net.to_string(), network: net.network().to_string(), broadcast: Some(net.broadcast().to_string()), netmask: net.netmask().to_string(), hostmask: net.hostmask().to_string(), prefix_len: net.prefix_len(), total_addresses: total, usable_addresses: usable, first_host: first, last_host: last, } } fn subnet_info_v6(input: &str, net: Ipv6Net) -> SubnetInfo { let total = total_addresses_v6(net.prefix_len()); let (first, last) = first_last_v6(net); SubnetInfo { input: input.to_string(), version: "ipv6".to_string(), cidr: net.to_string(), network: net.network().to_string(), broadcast: None, netmask: net.netmask().to_string(), hostmask: net.hostmask().to_string(), prefix_len: net.prefix_len(), total_addresses: total.clone(), usable_addresses: total, first_host: first, last_host: last, } } fn parse_net(value: &str) -> Result { let trimmed = value.trim(); if trimmed.is_empty() { return Err(CalcError::InvalidInput("empty input".to_string())); } let mut parts = trimmed.split_whitespace(); let first = parts.next().unwrap(); if let Some(mask) = parts.next() { if parts.next().is_some() { return Err(CalcError::InvalidInput( "expected: ".to_string(), )); } return parse_ip_mask(first, mask); } if let Some((ip, mask)) = trimmed.split_once('/') { if mask.contains('.') || mask.contains(':') { return parse_ip_mask(ip, mask); } } trimmed .parse::() .map_err(|err| CalcError::Parse(err.to_string())) } fn parse_ip_mask(ip: &str, mask: &str) -> Result { let ip: IpAddr = ip .parse() .map_err(|_| CalcError::Parse(format!("invalid ip: {ip}")))?; let mask: IpAddr = mask .parse() .map_err(|_| CalcError::Parse(format!("invalid mask: {mask}")))?; IpNet::with_netmask(ip, mask).map_err(|err| CalcError::Parse(err.to_string())) } fn total_addresses_v4(prefix: u8) -> String { let bits = 32u32.saturating_sub(prefix as u32); (1u128 << bits).to_string() } fn usable_addresses_v4(prefix: u8) -> String { let total = 1u128 << (32u32.saturating_sub(prefix as u32)); let usable = if prefix <= 30 { total.saturating_sub(2) } else { total }; usable.to_string() } fn total_addresses_v6(prefix: u8) -> String { let bits = 128u32.saturating_sub(prefix as u32); if bits == 128 { return "340282366920938463463374607431768211456".to_string(); } (1u128 << bits).to_string() } fn first_last_v4(net: Ipv4Net) -> (Option, Option) { let network = net.network(); let broadcast = net.broadcast(); let (first, last) = if net.prefix_len() <= 30 { ( Some(Ipv4Addr::from(u32::from(network).saturating_add(1)).to_string()), Some(Ipv4Addr::from(u32::from(broadcast).saturating_sub(1)).to_string()), ) } else { (Some(network.to_string()), Some(broadcast.to_string())) }; (first, last) } fn first_last_v6(net: Ipv6Net) -> (Option, Option) { ( Some(net.network().to_string()), Some(net.broadcast().to_string()), ) } fn overlap_v4(a: Ipv4Net, b: Ipv4Net) -> bool { let a_start = u32::from(a.network()); let a_end = u32::from(a.broadcast()); let b_start = u32::from(b.network()); let b_end = u32::from(b.broadcast()); a_start <= b_end && b_start <= a_end } fn overlap_v6(a: Ipv6Net, b: Ipv6Net) -> bool { let a_start = u128::from(a.network()); let a_end = u128::from(a.broadcast()); let b_start = u128::from(b.network()); let b_end = u128::from(b.broadcast()); a_start <= b_end && b_start <= a_end } #[cfg(test)] mod tests { use super::*; #[test] fn subnet_v4_from_mask() { let info = subnet_info("192.168.1.10 255.255.255.0").expect("subnet"); assert_eq!(info.cidr, "192.168.1.10/24"); assert_eq!(info.network, "192.168.1.0"); assert_eq!(info.broadcast.as_deref(), Some("192.168.1.255")); assert_eq!(info.usable_addresses, "254"); } #[test] fn contains_and_overlap() { assert!(contains("192.168.0.0/16", "192.168.1.0/24").unwrap()); assert!(overlap("10.0.0.0/24", "10.0.0.128/25").unwrap()); assert!(!overlap("10.0.0.0/24", "10.0.1.0/24").unwrap()); } #[test] fn summarize_ipv4() { let inputs = vec!["10.0.0.0/24".to_string(), "10.0.1.0/24".to_string()]; let result = summarize(&inputs).expect("summarize"); assert_eq!(result.len(), 1); assert_eq!(result[0].to_string(), "10.0.0.0/23"); } }