use core::fmt;
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use core::str::FromStr;
use rama_core::error::{BoxError, BoxErrorExt as _, ErrorExt as _};
use rama_utils::str::eq_ignore_ascii_kebab_case;
use crate::std::vec::Vec;
use super::ipnet::{IpNet, Ipv4Net, Ipv6Net};
use super::private::{
is_ipv4_benchmarking, is_ipv4_protocol_assignments, is_ipv4_reserved, is_ipv4_shared,
is_ipv4_this_network, is_ipv6_benchmarking, is_ipv6_discard_only, is_ipv6_documentation,
is_ipv6_dummy_prefix, is_ipv6_local_use_translation, is_ipv6_site_local,
};
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct IpScopes: u16 {
const LOOPBACK = 1 << 0;
const PRIVATE = 1 << 1;
const LINK_LOCAL = 1 << 2;
const SHARED = 1 << 3;
const UNSPECIFIED = 1 << 4;
const MULTICAST = 1 << 5;
const DOCUMENTATION = 1 << 6;
const BENCHMARKING = 1 << 7;
const RESERVED = 1 << 8;
const GLOBAL = 1 << 15;
}
}
impl IpScopes {
pub const NON_GLOBAL: Self = Self::all().difference(Self::GLOBAL);
pub const LOCAL: Self = Self::LOOPBACK
.union(Self::PRIVATE)
.union(Self::LINK_LOCAL)
.union(Self::SHARED);
}
const SCOPE_NAMES: &[(&str, IpScopes)] = &[
("loopback", IpScopes::LOOPBACK),
("private", IpScopes::PRIVATE),
("link-local", IpScopes::LINK_LOCAL),
("shared", IpScopes::SHARED),
("unspecified", IpScopes::UNSPECIFIED),
("multicast", IpScopes::MULTICAST),
("documentation", IpScopes::DOCUMENTATION),
("benchmarking", IpScopes::BENCHMARKING),
("reserved", IpScopes::RESERVED),
("global", IpScopes::GLOBAL),
];
const SCOPE_ALIASES: &[(&str, IpScopes)] = &[
("all", IpScopes::all()),
("local", IpScopes::LOCAL),
("non-global", IpScopes::NON_GLOBAL),
];
impl fmt::Display for IpScopes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for (name, scope) in SCOPE_NAMES {
if self.contains(*scope) {
if !first {
f.write_str(",")?;
}
first = false;
f.write_str(name)?;
}
}
Ok(())
}
}
impl FromStr for IpScopes {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut scopes = Self::empty();
for token in s.split([',', '|']) {
let token = token.trim();
if token.is_empty() {
continue;
}
scopes |= SCOPE_NAMES
.iter()
.chain(SCOPE_ALIASES)
.find_map(|(name, scope)| {
eq_ignore_ascii_kebab_case(token.as_bytes(), name.as_bytes()).then_some(*scope)
})
.ok_or_else(|| {
BoxError::from_static_str("unknown ip scope").context_str_field("scope", token)
})?;
}
Ok(scopes)
}
}
impl serde::Serialize for IpScopes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> serde::Deserialize<'de> for IpScopes {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = <crate::std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[inline]
#[must_use]
pub fn ip_scope(addr: impl Into<IpAddr>) -> IpScopes {
match addr.into() {
IpAddr::V4(addr) => ipv4_scope(addr),
IpAddr::V6(addr) => ipv6_scope(addr),
}
}
#[must_use]
pub fn ipv4_scope(addr: Ipv4Addr) -> IpScopes {
let val = u32::from(addr);
if addr.is_loopback() {
IpScopes::LOOPBACK
} else if addr.is_private() {
IpScopes::PRIVATE
} else if is_ipv4_shared(val) {
IpScopes::SHARED
} else if addr.is_link_local() {
IpScopes::LINK_LOCAL
} else if is_ipv4_this_network(val) {
IpScopes::UNSPECIFIED
} else if is_ipv4_benchmarking(val) {
IpScopes::BENCHMARKING
} else if addr.is_documentation() {
IpScopes::DOCUMENTATION
} else if addr.is_multicast() {
IpScopes::MULTICAST
} else if is_ipv4_protocol_assignments(val) || addr.is_broadcast() || is_ipv4_reserved(val) {
IpScopes::RESERVED
} else {
IpScopes::GLOBAL
}
}
#[must_use]
pub fn ipv6_scope(addr: Ipv6Addr) -> IpScopes {
let val = u128::from(addr);
if addr.is_loopback() {
IpScopes::LOOPBACK
} else if addr.is_unspecified() {
IpScopes::UNSPECIFIED
} else if addr.is_multicast() {
IpScopes::MULTICAST
} else if addr.is_unicast_link_local() {
IpScopes::LINK_LOCAL
} else if addr.is_unique_local() {
IpScopes::PRIVATE
} else if is_ipv6_benchmarking(val) {
IpScopes::BENCHMARKING
} else if is_ipv6_documentation(val) {
IpScopes::DOCUMENTATION
} else if is_ipv6_site_local(val)
|| is_ipv6_discard_only(val)
|| is_ipv6_dummy_prefix(val)
|| is_ipv6_local_use_translation(val)
{
IpScopes::RESERVED
} else {
IpScopes::GLOBAL
}
}
#[must_use]
pub fn scope_cidrs(scopes: IpScopes) -> Vec<IpNet> {
fn v4(a: [u8; 4], prefix: u8) -> IpNet {
IpNet::V4(Ipv4Net::new_assert(
Ipv4Addr::new(a[0], a[1], a[2], a[3]),
prefix,
))
}
fn v6(addr: Ipv6Addr, prefix: u8) -> IpNet {
IpNet::V6(Ipv6Net::new_assert(addr, prefix))
}
let mut out = Vec::new();
if scopes.contains(IpScopes::LOOPBACK) {
out.push(v4([127, 0, 0, 0], 8));
out.push(v6(Ipv6Addr::LOCALHOST, 128));
}
if scopes.contains(IpScopes::PRIVATE) {
out.push(v4([10, 0, 0, 0], 8));
out.push(v4([172, 16, 0, 0], 12));
out.push(v4([192, 168, 0, 0], 16));
out.push(v6(Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 0), 7)); }
if scopes.contains(IpScopes::LINK_LOCAL) {
out.push(v4([169, 254, 0, 0], 16));
out.push(v6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0), 10)); }
if scopes.contains(IpScopes::SHARED) {
out.push(v4([100, 64, 0, 0], 10));
}
if scopes.contains(IpScopes::UNSPECIFIED) {
out.push(v4([0, 0, 0, 0], 8));
out.push(v6(Ipv6Addr::UNSPECIFIED, 128));
}
if scopes.contains(IpScopes::MULTICAST) {
out.push(v4([224, 0, 0, 0], 4));
out.push(v6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0), 8)); }
if scopes.contains(IpScopes::DOCUMENTATION) {
out.push(v4([192, 0, 2, 0], 24));
out.push(v4([198, 51, 100, 0], 24));
out.push(v4([203, 0, 113, 0], 24));
out.push(v6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 32)); }
if scopes.contains(IpScopes::BENCHMARKING) {
out.push(v4([198, 18, 0, 0], 15));
out.push(v6(Ipv6Addr::new(0x2001, 0x2, 0, 0, 0, 0, 0, 0), 48)); }
if scopes.contains(IpScopes::RESERVED) {
out.push(v4([240, 0, 0, 0], 4));
}
out
}
#[cfg(test)]
mod tests {
use super::super::private::is_private_ip;
use super::*;
#[test]
fn scope_non_global_matches_is_private_ip() {
let v4: &[[u8; 4]] = &[
[127, 0, 0, 1],
[10, 0, 0, 1],
[172, 16, 0, 1],
[192, 168, 1, 1],
[0, 1, 2, 3],
[100, 64, 0, 1],
[100, 63, 255, 255],
[169, 254, 1, 1],
[192, 0, 0, 8],
[192, 0, 0, 9],
[192, 0, 2, 1],
[198, 18, 0, 1],
[198, 51, 100, 1],
[224, 0, 0, 1],
[240, 0, 0, 1],
[255, 255, 255, 255],
[8, 8, 8, 8],
[1, 1, 1, 1],
[192, 88, 99, 1],
];
for a in v4 {
let ip = IpAddr::from(*a);
assert_eq!(
ip_scope(ip).contains(IpScopes::GLOBAL),
!is_private_ip(ip),
"v4 {ip}: scope={:?}",
ip_scope(ip)
);
assert_eq!(
ip_scope(ip).bits().count_ones(),
1,
"v4 {ip} not single-bit"
);
}
let v6: &[&str] = &[
"::1",
"::",
"fc00::1",
"fe80::1",
"ff02::1",
"fec0::1",
"2001:db8::1",
"2001:2::1",
"100::1",
"64:ff9b:1::1",
"64:ff9b::1",
"2001:4860:4860::8888",
"2606:4700:4700::1111",
];
for s in v6 {
let ip: IpAddr = s.parse().unwrap();
assert_eq!(
ip_scope(ip).contains(IpScopes::GLOBAL),
!is_private_ip(ip),
"v6 {ip}: scope={:?}",
ip_scope(ip)
);
assert_eq!(
ip_scope(ip).bits().count_ones(),
1,
"v6 {ip} not single-bit"
);
}
}
#[test]
fn scope_assigns_expected_categories() {
assert_eq!(ip_scope([127, 0, 0, 1]), IpScopes::LOOPBACK);
assert_eq!(ip_scope([10, 1, 2, 3]), IpScopes::PRIVATE);
assert_eq!(ip_scope([100, 64, 0, 1]), IpScopes::SHARED);
assert_eq!(ip_scope([169, 254, 0, 1]), IpScopes::LINK_LOCAL);
assert_eq!(ip_scope([224, 0, 0, 1]), IpScopes::MULTICAST);
assert_eq!(ip_scope([8, 8, 8, 8]), IpScopes::GLOBAL);
assert_eq!(
ip_scope("::1".parse::<IpAddr>().unwrap()),
IpScopes::LOOPBACK
);
assert_eq!(
ip_scope("fc00::1".parse::<IpAddr>().unwrap()),
IpScopes::PRIVATE
);
assert_eq!(
ip_scope("fe80::1".parse::<IpAddr>().unwrap()),
IpScopes::LINK_LOCAL
);
}
#[test]
fn masks_compose_as_expected() {
assert!(ip_scope([10, 0, 0, 1]).contains(IpScopes::PRIVATE));
assert!(!ip_scope([10, 0, 0, 1]).contains(IpScopes::LOOPBACK));
assert!(IpScopes::LOCAL.contains(ip_scope([127, 0, 0, 1])));
assert!(IpScopes::LOCAL.contains(ip_scope([100, 64, 0, 1])));
assert!(!IpScopes::LOCAL.intersects(ip_scope([224, 0, 0, 1])));
assert!(!IpScopes::LOCAL.intersects(ip_scope([8, 8, 8, 8])));
assert!(IpScopes::NON_GLOBAL.intersects(ip_scope([10, 0, 0, 1])));
assert!(!IpScopes::NON_GLOBAL.intersects(ip_scope([8, 8, 8, 8])));
}
#[test]
fn scope_cidrs_cover_their_members() {
let nets = scope_cidrs(IpScopes::LOCAL);
for net in &nets {
assert!(
IpScopes::LOCAL.contains(ip_scope(net.network())),
"cidr {net} not LOCAL"
);
}
for ip in [
IpAddr::from([10, 1, 2, 3]),
IpAddr::from([127, 0, 0, 5]),
IpAddr::from([169, 254, 9, 9]),
IpAddr::from([100, 100, 0, 1]),
] {
assert!(
nets.iter().any(|n| n.contains(&ip)),
"no cidr contains {ip}"
);
}
assert!(scope_cidrs(IpScopes::GLOBAL).is_empty());
}
#[test]
fn ip_scopes_display_from_str_roundtrip() {
for (name, scope) in SCOPE_NAMES {
assert_eq!(scope.to_string(), *name);
assert_eq!(name.parse::<IpScopes>().unwrap(), *scope);
}
let set = IpScopes::LOOPBACK | IpScopes::LINK_LOCAL | IpScopes::GLOBAL;
assert_eq!(set.to_string(), "loopback,link-local,global");
assert_eq!(set.to_string().parse::<IpScopes>().unwrap(), set);
assert_eq!(IpScopes::empty().to_string(), "");
}
#[test]
fn ip_scopes_parse_flexible_grammar() {
assert_eq!(
"Private, LOOPBACK".parse::<IpScopes>().unwrap(),
IpScopes::PRIVATE | IpScopes::LOOPBACK
);
assert_eq!(
"link_local|shared".parse::<IpScopes>().unwrap(),
IpScopes::LINK_LOCAL | IpScopes::SHARED
);
assert_eq!(
"NON_GLOBAL".parse::<IpScopes>().unwrap(),
IpScopes::NON_GLOBAL
);
assert_eq!("all".parse::<IpScopes>().unwrap(), IpScopes::all());
assert_eq!("local".parse::<IpScopes>().unwrap(), IpScopes::LOCAL);
assert_eq!("".parse::<IpScopes>().unwrap(), IpScopes::empty());
assert_eq!(" , ".parse::<IpScopes>().unwrap(), IpScopes::empty());
let err = "bogus".parse::<IpScopes>().unwrap_err();
assert!(err.to_string().contains("bogus"), "err: {err}");
}
#[test]
fn ip_scopes_serde_string_roundtrip() {
let set = IpScopes::PRIVATE | IpScopes::GLOBAL;
let json = serde_json::to_string(&set).unwrap();
assert_eq!(json, "\"private,global\"");
assert_eq!(serde_json::from_str::<IpScopes>(&json).unwrap(), set);
assert_eq!(
serde_json::from_str::<IpScopes>("\"ALL\"").unwrap(),
IpScopes::all()
);
let _ = serde_json::from_str::<IpScopes>("\"bogus\"").unwrap_err();
}
}