// Classification follows the two IANA special-purpose address registries, as
// they stood on 2026-10-31:
// https://www.iana.org/assignments/iana-ipv4-special-registry/
// https://www.iana.org/assignments/iana-ipv6-special-registry/
// An IPv4-mapped IPv6 address is judged by the IPv4 address inside it, the way a
// stack would route it.
///|
/// Whether the top `bits` of a 32-bit value match the block.
fn v4_within(value : UInt64, block : UInt64, bits : Int32) -> Bool {
if bits <= 0 {
true
} else if bits >= 32 {
value == block
} else {
let shift = 32 - bits
value >> shift == block >> shift
}
}
///|
/// Whether the top `bits` of a 128-bit value match the block.
fn v6_within(
hi : UInt64,
lo : UInt64,
bhi : UInt64,
blo : UInt64,
bits : Int32,
) -> Bool {
if bits <= 0 {
true
} else if bits <= 64 {
if bits == 64 {
hi == bhi
} else {
let shift = 64 - bits
hi >> shift == bhi >> shift
}
} else if hi != bhi {
false
} else if bits >= 128 {
lo == blo
} else {
let shift = 128 - bits
lo >> shift == blo >> shift
}
}
///|
/// Whether the address is in a block the IPv4 registry lists as not globally
/// reachable. Shared address space (100.64.0.0/10) is tracked separately, because
/// it is neither private-use nor reachable.
fn v4_unreachable(value : UInt64) -> Bool {
let blocks = [
(0x0000_0000UL, 8), // 0.0.0.0/8 this network
(0x0A00_0000UL, 8), // 10.0.0.0/8 private-use
(0x7F00_0000UL, 8), // 127.0.0.0/8 loopback
(0xA9FE_0000UL, 16), // 169.254.0.0/16 link local
(0xAC10_0000UL, 12), // 172.16.0.0/12 private-use
(0xC000_0000UL, 24), // 192.0.0.0/24 IETF protocol assignments
(0xC000_00AAUL, 31), // 192.0.0.170/31 NAT64 discovery
(0xC000_0200UL, 24), // 192.0.2.0/24 documentation
(0xC0A8_0000UL, 16), // 192.168.0.0/16 private-use
(0xC612_0000UL, 15), // 198.18.0.0/15 benchmarking
(0xC633_6400UL, 24), // 198.51.100.0/24 documentation
(0xCB00_7100UL, 24), // 203.0.113.0/24 documentation
(0xF000_0000UL, 4), // 240.0.0.0/4 reserved
(0xFFFF_FFFFUL, 32), // 255.255.255.255 limited broadcast
]
let mut inside = false
for block in blocks {
if v4_within(value, block.0, block.1) {
inside = true
}
}
if !inside {
return false
}
// Two anycast addresses inside 192.0.0.0/24 are reachable.
!(v4_within(value, 0xC000_0009UL, 32) || v4_within(value, 0xC000_000AUL, 32))
}
///|
/// Whether the address is in a block the IPv6 registry lists as not globally
/// reachable.
fn v6_unreachable(hi : UInt64, lo : UInt64) -> Bool {
let blocks : Array[(UInt64, UInt64, Int32)] = [
(0x0000_0000_0000_0000UL, 0x0000_0000_0000_0001UL, 128), // ::1/128
(0x0000_0000_0000_0000UL, 0x0000_0000_0000_0000UL, 128), // ::/128
(0x0000_0000_0000_0000UL, 0x0000_FFFF_0000_0000UL, 96), // ::ffff:0:0/96
(0x0064_FF9B_0001_0000UL, 0x0000_0000_0000_0000UL, 48), // 64:ff9b:1::/48
(0x0100_0000_0000_0000UL, 0x0000_0000_0000_0000UL, 64), // 100::/64
(0x2001_0000_0000_0000UL, 0x0000_0000_0000_0000UL, 23), // 2001::/23
(0x2001_0DB8_0000_0000UL, 0x0000_0000_0000_0000UL, 32), // 2001:db8::/32
(0x2002_0000_0000_0000UL, 0x0000_0000_0000_0000UL, 16), // 2002::/16
(0x3FFF_0000_0000_0000UL, 0x0000_0000_0000_0000UL, 20), // 3fff::/20
(0xFC00_0000_0000_0000UL, 0x0000_0000_0000_0000UL, 7), // fc00::/7
(0xFE80_0000_0000_0000UL, 0x0000_0000_0000_0000UL, 10), // fe80::/10
]
let mut inside = false
for block in blocks {
if v6_within(hi, lo, block.0, block.1, block.2) {
inside = true
}
}
if !inside {
return false
}
// Blocks inside 2001::/23 that are reachable after all.
let reachable : Array[(UInt64, UInt64, Int32)] = [
(0x2001_0001_0000_0000UL, 0x0000_0000_0000_0001UL, 128), // 2001:1::1/128 PCP anycast
(0x2001_0001_0000_0000UL, 0x0000_0000_0000_0002UL, 128), // 2001:1::2/128 TURN anycast
(0x2001_0003_0000_0000UL, 0x0000_0000_0000_0000UL, 32), // 2001:3::/32 AMT
(0x2001_0004_0112_0000UL, 0x0000_0000_0000_0000UL, 48), // 2001:4:112::/48 AS112
(0x2001_0020_0000_0000UL, 0x0000_0000_0000_0000UL, 28), // 2001:20::/28 ORCHIDv2
(0x2001_0030_0000_0000UL, 0x0000_0000_0000_0000UL, 28), // 2001:30::/28 DRIP
]
let mut exempt = false
for block in reachable {
if v6_within(hi, lo, block.0, block.1, block.2) {
exempt = true
}
}
!exempt
}
///|
/// Whether this address is the same address with any IPv4-mapped prefix removed,
/// which is how the two stacks treat it when the question is where it routes.
fn routed_as(addr : Addr) -> Addr {
addr.unmap()
}
///|
/// Whether this is the unspecified address, `0.0.0.0` or `::`. An IPv4-mapped
/// address is judged by the IPv4 address inside it, like every other question
/// here.
pub fn Addr::is_unspecified(self : Addr) -> Bool {
let addr = routed_as(self)
if addr.v4 {
u32_of(addr) == 0UL
} else {
addr.hi == 0UL && addr.lo == 0UL
}
}
///|
/// Whether this is a loopback address: `127.0.0.0/8` for IPv4, `::1` for IPv6.
pub fn Addr::is_loopback(self : Addr) -> Bool {
let addr = routed_as(self)
if addr.v4 {
v4_within(u32_of(addr), 0x7F00_0000UL, 8)
} else {
addr.hi == 0UL && addr.lo == 1UL
}
}
///|
/// Whether this is a multicast address: `224.0.0.0/4` for IPv4, `ff00::/8` for
/// IPv6.
pub fn Addr::is_multicast(self : Addr) -> Bool {
let addr = routed_as(self)
if addr.v4 {
v4_within(u32_of(addr), 0xE000_0000UL, 4)
} else {
addr.hi >> 56 == 0xFFUL
}
}
///|
/// Whether this is an interface-local multicast address, `ff01::/16` (RFC 4291
/// section 2.7). The scope says the packet never leaves the interface it was sent
/// on, which is why IPv4, whose multicast has no such scope, is always false.
///
/// ```mbt check
/// test {
/// assert_true(@moonip.Addr::parse("ff01::1").is_interface_local_multicast())
/// assert_false(@moonip.Addr::parse("ff02::1").is_interface_local_multicast())
/// }
/// ```
pub fn Addr::is_interface_local_multicast(self : Addr) -> Bool {
let addr = routed_as(self)
!addr.v4 && addr.hi >> 48 == 0xFF01UL
}
///|
/// Whether this is a link-local multicast address: `224.0.0.0/24` for IPv4,
/// `ff02::/16` for IPv6. `is_link_local` is the unicast counterpart.
///
/// ```mbt check
/// test {
/// assert_true(@moonip.Addr::parse("224.0.0.251").is_link_local_multicast())
/// assert_true(@moonip.Addr::parse("ff02::fb").is_link_local_multicast())
/// assert_false(@moonip.Addr::parse("239.1.1.1").is_link_local_multicast())
/// }
/// ```
pub fn Addr::is_link_local_multicast(self : Addr) -> Bool {
let addr = routed_as(self)
if addr.v4 {
v4_within(u32_of(addr), 0xE000_0000UL, 24)
} else {
addr.hi >> 48 == 0xFF02UL
}
}
///|
/// Whether this is a link-local address: `169.254.0.0/16` for IPv4 (RFC 3927),
/// `fe80::/10` for IPv6.
pub fn Addr::is_link_local(self : Addr) -> Bool {
let addr = routed_as(self)
if addr.v4 {
v4_within(u32_of(addr), 0xA9FE_0000UL, 16)
} else {
v6_within(addr.hi, addr.lo, 0xFE80_0000_0000_0000UL, 0UL, 10)
}
}
///|
/// Whether this is a deprecated site-local address, `fec0::/10` (RFC 3879). IPv4
/// has no such thing, so it is always false there.
pub fn Addr::is_site_local(self : Addr) -> Bool {
if self.v4 {
false
} else {
v6_within(self.hi, self.lo, 0xFEC0_0000_0000_0000UL, 0UL, 10)
}
}
///|
/// Whether this address is reserved for a purpose other than unicast: `240.0.0.0/4`
/// for IPv4, and for IPv6 every block outside `2000::/3` and `fe80::/10`
/// (RFC 4291 section 2.4).
pub fn Addr::is_reserved(self : Addr) -> Bool {
let addr = routed_as(self)
if addr.v4 {
v4_within(u32_of(addr), 0xF000_0000UL, 4)
} else {
// RFC 4291 section 2.4 reserves everything outside five blocks: global
// unicast, unique local, link local, the deprecated site local one, and the
// multicast space, which has a predicate of its own.
!v6_within(addr.hi, addr.lo, 0x2000_0000_0000_0000UL, 0UL, 3) &&
!v6_within(addr.hi, addr.lo, 0xFC00_0000_0000_0000UL, 0UL, 7) &&
!v6_within(addr.hi, addr.lo, 0xFE80_0000_0000_0000UL, 0UL, 10) &&
!v6_within(addr.hi, addr.lo, 0xFEC0_0000_0000_0000UL, 0UL, 10) &&
!v6_within(addr.hi, addr.lo, 0xFF00_0000_0000_0000UL, 0UL, 8)
}
}
///|
/// Whether the address sits in a block the IANA registries mark as not globally
/// reachable: the private-use ranges, loopback, link local, documentation,
/// benchmarking and the rest, with the IPv6 exceptions the registry itself names.
///
/// ```mbt check
/// test {
/// assert_true(@moonip.Addr::parse("10.1.2.3").is_private())
/// assert_false(@moonip.Addr::parse("8.8.8.8").is_private())
/// assert_true(@moonip.Addr::parse("fd00::1").is_private())
/// }
/// ```
pub fn Addr::is_private(self : Addr) -> Bool {
let addr = routed_as(self)
if addr.v4 {
v4_unreachable(u32_of(addr))
} else {
v6_unreachable(addr.hi, addr.lo)
}
}
///|
/// Whether the address is globally reachable: the opposite of `is_private`, except
/// for shared address space `100.64.0.0/10`, which is neither private nor
/// reachable and so answers false to both.
///
/// ```mbt check
/// test {
/// assert_true(@moonip.Addr::parse("8.8.8.8").is_global())
/// assert_true(@moonip.Addr::parse("2001:4860::1").is_global())
/// assert_false(@moonip.Addr::parse("100.64.0.1").is_global())
/// assert_false(@moonip.Addr::parse("100.64.0.1").is_private())
/// }
/// ```
pub fn Addr::is_global(self : Addr) -> Bool {
let addr = routed_as(self)
if addr.v4 {
!v4_within(u32_of(addr), 0x6440_0000UL, 10) && !v4_unreachable(u32_of(addr))
} else {
!v6_unreachable(addr.hi, addr.lo)
}
}