use std::net::{Ipv4Addr, Ipv6Addr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostClass {
Loopback,
LinkLocal,
Private,
Cgnat,
Unspecified,
Public,
}
impl HostClass {
pub fn is_internal(self) -> bool {
!matches!(self, HostClass::Public)
}
pub fn is_loopback(self) -> bool {
matches!(self, HostClass::Loopback)
}
}
pub fn classify_host(host: &str) -> HostClass {
let h = host
.trim_start_matches('[')
.trim_end_matches(']')
.trim_end_matches('.')
.to_ascii_lowercase();
if h == "localhost" || h.ends_with(".localhost") {
return HostClass::Loopback;
}
if let Ok(ip) = h.parse::<Ipv4Addr>() {
return classify_ipv4(ip);
}
if let Ok(ip) = h.parse::<Ipv6Addr>() {
if let Some(v4) = ip.to_ipv4_mapped() {
return classify_ipv4(v4);
}
if ip.is_loopback() {
return HostClass::Loopback;
}
if ip.is_unspecified() {
return HostClass::Unspecified;
}
if (ip.segments()[0] & 0xfe00) == 0xfc00 {
return HostClass::Private; }
if (ip.segments()[0] & 0xffc0) == 0xfe80 {
return HostClass::LinkLocal; }
return HostClass::Public;
}
HostClass::Public
}
fn classify_ipv4(ip: Ipv4Addr) -> HostClass {
if ip.is_loopback() {
return HostClass::Loopback;
}
if ip.is_unspecified() || ip.is_broadcast() {
return HostClass::Unspecified;
}
if ip.is_link_local() {
return HostClass::LinkLocal;
}
if ip.is_private() {
return HostClass::Private;
}
let o = ip.octets();
if o[0] == 100 && (64..=127).contains(&o[1]) {
return HostClass::Cgnat; }
HostClass::Public
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loopback_forms() {
for h in [
"localhost",
"localhost.",
"127.0.0.1",
"127.1.2.3",
"[::1]",
"[::ffff:127.0.0.1]",
"app.localhost",
] {
assert_eq!(classify_host(h), HostClass::Loopback, "{h}");
assert!(classify_host(h).is_internal());
assert!(classify_host(h).is_loopback());
}
}
#[test]
fn internal_but_not_loopback() {
for h in [
"10.0.0.5",
"192.168.1.1",
"172.16.0.1",
"169.254.169.254",
"[::ffff:169.254.169.254]", "[fc00::1]", "[fe80::1]", "100.100.100.200", "0.0.0.0",
] {
assert!(classify_host(h).is_internal(), "{h} should be internal");
assert!(!classify_host(h).is_loopback(), "{h} must not be loopback");
}
}
#[test]
fn public_hosts() {
for h in [
"example.com",
"8.8.8.8",
"1.1.1.1",
"[2606:4700:4700::1111]",
"api.openai.com",
] {
assert_eq!(classify_host(h), HostClass::Public, "{h}");
assert!(!classify_host(h).is_internal(), "{h}");
}
}
}