use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use super::network::normalize_mapped_ipv4;
const LOOPBACK: u16 = 0x0001;
const PRIVATE_NETWORK: u16 = 0x0002;
const LINK_LOCAL: u16 = 0x0004;
const SHARED_ADDRESS_SPACE: u16 = 0x0008;
const THIS_HOST: u16 = 0x0010;
const UNSPECIFIED: u16 = 0x0020;
const MULTICAST: u16 = 0x0040;
const SPECIAL_USE: u16 = 0x0080;
const CLOUD_METADATA: u16 = 0x0100;
const NON_PUBLIC: u16 = 0x0200;
const V4_SPECIAL_USE: &[V4Block] = &[
V4Block::new(Ipv4Addr::new(192, 0, 0, 0), 24), V4Block::new(Ipv4Addr::new(192, 0, 2, 0), 24), V4Block::new(Ipv4Addr::new(198, 51, 100, 0), 24), V4Block::new(Ipv4Addr::new(203, 0, 113, 0), 24), V4Block::new(Ipv4Addr::new(192, 88, 99, 0), 24), V4Block::new(Ipv4Addr::new(198, 18, 0, 0), 15), V4Block::new(Ipv4Addr::new(240, 0, 0, 0), 4), ];
const V4_GLOBALLY_REACHABLE_EXCEPTIONS: &[Ipv4Addr] = &[
Ipv4Addr::new(192, 0, 0, 9), Ipv4Addr::new(192, 0, 0, 10), ];
const V6_SPECIAL_USE: &[V6Block] = &[
V6Block::new(Ipv6Addr::new(0x2001, 0, 0, 0, 0, 0, 0, 0), 23),
V6Block::new(Ipv6Addr::new(0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0), 32), V6Block::new(Ipv6Addr::new(0x2002, 0, 0, 0, 0, 0, 0, 0), 16), V6Block::new(Ipv6Addr::new(0x3FFF, 0, 0, 0, 0, 0, 0, 0), 20), V6Block::new(Ipv6Addr::new(0x5F00, 0, 0, 0, 0, 0, 0, 0), 16), V6Block::new(Ipv6Addr::new(0x0064, 0xFF9B, 0x0001, 0, 0, 0, 0, 0), 48), V6Block::new(Ipv6Addr::new(0x0100, 0, 0, 0, 0, 0, 0, 0), 64), V6Block::new(Ipv6Addr::new(0x0100, 0, 0, 1, 0, 0, 0, 0), 64), ];
const V6_GLOBALLY_REACHABLE_EXCEPTIONS: &[V6Block] = &[
V6Block::new(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 1), 128), V6Block::new(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 2), 128), V6Block::new(Ipv6Addr::new(0x2001, 1, 0, 0, 0, 0, 0, 3), 128), V6Block::new(Ipv6Addr::new(0x2001, 3, 0, 0, 0, 0, 0, 0), 32), V6Block::new(Ipv6Addr::new(0x2001, 4, 0x0112, 0, 0, 0, 0, 0), 48), V6Block::new(Ipv6Addr::new(0x2001, 0x0020, 0, 0, 0, 0, 0, 0), 28), V6Block::new(Ipv6Addr::new(0x2001, 0x0030, 0, 0, 0, 0, 0, 0), 28), ];
const V4_CLOUD_METADATA: &[Ipv4Addr] = &[
Ipv4Addr::new(169, 254, 169, 254),
Ipv4Addr::new(169, 254, 170, 2), Ipv4Addr::new(169, 254, 170, 23), Ipv4Addr::new(100, 100, 100, 200), Ipv4Addr::new(169, 254, 0, 23), Ipv4Addr::new(169, 254, 10, 10), ];
const V6_CLOUD_METADATA: &[Ipv6Addr] = &[
Ipv6Addr::new(0xFD00, 0x0EC2, 0, 0, 0, 0, 0, 0x0254), Ipv6Addr::new(0xFD00, 0x0EC2, 0, 0, 0, 0, 0, 0x0023), Ipv6Addr::new(0xFD20, 0x00CE, 0, 0, 0, 0, 0, 0x0254), Ipv6Addr::new(0xFE80, 0, 0, 0, 0, 0, 0xA9FE, 0xA9FE), ];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IpClassification {
flags: u16,
normalized: IpAddr,
}
impl IpClassification {
#[must_use]
pub const fn is_cloud_metadata(self) -> bool {
(self.flags & CLOUD_METADATA) != 0
}
#[must_use]
pub const fn is_link_local(self) -> bool {
(self.flags & LINK_LOCAL) != 0
}
#[must_use]
pub const fn is_loopback(self) -> bool {
(self.flags & LOOPBACK) != 0
}
#[must_use]
pub const fn is_multicast(self) -> bool {
(self.flags & MULTICAST) != 0
}
#[must_use]
pub const fn is_non_public(self) -> bool {
(self.flags & NON_PUBLIC) != 0
}
#[must_use]
pub const fn is_private_network(self) -> bool {
(self.flags & PRIVATE_NETWORK) != 0
}
#[must_use]
pub const fn is_shared_address_space(self) -> bool {
(self.flags & SHARED_ADDRESS_SPACE) != 0
}
#[must_use]
pub const fn is_special_use(self) -> bool {
(self.flags & SPECIAL_USE) != 0
}
#[must_use]
pub const fn is_this_host(self) -> bool {
(self.flags & THIS_HOST) != 0
}
#[must_use]
pub const fn is_unspecified(self) -> bool {
(self.flags & UNSPECIFIED) != 0
}
#[must_use]
pub const fn normalized(self) -> IpAddr {
self.normalized
}
}
#[must_use]
pub fn classify_ip(ip: &IpAddr) -> IpClassification {
let normalized = normalize_mapped_ipv4(*ip);
if let IpAddr::V6(v6) = normalized
&& let Some(embedded) = nat64_embedded_ipv4(v6)
{
return classify_ip(&IpAddr::V4(embedded));
}
classify_normalized(normalized)
}
#[must_use]
pub(crate) fn classify_without_nat64(ip: &IpAddr) -> IpClassification {
classify_normalized(normalize_mapped_ipv4(*ip))
}
fn classify_normalized(normalized: IpAddr) -> IpClassification {
let flags = match normalized {
IpAddr::V4(v4) => classify_v4(v4),
IpAddr::V6(v6) => classify_v6(v6),
};
IpClassification { flags, normalized }
}
fn classify_v4(v4: Ipv4Addr) -> u16 {
let mut flags = 0;
if v4.is_loopback() {
flags |= LOOPBACK;
}
if v4.is_private() {
flags |= PRIVATE_NETWORK;
}
if v4.is_link_local() {
flags |= LINK_LOCAL;
}
if is_shared_v4(v4) {
flags |= SHARED_ADDRESS_SPACE;
}
if v4.octets()[0] == 0 {
flags |= THIS_HOST;
}
if v4.is_unspecified() {
flags |= UNSPECIFIED;
}
if v4.is_multicast() {
flags |= MULTICAST;
}
if is_special_use_v4(v4) {
flags |= SPECIAL_USE;
}
if V4_CLOUD_METADATA.contains(&v4) {
flags |= CLOUD_METADATA;
}
with_non_public(flags)
}
fn classify_v6(v6: Ipv6Addr) -> u16 {
let mut flags = 0;
if v6.is_loopback() {
flags |= LOOPBACK;
}
if v6.is_unspecified() {
flags |= UNSPECIFIED;
}
if is_unique_local_v6(v6) {
flags |= PRIVATE_NETWORK;
}
if is_link_local_v6(v6) {
flags |= LINK_LOCAL;
}
if v6.is_multicast() {
flags |= MULTICAST;
}
if is_special_use_v6(v6) {
flags |= SPECIAL_USE;
}
if V6_CLOUD_METADATA.contains(&v6) {
flags |= CLOUD_METADATA;
}
with_non_public(flags)
}
const fn with_non_public(flags: u16) -> u16 {
if flags == 0 { flags } else { flags | NON_PUBLIC }
}
const fn is_shared_v4(v4: Ipv4Addr) -> bool {
(v4.to_bits() & 0xFFC0_0000) == 0x6440_0000
}
const fn is_unique_local_v6(v6: Ipv6Addr) -> bool {
(v6.segments()[0] & 0xFE00) == 0xFC00
}
const fn is_link_local_v6(v6: Ipv6Addr) -> bool {
(v6.segments()[0] & 0xFFC0) == 0xFE80
}
const fn is_site_local_v6(v6: Ipv6Addr) -> bool {
(v6.segments()[0] & 0xFFC0) == 0xFEC0
}
fn nat64_embedded_ipv4(v6: Ipv6Addr) -> Option<Ipv4Addr> {
let seg = v6.segments();
let in_well_known_prefix =
seg[0] == 0x0064 && seg[1] == 0xFF9B && seg[2] == 0 && seg[3] == 0 && seg[4] == 0 && seg[5] == 0;
in_well_known_prefix.then(|| Ipv4Addr::from((u32::from(seg[6]) << 16) | u32::from(seg[7])))
}
fn is_special_use_v4(v4: Ipv4Addr) -> bool {
if V4_GLOBALLY_REACHABLE_EXCEPTIONS.contains(&v4) {
return false;
}
V4_SPECIAL_USE.iter().any(|block| block.contains(v4))
}
fn is_special_use_v6(v6: Ipv6Addr) -> bool {
if V6_GLOBALLY_REACHABLE_EXCEPTIONS.iter().any(|block| block.contains(v6)) {
return false;
}
is_site_local_v6(v6) || V6_SPECIAL_USE.iter().any(|block| block.contains(v6))
}
struct V4Block {
net: Ipv4Addr,
prefix: u8,
}
impl V4Block {
const fn new(net: Ipv4Addr, prefix: u8) -> Self {
Self { net, prefix }
}
const fn contains(&self, addr: Ipv4Addr) -> bool {
let mask = if self.prefix == 0 {
0
} else {
u32::MAX << 32_u8.saturating_sub(self.prefix)
};
(addr.to_bits() & mask) == (self.net.to_bits() & mask)
}
}
struct V6Block {
net: Ipv6Addr,
prefix: u8,
}
impl V6Block {
const fn new(net: Ipv6Addr, prefix: u8) -> Self {
Self { net, prefix }
}
const fn contains(&self, addr: Ipv6Addr) -> bool {
let mask = if self.prefix == 0 {
0
} else {
u128::MAX << 128_u8.saturating_sub(self.prefix)
};
(addr.to_bits() & mask) == (self.net.to_bits() & mask)
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::min_ident_chars,
reason = "tests use unwrap/indexing for brevity"
)]
mod tests {
use super::*;
#[test]
fn loopback() {
for addr in ["127.0.0.1", "127.0.0.0", "127.255.255.255", "::1"] {
assert!(classify(addr).is_loopback(), "{addr} is loopback");
assert!(classify(addr).is_non_public(), "{addr} is non-public");
}
assert!(!classify("128.0.0.0").is_loopback(), "128.0.0.0 is not loopback");
assert!(!classify("126.255.255.255").is_loopback(), "126.* is not loopback");
}
#[test]
fn private_network_v4_boundaries() {
for addr in [
"10.0.0.0",
"10.255.255.255",
"172.16.0.0",
"172.31.255.255",
"192.168.0.0",
"192.168.255.255",
] {
assert!(classify(addr).is_private_network(), "{addr} is RFC 1918");
}
for addr in [
"9.255.255.255",
"11.0.0.0",
"172.15.255.255",
"172.32.0.0",
"192.167.255.255",
"192.169.0.0",
] {
assert!(!classify(addr).is_private_network(), "{addr} is not RFC 1918");
}
}
#[test]
fn private_network_v6_ula() {
for addr in [
"fc00::",
"fc00::1",
"fd00::1",
"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
] {
assert!(classify(addr).is_private_network(), "{addr} is ULA");
}
assert!(!classify("fbff::1").is_private_network(), "fbff:: is below fc00::/7");
assert!(!classify("fe00::1").is_private_network(), "fe00:: is above fc00::/7");
}
#[test]
fn link_local_boundaries() {
assert!(classify("169.254.0.0").is_link_local(), "169.254.0.0 is link-local");
assert!(
classify("169.254.255.255").is_link_local(),
"169.254.255.255 is link-local"
);
assert!(
!classify("169.253.255.255").is_link_local(),
"169.253.* is not link-local"
);
assert!(!classify("169.255.0.0").is_link_local(), "169.255.* is not link-local");
assert!(classify("fe80::1").is_link_local(), "fe80::1 is link-local");
assert!(
classify("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff").is_link_local(),
"febf:* is link-local"
);
assert!(!classify("fec0::1").is_link_local(), "fec0:: is above fe80::/10");
}
#[test]
fn shared_address_space_boundaries() {
assert!(classify("100.64.0.0").is_shared_address_space(), "100.64.0.0 is CGNAT");
assert!(
classify("100.127.255.255").is_shared_address_space(),
"100.127.* is CGNAT"
);
assert!(
!classify("100.63.255.255").is_shared_address_space(),
"100.63.* is below CGNAT"
);
assert!(
!classify("100.128.0.0").is_shared_address_space(),
"100.128.* is above CGNAT"
);
}
#[test]
fn this_host_and_unspecified() {
assert_exact("0.0.0.0", THIS_HOST | UNSPECIFIED);
assert!(classify("0.0.0.1").is_this_host(), "0.0.0.1 is this-host");
assert!(!classify("0.0.0.1").is_unspecified(), "0.0.0.1 is not unspecified");
assert!(classify("0.255.255.255").is_this_host(), "0.255.* is this-host");
assert!(!classify("1.0.0.0").is_this_host(), "1.0.0.0 is not this-host");
assert_exact("::", UNSPECIFIED);
assert!(!classify("::").is_this_host(), ":: is unspecified, not this-host");
}
#[test]
fn multicast() {
assert!(classify("224.0.0.1").is_multicast(), "224.0.0.1 is multicast");
assert!(classify("239.255.255.255").is_multicast(), "239.* is multicast");
assert!(!classify("223.255.255.255").is_multicast(), "223.* is not multicast");
assert!(classify("ff02::1").is_multicast(), "ff02::1 is multicast");
assert!(classify("224.0.0.1").is_non_public(), "multicast is non-public");
assert!(!classify("224.0.0.1").is_special_use(), "multicast is not special-use");
}
#[test]
fn special_use_v4_documentation() {
for block in ["192.0.2", "198.51.100", "203.0.113"] {
assert!(
classify(&format!("{block}.0")).is_special_use(),
"{block}.0 is documentation"
);
assert!(
classify(&format!("{block}.255")).is_special_use(),
"{block}.255 is documentation"
);
assert!(
classify(&format!("{block}.1")).is_non_public(),
"{block}.1 is non-public"
);
}
assert!(!classify("192.0.3.0").is_special_use(), "192.0.3.0 is public");
assert!(!classify("198.51.101.0").is_special_use(), "198.51.101.0 is public");
}
#[test]
fn special_use_v4_other_blocks() {
assert!(classify("198.18.0.0").is_special_use(), "198.18.0.0 is benchmarking");
assert!(
classify("198.19.255.255").is_special_use(),
"198.19.255.255 is benchmarking"
);
assert!(!classify("198.17.255.255").is_special_use(), "198.17.* is public");
assert!(!classify("198.20.0.0").is_special_use(), "198.20.* is public");
assert!(classify("240.0.0.0").is_special_use(), "240.0.0.0 is reserved");
assert!(classify("255.255.255.255").is_special_use(), "broadcast is reserved");
assert!(
!classify("239.255.255.255").is_special_use(),
"239.* is multicast, not reserved"
);
assert!(
classify("192.88.99.1").is_special_use(),
"192.88.99.1 is deprecated 6to4 anycast"
);
assert!(
classify("192.0.0.0").is_special_use(),
"192.0.0.0 is protocol assignments"
);
assert!(
classify("192.0.0.255").is_special_use(),
"192.0.0.255 is protocol assignments"
);
assert!(
classify("192.0.0.170").is_special_use(),
"192.0.0.170 is NAT64/DNS64 discovery"
);
}
#[test]
fn globally_reachable_islands_v4() {
for addr in ["192.0.0.9", "192.0.0.10"] {
let c = classify(addr);
assert!(
!c.is_special_use(),
"{addr} is a globally reachable exception in 192.0.0.0/24"
);
assert!(!c.is_non_public(), "{addr} is globally reachable");
}
for addr in ["192.31.196.1", "192.52.193.1", "192.175.48.1"] {
assert!(
!classify(addr).is_non_public(),
"{addr} is a standalone globally reachable delegation"
);
}
}
#[test]
fn special_use_v4_boundaries_from_fixture() {
assert_special_use_cases(V4_SPECIAL_USE_FIXTURE);
}
#[test]
fn special_use_v6_boundaries_from_fixture() {
assert_special_use_cases(V6_SPECIAL_USE_FIXTURE);
}
#[test]
fn special_use_v4_fixture_matches_table() {
assert_eq!(
V4_SPECIAL_USE_FIXTURE.len(),
V4_SPECIAL_USE.len(),
"one independent v4 fixture case per table block"
);
for block in V4_SPECIAL_USE {
let want = (IpAddr::V4(block.net), block.prefix);
assert!(
V4_SPECIAL_USE_FIXTURE.iter().any(|case| parse_cidr(case.cidr) == want),
"{}/{} has no independent fixture case",
block.net,
block.prefix
);
}
}
#[test]
fn special_use_v6_fixture_matches_table() {
assert_eq!(
V6_SPECIAL_USE_FIXTURE.len(),
V6_SPECIAL_USE.len(),
"one independent v6 fixture case per table block"
);
for block in V6_SPECIAL_USE {
let want = (IpAddr::V6(block.net), block.prefix);
assert!(
V6_SPECIAL_USE_FIXTURE.iter().any(|case| parse_cidr(case.cidr) == want),
"{}/{} has no independent fixture case",
block.net,
block.prefix
);
}
}
#[test]
fn site_local_v6_deprecated() {
for addr in ["fec0::", "fec0::1", "feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"] {
assert!(classify(addr).is_special_use(), "{addr} is deprecated site-local");
assert!(classify(addr).is_non_public(), "{addr} is non-public");
}
assert!(
!classify("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff").is_special_use(),
"febf:* is link-local, just below fec0::/10"
);
assert!(
!classify("ff00::").is_special_use(),
"ff00:: is multicast, just above fec0::/10"
);
}
#[test]
fn globally_reachable_islands_v6() {
for (addr, name) in [
("2001:1::1", "Port Control Protocol Anycast"),
("2001:1::2", "TURN Anycast"),
("2001:1::3", "DNS-SD SRP Anycast"),
("2001:3::1", "AMT"),
("2001:4:112::1", "AS112-v6"),
("2001:20::1", "ORCHIDv2"),
("2001:30::1", "Drone Remote ID"),
("2620:4f:8000::1", "Direct Delegation AS112"),
] {
let c = classify(addr);
assert!(!c.is_special_use(), "{addr} is a globally reachable exception ({name})");
assert!(!c.is_non_public(), "{addr} is globally reachable ({name})");
}
}
#[test]
fn public_addresses() {
for addr in [
"8.8.8.8",
"1.1.1.1",
"203.0.114.1",
"2001:4860:4860::8888",
"2606:4700::1",
] {
let c = classify(addr);
assert!(!c.is_non_public(), "{addr} is public");
assert_eq!(c.flags, 0, "{addr} has no category flags");
}
}
#[test]
fn ipv4_mapped_normalization() {
let c = classify("::ffff:10.0.0.1");
assert!(c.is_private_network(), "mapped 10.0.0.1 is private");
assert_eq!(
c.normalized(),
IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
"normalized to plain v4"
);
assert!(!classify("::ffff:8.8.8.8").is_non_public(), "mapped 8.8.8.8 is public");
}
#[test]
fn nat64_wrapped_public_private_and_metadata() {
let public = classify("64:ff9b::8.8.8.8");
assert!(!public.is_non_public(), "NAT64-wrapped 8.8.8.8 is public");
assert_eq!(
public.normalized(),
IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)),
"reduces to embedded v4"
);
let private = classify("64:ff9b::10.0.0.1");
assert!(private.is_private_network(), "NAT64-wrapped 10.0.0.1 is private");
let loopback = classify("64:ff9b::127.0.0.1");
assert!(loopback.is_loopback(), "NAT64-wrapped 127.0.0.1 is loopback");
for wrapped in [
"64:ff9b::169.254.169.254",
"64:ff9b::169.254.170.2",
"64:ff9b::169.254.170.23",
"64:ff9b::169.254.0.23",
"64:ff9b::169.254.10.10",
] {
let meta = classify(wrapped);
assert!(meta.is_cloud_metadata(), "{wrapped}: NAT64-wrapped metadata endpoint");
assert!(meta.is_link_local(), "{wrapped}: NAT64-wrapped metadata is link-local");
}
}
#[test]
fn cloud_metadata_endpoints() {
for (addr, range, name) in [
("169.254.169.254", LINK_LOCAL, "multi-cloud IMDS"),
("169.254.170.2", LINK_LOCAL, "AWS ECS task credentials"),
("169.254.170.23", LINK_LOCAL, "AWS EKS Pod Identity (v4)"),
("100.100.100.200", SHARED_ADDRESS_SPACE, "Alibaba"),
("169.254.0.23", LINK_LOCAL, "Tencent CVM (VPC / intl)"),
("169.254.10.10", LINK_LOCAL, "Tencent CVM (basic network / mainland)"),
("fd00:ec2::254", PRIVATE_NETWORK, "AWS EC2 IMDS (v6, ULA)"),
("fd00:ec2::23", PRIVATE_NETWORK, "AWS EKS Pod Identity (v6, ULA)"),
("fd20:ce::254", PRIVATE_NETWORK, "GCP metadata (v6, ULA)"),
(
"fe80::a9fe:a9fe",
LINK_LOCAL,
"OpenStack Nova metadata (v6, link-local)",
),
] {
let c = classify(addr);
assert!(c.is_cloud_metadata(), "{addr} is a cloud metadata endpoint ({name})");
assert_eq!(
c.flags & range,
range,
"{addr} carries its non-public range flag ({name})"
);
}
}
#[test]
fn cloud_metadata_near_miss_neighbors_are_not_metadata() {
for (neighbor, why) in [
("169.254.169.253", "borders AWS IMDS"),
("169.254.0.22", "borders Tencent .0.23"),
("169.254.20.10", "is Tencent TKE NodeLocal DNS, not metadata"),
] {
assert!(
!classify(neighbor).is_cloud_metadata(),
"{neighbor} is a link-local neighbor, not a documented metadata endpoint ({why})"
);
}
}
#[test]
fn is_non_public_is_the_union() {
for addr in [
"127.0.0.1",
"10.0.0.1",
"169.254.0.1",
"100.64.0.1",
"0.0.0.0",
"::",
"224.0.0.1",
"192.0.2.1",
"169.254.169.254",
"::1",
"fc00::1",
"fe80::1",
"fec0::1",
"ff02::1",
] {
assert!(classify(addr).is_non_public(), "{addr} must be non-public");
}
}
fn classify(s: &str) -> IpClassification {
classify_ip(&s.parse().unwrap())
}
struct BlockCase {
above: Option<&'static str>,
below: Option<&'static str>,
cidr: &'static str,
first: &'static str,
last: &'static str,
}
const V4_SPECIAL_USE_FIXTURE: &[BlockCase] = &[
BlockCase {
above: Some("192.0.1.0"),
below: Some("191.255.255.255"),
cidr: "192.0.0.0/24",
first: "192.0.0.0",
last: "192.0.0.255",
},
BlockCase {
above: Some("192.0.3.0"),
below: Some("192.0.1.255"),
cidr: "192.0.2.0/24",
first: "192.0.2.0",
last: "192.0.2.255",
},
BlockCase {
above: Some("198.51.101.0"),
below: Some("198.51.99.255"),
cidr: "198.51.100.0/24",
first: "198.51.100.0",
last: "198.51.100.255",
},
BlockCase {
above: Some("203.0.114.0"),
below: Some("203.0.112.255"),
cidr: "203.0.113.0/24",
first: "203.0.113.0",
last: "203.0.113.255",
},
BlockCase {
above: Some("192.88.100.0"),
below: Some("192.88.98.255"),
cidr: "192.88.99.0/24",
first: "192.88.99.0",
last: "192.88.99.255",
},
BlockCase {
above: Some("198.20.0.0"),
below: Some("198.17.255.255"),
cidr: "198.18.0.0/15",
first: "198.18.0.0",
last: "198.19.255.255",
},
BlockCase {
above: None,
below: Some("239.255.255.255"),
cidr: "240.0.0.0/4",
first: "240.0.0.0",
last: "255.255.255.255",
},
];
const V6_SPECIAL_USE_FIXTURE: &[BlockCase] = &[
BlockCase {
above: Some("2001:200::"),
below: Some("2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
cidr: "2001::/23",
first: "2001::",
last: "2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff",
},
BlockCase {
above: Some("2001:db9::"),
below: Some("2001:db7:ffff:ffff:ffff:ffff:ffff:ffff"),
cidr: "2001:db8::/32",
first: "2001:db8::",
last: "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff",
},
BlockCase {
above: Some("2003::"),
below: Some("2001:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
cidr: "2002::/16",
first: "2002::",
last: "2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
},
BlockCase {
above: Some("3fff:1000::"),
below: Some("3ffe:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
cidr: "3fff::/20",
first: "3fff::",
last: "3fff:fff:ffff:ffff:ffff:ffff:ffff:ffff",
},
BlockCase {
above: Some("5f01::"),
below: Some("5eff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
cidr: "5f00::/16",
first: "5f00::",
last: "5f00:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
},
BlockCase {
above: Some("64:ff9b:2::"),
below: Some("64:ff9b:0:ffff:ffff:ffff:ffff:ffff"),
cidr: "64:ff9b:1::/48",
first: "64:ff9b:1::",
last: "64:ff9b:1:ffff:ffff:ffff:ffff:ffff",
},
BlockCase {
above: None,
below: Some("ff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
cidr: "100::/64",
first: "100::",
last: "100::ffff:ffff:ffff:ffff",
},
BlockCase {
above: Some("100:0:0:2::"),
below: None,
cidr: "100:0:0:1::/64",
first: "100:0:0:1::",
last: "100:0:0:1:ffff:ffff:ffff:ffff",
},
];
fn assert_special_use_cases(cases: &[BlockCase]) {
for case in cases {
for edge in [case.first, case.last] {
let c = classify(edge);
assert!(c.is_special_use(), "{edge} is special-use ({})", case.cidr);
assert!(c.is_non_public(), "{edge} is non-public ({})", case.cidr);
}
for neighbor in [case.below, case.above].into_iter().flatten() {
assert!(
!classify(neighbor).is_special_use(),
"{neighbor} borders {} but is public",
case.cidr
);
}
}
}
fn parse_cidr(cidr: &str) -> (IpAddr, u8) {
let (net, prefix) = cidr.split_once('/').unwrap();
(net.parse().unwrap(), prefix.parse().unwrap())
}
fn assert_exact(addr: &str, flags: u16) {
let c = classify(addr);
let expected = if flags == 0 { 0 } else { flags | NON_PUBLIC };
assert_eq!(
c.flags, expected,
"{addr}: flags {:#06x} != expected {expected:#06x}",
c.flags
);
}
}