pub fn is_private_ip(host: &str) -> bool {
let clean_host = if host.starts_with('[') {
host.trim_start_matches('[')
.split(']')
.next()
.unwrap_or(host)
} else if host.contains("::") || host.matches(':').count() > 1 {
host
} else {
host.rsplit_once(':').map(|(h, _)| h).unwrap_or(host)
};
if clean_host == "localhost" || clean_host == "0.0.0.0" {
return true;
}
if clean_host == "::1" || clean_host == "::0" || clean_host == "::" {
return true;
}
if let Some(mapped) = clean_host.strip_prefix("::ffff:") {
return is_private_ipv4(mapped);
}
if clean_host.starts_with("fc") || clean_host.starts_with("fd") {
return true;
}
if clean_host.starts_with("fe80") {
return true;
}
is_private_ipv4(clean_host)
}
fn is_private_ipv4(host: &str) -> bool {
let octets: Vec<u8> = host.split('.').filter_map(|s| s.parse().ok()).collect();
if octets.len() == 4 {
match (octets[0], octets[1]) {
(127, _) => true, (10, _) => true, (172, 16..=31) => true, (192, 168) => true, (169, 254) => true, (0, _) => true, _ => false,
}
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_localhost() {
assert!(is_private_ip("localhost"));
assert!(is_private_ip("localhost:8080"));
}
#[test]
fn blocks_loopback_127() {
assert!(is_private_ip("127.0.0.1"));
assert!(is_private_ip("127.0.0.1:80"));
assert!(is_private_ip("127.255.255.255"));
assert!(is_private_ip("127.0.0.2"));
}
#[test]
fn blocks_ipv6_loopback() {
assert!(is_private_ip("::1"));
assert!(is_private_ip("[::1]:8080"));
}
#[test]
fn blocks_ipv6_unspecified() {
assert!(is_private_ip("::0"));
assert!(is_private_ip("::"));
}
#[test]
fn blocks_unspecified() {
assert!(is_private_ip("0.0.0.0"));
assert!(is_private_ip("0.0.0.0:443"));
}
#[test]
fn blocks_10_network() {
assert!(is_private_ip("10.0.0.1"));
assert!(is_private_ip("10.255.255.255"));
assert!(is_private_ip("10.0.0.1:9090"));
}
#[test]
fn blocks_172_16_network() {
assert!(is_private_ip("172.16.0.1"));
assert!(is_private_ip("172.31.255.255"));
assert!(is_private_ip("172.20.0.1:443"));
}
#[test]
fn allows_172_outside_range() {
assert!(!is_private_ip("172.15.0.1"));
assert!(!is_private_ip("172.32.0.1"));
}
#[test]
fn blocks_192_168_network() {
assert!(is_private_ip("192.168.0.1"));
assert!(is_private_ip("192.168.1.100:8080"));
assert!(is_private_ip("192.168.255.255"));
}
#[test]
fn blocks_link_local() {
assert!(is_private_ip("169.254.0.1"));
assert!(is_private_ip("169.254.169.254")); assert!(is_private_ip("169.254.169.254:80"));
}
#[test]
fn blocks_zero_network() {
assert!(is_private_ip("0.1.2.3"));
assert!(is_private_ip("0.255.255.255"));
}
#[test]
fn blocks_ipv4_mapped_ipv6() {
assert!(is_private_ip("::ffff:127.0.0.1"));
assert!(is_private_ip("::ffff:10.0.0.1"));
assert!(is_private_ip("::ffff:192.168.1.1"));
}
#[test]
fn allows_ipv4_mapped_ipv6_public() {
assert!(!is_private_ip("::ffff:8.8.8.8"));
}
#[test]
fn blocks_ipv6_unique_local() {
assert!(is_private_ip("fc00::1"));
assert!(is_private_ip("fd12::1"));
assert!(is_private_ip("fdab:cdef::1"));
}
#[test]
fn blocks_ipv6_link_local() {
assert!(is_private_ip("fe80::1"));
assert!(is_private_ip("fe80::abcd:1234"));
}
#[test]
fn blocks_bracketed_ipv6_with_port() {
assert!(is_private_ip("[::1]:8080"));
assert!(is_private_ip("[fc00::1]:443"));
assert!(is_private_ip("[fe80::1]:80"));
}
#[test]
fn allows_public_ips() {
assert!(!is_private_ip("8.8.8.8"));
assert!(!is_private_ip("1.1.1.1"));
assert!(!is_private_ip("203.0.113.1"));
assert!(!is_private_ip("93.184.216.34:443"));
}
#[test]
fn allows_public_ipv6() {
assert!(!is_private_ip("2001:db8::1"));
}
#[test]
fn allows_public_hostnames() {
assert!(!is_private_ip("example.com"));
assert!(!is_private_ip("example.com:443"));
assert!(!is_private_ip("api.github.com"));
}
}