const PRIVATE_RANGES: [(u32, u32); 13] = [
(ip_to_num(127, 0, 0, 0), 8), (ip_to_num(10, 0, 0, 0), 8), (ip_to_num(172, 16, 0, 0), 12), (ip_to_num(192, 168, 0, 0), 16), (ip_to_num(169, 254, 0, 0), 16), (ip_to_num(0, 0, 0, 0), 8), (ip_to_num(100, 64, 0, 0), 10), (ip_to_num(192, 0, 0, 0), 24), (ip_to_num(192, 0, 2, 0), 24), (ip_to_num(198, 51, 100, 0), 24), (ip_to_num(203, 0, 113, 0), 24), (ip_to_num(224, 0, 0, 0), 4), (ip_to_num(240, 0, 0, 0), 4), ];
const fn ip_to_num(a: u32, b: u32, c: u32, d: u32) -> u32 {
(a << 24) | (b << 16) | (c << 8) | d
}
fn parse_ipv4(ip: &str) -> Option<u32> {
let mut parts = ip.splitn(4, '.');
let a = parts.next()?.parse::<u32>().ok()?;
let b = parts.next()?.parse::<u32>().ok()?;
let c = parts.next()?.parse::<u32>().ok()?;
let d = parts.next()?.parse::<u32>().ok()?;
if a > 255 || b > 255 || c > 255 || d > 255 {
return None;
}
Some(ip_to_num(a, b, c, d))
}
fn is_private_ipv4(ip: &str) -> bool {
let Some(num) = parse_ipv4(ip) else {
return true; };
for &(network, prefix) in &PRIVATE_RANGES {
let mask = if prefix == 0 {
0
} else {
0xFFFF_FFFF_u32 << (32 - prefix)
};
if (num & mask) == (network & mask) {
return true;
}
}
false
}
fn looks_like_ipv4(s: &str) -> bool {
let mut dots = 0;
for ch in s.chars() {
if ch == '.' {
dots += 1;
} else if !ch.is_ascii_digit() {
return false;
}
}
dots == 3
}
fn looks_like_obfuscated_ip(s: &str) -> bool {
if s.is_empty() {
return false;
}
if s.chars().all(|c| c.is_ascii_digit()) && s.len() > 3 {
return true;
}
let lower = s.to_ascii_lowercase();
if lower.contains("0x") && s.chars().all(|c| c.is_ascii_hexdigit() || c == '.' || c == 'x' || c == 'X') {
return true;
}
if looks_like_ipv4(s) {
for octet in s.split('.') {
if octet.len() > 1 && octet.starts_with('0') {
return true;
}
}
}
false
}
fn extract_hostname(url: &str) -> Option<&str> {
let after_scheme = url.find("://").map(|i| &url[i + 3..])?;
let after_userinfo = if let Some(at) = after_scheme.find('@') {
let before_at = &after_scheme[..at];
if !before_at.contains('/') {
&after_scheme[at + 1..]
} else {
after_scheme
}
} else {
after_scheme
};
let end = after_userinfo
.find(['/', '?', '#'])
.unwrap_or(after_userinfo.len());
let authority = &after_userinfo[..end];
if authority.starts_with('[') {
let bracket_end = authority.find(']')?;
return Some(&authority[..bracket_end + 1]);
}
match authority.rfind(':') {
Some(colon) => Some(&authority[..colon]),
None => Some(authority),
}
}
#[must_use]
pub fn is_private_host(url: &str) -> bool {
let Some(raw_host) = extract_hostname(url) else {
return true; };
if raw_host.is_empty() {
return true; }
let clean = if raw_host.starts_with('[') && raw_host.ends_with(']') {
&raw_host[1..raw_host.len() - 1]
} else {
raw_host
};
if clean.eq_ignore_ascii_case("localhost")
|| clean
.to_ascii_lowercase()
.ends_with(".localhost")
{
return true;
}
if looks_like_obfuscated_ip(clean) {
return true;
}
if looks_like_ipv4(clean) {
return is_private_ipv4(clean);
}
let lower = clean.to_ascii_lowercase();
if lower == "::1"
|| lower.starts_with("fe80:")
|| lower.starts_with("fc00:")
|| lower.starts_with("fd00:")
{
return true;
}
if let Some(mapped) = lower.strip_prefix("::ffff:") {
if looks_like_ipv4(mapped) {
return is_private_ipv4(mapped);
}
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ip_to_num_basic() {
assert_eq!(ip_to_num(127, 0, 0, 1), 0x7F00_0001);
assert_eq!(ip_to_num(192, 168, 1, 1), 0xC0A8_0101);
}
#[test]
fn malformed_url_returns_true() {
assert!(is_private_host("not-a-url"));
assert!(is_private_host(""));
assert!(is_private_host("://"));
}
#[test]
fn localhost_variants() {
assert!(is_private_host("http://localhost/path"));
assert!(is_private_host("http://LOCALHOST/path"));
assert!(is_private_host("http://foo.localhost/path"));
assert!(is_private_host("http://sub.foo.localhost"));
}
#[test]
fn ipv4_loopback() {
assert!(is_private_host("http://127.0.0.1/"));
assert!(is_private_host("http://127.255.255.255/"));
}
#[test]
fn ipv4_rfc1918() {
assert!(is_private_host("http://10.0.0.1/"));
assert!(is_private_host("http://172.16.0.1/"));
assert!(is_private_host("http://172.31.255.255/"));
assert!(is_private_host("http://192.168.0.1/"));
assert!(is_private_host("http://192.168.255.255/"));
}
#[test]
fn ipv4_link_local() {
assert!(is_private_host("http://169.254.169.254/")); }
#[test]
fn ipv4_other_reserved() {
assert!(is_private_host("http://0.0.0.0/"));
assert!(is_private_host("http://100.64.0.1/"));
assert!(is_private_host("http://192.0.0.1/"));
assert!(is_private_host("http://192.0.2.1/"));
assert!(is_private_host("http://198.51.100.1/"));
assert!(is_private_host("http://203.0.113.1/"));
assert!(is_private_host("http://224.0.0.1/")); assert!(is_private_host("http://240.0.0.1/")); }
#[test]
fn ipv4_public_is_ok() {
assert!(!is_private_host("http://8.8.8.8/"));
assert!(!is_private_host("http://1.1.1.1/"));
assert!(!is_private_host("http://93.184.216.34/")); }
#[test]
fn ipv4_172_outside_range() {
assert!(!is_private_host("http://172.32.0.1/"));
}
#[test]
fn ipv6_loopback() {
assert!(is_private_host("http://[::1]/"));
}
#[test]
fn ipv6_link_local_and_private() {
assert!(is_private_host("http://[fe80::1]/path"));
assert!(is_private_host("http://[fc00::1]/"));
assert!(is_private_host("http://[fd00::1]/"));
}
#[test]
fn ipv6_mapped_ipv4() {
assert!(is_private_host("http://[::ffff:127.0.0.1]/"));
assert!(is_private_host("http://[::ffff:10.0.0.1]/"));
assert!(is_private_host("http://[::ffff:192.168.1.1]/"));
assert!(!is_private_host("http://[::ffff:8.8.8.8]/"));
}
#[test]
fn public_hostnames_pass() {
assert!(!is_private_host("https://example.com/path"));
assert!(!is_private_host("https://sub.example.com/path?q=1"));
assert!(!is_private_host("https://example.com:8080/path"));
}
#[test]
fn strips_port() {
assert!(is_private_host("http://127.0.0.1:8080/"));
assert!(!is_private_host("http://8.8.8.8:53/"));
}
#[test]
fn hostname_with_userinfo() {
assert!(is_private_host("http://user:pass@127.0.0.1/"));
}
#[test]
fn hex_ip_blocked() {
assert!(is_private_host("http://0x7f.0.0.1/"));
assert!(is_private_host("http://0x7f000001/"));
}
#[test]
fn octal_ip_blocked() {
assert!(is_private_host("http://0177.0.0.1/"));
}
#[test]
fn bare_integer_ip_blocked() {
assert!(is_private_host("http://2130706433/")); }
#[test]
fn zero_zero_zero_zero() {
assert!(is_private_host("http://0.0.0.0/"));
assert!(is_private_host("http://0.0.0.0:8080/"));
}
#[test]
fn hostname_no_trailing_slash() {
assert!(!is_private_host("http://example.com"));
assert!(is_private_host("http://localhost"));
}
}