use std::net::IpAddr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Locality {
Loopback,
Private,
LinkLocal,
Public,
Unspecified,
}
pub(crate) fn classify(ip: IpAddr) -> Locality {
match canonicalize(ip) {
IpAddr::V4(v4) => {
if v4.is_loopback() {
Locality::Loopback
} else if v4.is_unspecified() {
Locality::Unspecified
} else if v4.is_link_local() {
Locality::LinkLocal
} else if v4.is_private() {
Locality::Private
} else {
Locality::Public
}
}
IpAddr::V6(v6) => {
let first = v6.segments()[0];
if v6.is_loopback() {
Locality::Loopback
} else if v6.is_unspecified() {
Locality::Unspecified
} else if first & 0xFFC0 == 0xFE80 {
Locality::LinkLocal
} else if first & 0xFE00 == 0xFC00 {
Locality::Private
} else {
Locality::Public
}
}
}
}
pub(crate) const fn admits(locality: Locality, allow_private: bool) -> bool {
match locality {
Locality::Loopback => true,
Locality::Private => allow_private,
Locality::LinkLocal | Locality::Public | Locality::Unspecified => false,
}
}
fn canonicalize(ip: IpAddr) -> IpAddr {
match ip {
IpAddr::V6(v6) => v6.to_ipv4_mapped().map_or(ip, IpAddr::V4),
IpAddr::V4(_) => ip,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ip(s: &str) -> IpAddr {
s.parse().expect("test address")
}
#[test]
fn loopback_is_admitted_without_any_flag() {
for s in ["127.0.0.1", "127.0.0.2", "127.255.255.254", "::1"] {
assert_eq!(classify(ip(s)), Locality::Loopback, "{s}");
assert!(admits(classify(ip(s)), false), "{s} needs no flag");
}
}
#[test]
fn the_private_ranges_need_the_flag_and_are_refused_without_it() {
for s in [
"10.0.0.1",
"10.255.255.255",
"172.16.0.1",
"172.31.255.255",
"192.168.1.5",
"fc00::1",
"fd12:3456::1",
] {
assert_eq!(classify(ip(s)), Locality::Private, "{s}");
assert!(!admits(classify(ip(s)), false), "{s} must need the flag");
assert!(
admits(classify(ip(s)), true),
"{s} must be admitted with it"
);
}
}
#[test]
fn the_private_range_boundaries_are_where_rfc_1918_puts_them() {
assert_eq!(classify(ip("172.15.255.255")), Locality::Public);
assert_eq!(classify(ip("172.16.0.0")), Locality::Private);
assert_eq!(classify(ip("172.31.255.255")), Locality::Private);
assert_eq!(classify(ip("172.32.0.0")), Locality::Public);
}
#[test]
fn link_local_is_refused_however_the_flag_is_set() {
for s in ["169.254.1.1", "169.254.169.254", "fe80::1", "febf::1"] {
assert_eq!(classify(ip(s)), Locality::LinkLocal, "{s}");
assert!(!admits(classify(ip(s)), false), "{s}");
assert!(!admits(classify(ip(s)), true), "{s} even with the flag");
}
}
#[test]
fn an_ipv4_mapped_metadata_address_is_still_link_local() {
let mapped = ip("::ffff:169.254.169.254");
assert!(mapped.is_ipv6(), "the input really is an IPv6 address");
assert_eq!(classify(mapped), Locality::LinkLocal);
assert!(!admits(classify(mapped), true), "even with the flag set");
assert_eq!(classify(ip("::ffff:127.0.0.1")), Locality::Loopback);
assert_eq!(classify(ip("::ffff:192.168.1.5")), Locality::Private);
assert_eq!(classify(ip("::ffff:8.8.8.8")), Locality::Public);
}
#[test]
fn an_ipv4_compatible_address_is_not_unwrapped() {
assert_eq!(classify(ip("::1")), Locality::Loopback);
assert_eq!(classify(ip("::2")), Locality::Public);
}
#[test]
fn the_unspecified_address_names_nothing_and_is_refused() {
for s in ["0.0.0.0", "::"] {
assert_eq!(classify(ip(s)), Locality::Unspecified, "{s}");
assert!(!admits(classify(ip(s)), false), "{s}");
assert!(!admits(classify(ip(s)), true), "{s} even with the flag");
}
}
#[test]
fn a_public_address_is_refused_however_the_flag_is_set() {
for s in [
"8.8.8.8",
"1.1.1.1",
"203.0.113.1",
"2606:4700:4700::1111",
"2001:db8::1",
] {
assert_eq!(classify(ip(s)), Locality::Public, "{s}");
assert!(!admits(classify(ip(s)), false), "{s}");
assert!(!admits(classify(ip(s)), true), "{s} even with the flag");
}
}
#[test]
fn the_flag_moves_private_and_nothing_else() {
for locality in [
Locality::Loopback,
Locality::Private,
Locality::LinkLocal,
Locality::Public,
Locality::Unspecified,
] {
let changed = admits(locality, true) != admits(locality, false);
assert_eq!(
changed,
locality == Locality::Private,
"{locality:?} must{} be affected by the flag",
if locality == Locality::Private {
""
} else {
" not"
}
);
}
}
}