use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[derive(Debug, Clone, PartialEq)]
pub enum IpValidationError {
InvalidFormat(String),
Reserved(String),
Blocked(String),
InvalidPort(u16),
}
impl std::fmt::Display for IpValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IpValidationError::InvalidFormat(msg) => write!(f, "Invalid IP format: {msg}"),
IpValidationError::Reserved(msg) => write!(f, "Reserved IP: {msg}"),
IpValidationError::Blocked(msg) => write!(f, "Blocked IP: {msg}"),
IpValidationError::InvalidPort(port) => write!(f, "Invalid port: {port}"),
}
}
}
pub type IpValidationResult<T> = Result<T, IpValidationError>;
pub struct IpValidator {
block_private: bool,
block_loopback: bool,
block_multicast: bool,
block_link_local: bool,
block_unspecified: bool,
block_documentation: bool,
blocked_ranges: Vec<IpRange>,
allowed_ports: PortRange,
allow_all_ports: bool,
}
impl Default for IpValidator {
fn default() -> Self {
Self {
block_private: true,
block_loopback: true,
block_multicast: true,
block_link_local: true,
block_unspecified: true,
block_documentation: true,
blocked_ranges: Vec::new(),
allowed_ports: PortRange::new(1024, 65535),
allow_all_ports: false,
}
}
}
#[allow(dead_code)]
impl IpValidator {
pub fn new() -> Self {
Self::default()
}
pub fn permissive() -> Self {
Self {
block_private: false,
block_loopback: false,
block_multicast: true, block_link_local: false,
block_unspecified: true, block_documentation: false,
blocked_ranges: Vec::new(),
allowed_ports: PortRange::new(0, 65535),
allow_all_ports: true,
}
}
pub fn allow_private(mut self) -> Self {
self.block_private = false;
self
}
pub fn block_private(mut self) -> Self {
self.block_private = true;
self
}
pub fn allow_loopback(mut self) -> Self {
self.block_loopback = false;
self
}
pub fn allow_multicast(mut self) -> Self {
self.block_multicast = false;
self
}
pub fn allow_link_local(mut self) -> Self {
self.block_link_local = false;
self
}
pub fn allow_ports(mut self, min: u16, max: u16) -> Self {
self.allowed_ports = PortRange::new(min, max);
self.allow_all_ports = false;
self
}
pub fn allow_all_ports(mut self) -> Self {
self.allow_all_ports = true;
self
}
pub fn add_blocked_range(mut self, range: IpRange) -> Self {
self.blocked_ranges.push(range);
self
}
pub fn validate_ip_str(&self, ip_str: &str) -> IpValidationResult<IpAddr> {
let ip = ip_str.parse::<IpAddr>().map_err(|e| {
IpValidationError::InvalidFormat(format!("Could not parse IP address: {e}"))
})?;
self.validate_ip(ip)
}
pub fn validate_socket_addr_str(&self, addr_str: &str) -> IpValidationResult<(IpAddr, u16)> {
let addr = addr_str.parse::<std::net::SocketAddr>().map_err(|e| {
IpValidationError::InvalidFormat(format!("Could not parse socket address: {e}"))
})?;
let ip = self.validate_ip(addr.ip())?;
self.validate_port(addr.port())?;
Ok((ip, addr.port()))
}
pub fn validate_ip(&self, ip: IpAddr) -> IpValidationResult<IpAddr> {
match ip {
IpAddr::V4(ipv4) => self.validate_ipv4(ipv4).map(IpAddr::V4),
IpAddr::V6(ipv6) => self.validate_ipv6(ipv6).map(IpAddr::V6),
}
}
pub fn validate_ipv4(&self, ip: Ipv4Addr) -> IpValidationResult<Ipv4Addr> {
if self.block_unspecified && ip.is_unspecified() {
return Err(IpValidationError::Reserved(
"Unspecified IPv4 address (0.0.0.0)".to_string(),
));
}
if self.block_loopback && ip.is_loopback() {
return Err(IpValidationError::Reserved(
"Loopback IPv4 address (127.0.0.0/8)".to_string(),
));
}
if self.block_private && ip.is_private() {
return Err(IpValidationError::Reserved(format!(
"Private IPv4 address: {ip}"
)));
}
if self.block_link_local && ip.is_link_local() {
return Err(IpValidationError::Reserved(
"Link-local IPv4 address (169.254.0.0/16)".to_string(),
));
}
if self.block_multicast && ip.is_multicast() {
return Err(IpValidationError::Reserved(
"Multicast IPv4 address (224.0.0.0/4)".to_string(),
));
}
if ip.is_broadcast() {
return Err(IpValidationError::Reserved(
"Broadcast IPv4 address (255.255.255.255)".to_string(),
));
}
if self.block_documentation {
let first_octet = ip.octets()[0];
let second_octet = ip.octets()[1];
let third_octet = ip.octets()[2];
if (first_octet == 192 && second_octet == 0 && third_octet == 2)
|| (first_octet == 198 && second_octet == 51 && third_octet == 100)
|| (first_octet == 203 && second_octet == 0 && third_octet == 113)
{
return Err(IpValidationError::Reserved(format!(
"Documentation IPv4 address: {ip}"
)));
}
}
for range in &self.blocked_ranges {
if range.contains_ipv4(&ip) {
return Err(IpValidationError::Blocked(format!(
"IPv4 address {ip} is in blocked range: {range}"
)));
}
}
Ok(ip)
}
pub fn validate_ipv6(&self, ip: Ipv6Addr) -> IpValidationResult<Ipv6Addr> {
if let Some(ipv4) = ip_v6_to_v4_mapped(&ip) {
self.validate_ipv4(ipv4)?;
}
if self.block_unspecified && ip.is_unspecified() {
return Err(IpValidationError::Reserved(
"Unspecified IPv6 address (::/128)".to_string(),
));
}
if self.block_loopback && ip.is_loopback() {
return Err(IpValidationError::Reserved(
"Loopback IPv6 address (::1/128)".to_string(),
));
}
if self.block_multicast && ip.is_multicast() {
return Err(IpValidationError::Reserved(
"Multicast IPv6 address (ff00::/8)".to_string(),
));
}
if self.block_documentation {
let segments = ip.segments();
if segments[0] == 0x2001 && segments[1] == 0xdb8 {
return Err(IpValidationError::Reserved(format!(
"Documentation IPv6 address (2001:db8::/32): {ip}"
)));
}
}
let is_ula = (ip.segments()[0] & 0xfe00) == 0xfc00;
if self.block_private && is_ula {
return Err(IpValidationError::Reserved(format!(
"Unique Local IPv6 address (ULA): {ip}"
)));
}
let is_link_local = (ip.segments()[0] & 0xffc0) == 0xfe80;
if self.block_link_local && is_link_local {
return Err(IpValidationError::Reserved(format!(
"Link-local IPv6 address: {ip}"
)));
}
for range in &self.blocked_ranges {
if range.contains_ipv6(&ip) {
return Err(IpValidationError::Blocked(format!(
"IPv6 address {ip} is in blocked range: {range}"
)));
}
}
Ok(ip)
}
pub fn validate_port(&self, port: u16) -> IpValidationResult<u16> {
if self.allow_all_ports {
return Ok(port);
}
if !self.allowed_ports.contains(port) {
return Err(IpValidationError::InvalidPort(port));
}
Ok(port)
}
pub fn is_valid_ip(&self, ip_str: &str) -> bool {
self.validate_ip_str(ip_str).is_ok()
}
pub fn is_valid_socket_addr(&self, addr_str: &str) -> bool {
self.validate_socket_addr_str(addr_str).is_ok()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum IpRange {
V4 { base: Ipv4Addr, prefix_len: u8 },
V6 { base: Ipv6Addr, prefix_len: u8 },
}
impl IpRange {
pub fn v4(base: Ipv4Addr, prefix_len: u8) -> Self {
if prefix_len > 32 {
panic!("IPv4 prefix length must be between 0 and 32, got {prefix_len}");
}
let ip_u32 = u32::from(base);
let mask = if prefix_len == 0 {
0
} else if prefix_len >= 32 {
!0u32 } else {
!0u32 << (32 - prefix_len)
};
let cleaned_ip = Ipv4Addr::from(ip_u32 & mask);
Self::V4 {
base: cleaned_ip,
prefix_len,
}
}
pub fn v6(base: Ipv6Addr, prefix_len: u8) -> Self {
if prefix_len > 128 {
panic!("IPv6 prefix length must be between 0 and 128, got {prefix_len}");
}
let segments = base.segments();
let mut cleaned_segments = [0u16; 8];
for i in 0usize..8 {
let segment_start_bit = i.saturating_mul(16);
if segment_start_bit >= prefix_len as usize {
cleaned_segments[i] = 0;
} else if segment_start_bit.saturating_add(16) <= prefix_len as usize {
cleaned_segments[i] = segments[i];
} else {
let bits_in_prefix = (prefix_len as usize).saturating_sub(segment_start_bit);
let mask = if bits_in_prefix >= 16 {
!0u16
} else {
!0u16 << (16 - bits_in_prefix)
};
cleaned_segments[i] = segments[i] & mask;
}
}
Self::V6 {
base: Ipv6Addr::from(cleaned_segments),
prefix_len,
}
}
pub fn from_cidr(cidr: &str) -> Result<Self, IpValidationError> {
let parts: Vec<&str> = cidr.split('/').collect();
if parts.len() != 2 {
return Err(IpValidationError::InvalidFormat(
"Invalid CIDR notation, expected format: ip/prefix".to_string(),
));
}
let ip_str = parts[0];
let prefix_len = parts[1]
.parse::<u8>()
.map_err(|_| IpValidationError::InvalidFormat("Invalid prefix length".to_string()))?;
match ip_str.parse::<IpAddr>() {
Ok(IpAddr::V4(ipv4)) => {
if prefix_len > 32 {
return Err(IpValidationError::InvalidFormat(format!(
"IPv4 prefix length must be between 0 and 32, got {prefix_len}"
)));
}
Ok(Self::v4(ipv4, prefix_len))
}
Ok(IpAddr::V6(ipv6)) => {
if prefix_len > 128 {
return Err(IpValidationError::InvalidFormat(format!(
"IPv6 prefix length must be between 0 and 128, got {prefix_len}"
)));
}
Ok(Self::v6(ipv6, prefix_len))
}
Err(_) => Err(IpValidationError::InvalidFormat(format!(
"Failed to parse IP address: {ip_str}"
))),
}
}
pub fn contains_ipv4(&self, ip: &Ipv4Addr) -> bool {
match self {
Self::V4 { base, prefix_len } => {
let ip_u32 = u32::from(*ip);
let base_u32 = u32::from(*base);
let mask = if *prefix_len == 0 {
0
} else if *prefix_len >= 32 {
!0u32 } else {
!0u32 << (32 - prefix_len)
};
(ip_u32 & mask) == base_u32
}
Self::V6 { .. } => false, }
}
pub fn contains_ipv6(&self, ip: &Ipv6Addr) -> bool {
match self {
Self::V4 { .. } => {
if let Some(ipv4) = ip_v6_to_v4_mapped(ip) {
self.contains_ipv4(&ipv4)
} else {
false
}
}
Self::V6 { base, prefix_len } => {
let ip_segments = ip.segments();
let base_segments = base.segments();
for i in 0usize..8 {
let segment_start_bit = i.saturating_mul(16);
if segment_start_bit >= *prefix_len as usize {
break;
}
let bits_in_prefix =
std::cmp::min(16, (*prefix_len as usize).saturating_sub(segment_start_bit));
if bits_in_prefix == 16 {
if ip_segments[i] != base_segments[i] {
return false;
}
} else {
let mask = if bits_in_prefix == 0 {
0
} else {
!0u16 << (16 - bits_in_prefix)
};
if (ip_segments[i] & mask) != (base_segments[i] & mask) {
return false;
}
}
}
true
}
}
}
}
impl std::fmt::Display for IpRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::V4 { base, prefix_len } => write!(f, "{base}/{prefix_len}"),
Self::V6 { base, prefix_len } => write!(f, "{base}/{prefix_len}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PortRange {
pub min: u16,
pub max: u16,
}
impl PortRange {
pub fn new(min: u16, max: u16) -> Self {
if min > max {
panic!("Min port must be less than or equal to max port");
}
Self { min, max }
}
pub fn contains(&self, port: u16) -> bool {
port >= self.min && port <= self.max
}
}
impl std::fmt::Display for PortRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}-{}", self.min, self.max)
}
}
fn ip_v6_to_v4_mapped(ip: &Ipv6Addr) -> Option<Ipv4Addr> {
let segments = ip.segments();
if segments[0] == 0
&& segments[1] == 0
&& segments[2] == 0
&& segments[3] == 0
&& segments[4] == 0
&& segments[5] == 0xffff
{
let a = (segments[6] >> 8) as u8;
let b = (segments[6] & 0xff) as u8;
let c = (segments[7] >> 8) as u8;
let d = (segments[7] & 0xff) as u8;
Some(Ipv4Addr::new(a, b, c, d))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ipv4_validation() {
let mut validator = IpValidator::new();
validator.block_private = true;
validator.block_loopback = true;
validator.block_documentation = false;
assert!(validator.is_valid_ip("8.8.8.8"));
assert!(validator.is_valid_ip("1.1.1.1"));
assert!(validator.is_valid_ip("203.0.113.1"));
assert!(!validator.is_valid_ip("256.0.0.1")); assert!(!validator.is_valid_ip("8.8.8")); assert!(!validator.is_valid_ip("8.8.8.8.8"));
assert!(!validator.is_valid_ip("0.0.0.0")); assert!(!validator.is_valid_ip("127.0.0.1")); assert!(!validator.is_valid_ip("10.0.0.1")); assert!(!validator.is_valid_ip("172.16.0.1")); assert!(!validator.is_valid_ip("192.168.1.1")); assert!(!validator.is_valid_ip("169.254.1.1")); assert!(!validator.is_valid_ip("224.0.0.1")); assert!(!validator.is_valid_ip("255.255.255.255")); }
#[test]
fn test_ipv6_validation() {
let mut validator = IpValidator::new();
validator.block_private = true;
validator.block_loopback = true;
validator.block_documentation = false;
assert!(validator.is_valid_ip("2001:db8:85a3::8a2e:370:7334"));
assert!(validator.is_valid_ip("2606:4700:4700::1111"));
assert!(!validator.is_valid_ip("2001:db8:85a3::8a2e:370:7334:extra")); assert!(!validator.is_valid_ip("2001:db8:85a3:::8a2e:370:7334"));
assert!(!validator.is_valid_ip("::")); assert!(!validator.is_valid_ip("::1")); assert!(!validator.is_valid_ip("fe80::1")); assert!(!validator.is_valid_ip("fc00::1")); assert!(!validator.is_valid_ip("ff00::1")); }
#[test]
fn test_permissive_validation() {
let validator = IpValidator::permissive();
assert!(validator.is_valid_ip("127.0.0.1")); assert!(validator.is_valid_ip("10.0.0.1")); assert!(validator.is_valid_ip("::1"));
assert!(!validator.is_valid_ip("999.0.0.1"));
assert!(!validator.is_valid_ip("0.0.0.0"));
assert!(!validator.is_valid_ip("ff00::1"));
}
#[test]
fn test_custom_ip_ranges() {
let validator = IpValidator::permissive()
.add_blocked_range(IpRange::from_cidr("5.5.5.0/24").unwrap())
.add_blocked_range(IpRange::from_cidr("2001:db8:1234::/48").unwrap());
assert!(!validator.is_valid_ip("5.5.5.1"));
assert!(!validator.is_valid_ip("5.5.5.255"));
assert!(validator.is_valid_ip("5.5.6.1"));
assert!(!validator.is_valid_ip("2001:db8:1234::1"));
assert!(!validator.is_valid_ip("2001:db8:1234:5::1"));
assert!(validator.is_valid_ip("2001:db8:5::1")); }
#[test]
fn test_port_validation() {
let validator = IpValidator::new();
assert!(!validator.is_valid_socket_addr("8.8.8.8:80")); assert!(validator.is_valid_socket_addr("8.8.8.8:8080"));
let custom_validator = IpValidator::new().allow_ports(8000, 9000);
assert!(!custom_validator.is_valid_socket_addr("8.8.8.8:7999")); assert!(custom_validator.is_valid_socket_addr("8.8.8.8:8000")); assert!(custom_validator.is_valid_socket_addr("8.8.8.8:8500")); assert!(custom_validator.is_valid_socket_addr("8.8.8.8:9000")); assert!(!custom_validator.is_valid_socket_addr("8.8.8.8:9001"));
let permissive = IpValidator::permissive();
assert!(permissive.is_valid_socket_addr("[2606:4700:4700::1111]:8080"));
assert!(!permissive.is_valid_socket_addr("2606:4700:4700::1111:8080"));
}
#[test]
fn test_ipv4_mapped_ipv6() {
let mut validator = IpValidator::new();
validator.block_private = false;
validator.block_loopback = false;
validator.block_documentation = false;
assert!(validator.is_valid_ip("8.8.8.8")); assert!(validator.is_valid_ip("2606:4700:4700::1111"));
validator.block_private = true;
assert!(!validator.is_valid_ip("10.0.0.1"));
let permissive = IpValidator::permissive();
assert!(permissive.is_valid_ip("10.0.0.1")); }
#[test]
fn test_ip_range_construction() {
let range = IpRange::v4(Ipv4Addr::new(192, 168, 1, 5), 24);
match range {
IpRange::V4 { base, prefix_len } => {
assert_eq!(base, Ipv4Addr::new(192, 168, 1, 0));
assert_eq!(prefix_len, 24);
}
_ => panic!("Expected IPv4 range"),
}
let range = IpRange::v6("2001:db8:1:2:3:4:5:6".parse().unwrap(), 48);
match range {
IpRange::V6 { base, prefix_len } => {
assert_eq!(base, "2001:db8:1::".parse::<Ipv6Addr>().unwrap());
assert_eq!(prefix_len, 48);
}
_ => panic!("Expected IPv6 range"),
}
}
#[test]
fn test_ipv4_cidr_edge_cases() {
let range = IpRange::v4(Ipv4Addr::new(0, 0, 0, 0), 0);
assert!(range.contains_ipv4(&Ipv4Addr::new(192, 168, 1, 1)));
assert!(range.contains_ipv4(&Ipv4Addr::new(10, 0, 0, 1)));
let range = IpRange::v4(Ipv4Addr::new(192, 168, 1, 1), 32);
assert!(range.contains_ipv4(&Ipv4Addr::new(192, 168, 1, 1)));
assert!(!range.contains_ipv4(&Ipv4Addr::new(192, 168, 1, 2)));
}
#[test]
fn test_ipv6_cidr_edge_cases() {
let range = IpRange::v6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), 0);
assert!(range.contains_ipv6(&Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)));
let addr = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
let range = IpRange::v6(addr, 128);
assert!(range.contains_ipv6(&addr));
assert!(!range.contains_ipv6(&Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)));
}
}