diff --git a/CHANGELOG.md b/CHANGELOG.md index 41fc844..566f80d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ `CONTRIBUTING.md` - Bugfix: detect embedded IPv4 across the entire 6to4 block (`2002::/16`, RFC 3056 § 2), not just canonical 6to4 addresses with a zeroed 80-bit tail. +- Bugfix: align `isPublicUse()`, `isUnicastGlobal()` and `isDocumentation()` + with the IANA special-purpose address registries, and update RFC + references/citations for all address classification methods. ## `6.0.0` diff --git a/src/IpInterface.php b/src/IpInterface.php index c93c318..01eb681 100644 --- a/src/IpInterface.php +++ b/src/IpInterface.php @@ -73,13 +73,23 @@ public function inRange(self $ip, int $cidr): bool; */ public function getCommonCidr(self $ip): int; - /** Whether the IP is an IPv4-mapped IPv6 address (eg, "::ffff:7f00:1"). */ + /** + * Whether the IP is an IPv4-mapped IPv6 address, according to + * RFC 4291 § 2.5.5.2 (eg, "::ffff:7f00:1"). + */ public function isMapped(): bool; - /** Whether the IP is a 6to4-derived address (eg, "2002:7f00:1::"). */ + /** + * Whether the IP is a 6to4-derived address, according to RFC 3056 § 2. Any + * address within the 6to4 block `2002::/16` (eg, "2002:7f00:1::"), all of + * which embed an IPv4 address in bits 16-47. + */ public function isDerived(): bool; - /** Whether the IP is an IPv4-compatible IPv6 address (eg, `::7f00:1`). */ + /** + * Whether the IP is an IPv4-compatible IPv6 address, according to + * RFC 4291 § 2.5.5.1 (eg, `::7f00:1`); deprecated by that same RFC. + */ public function isCompatible(): bool; /** @@ -89,20 +99,20 @@ public function isCompatible(): bool; public function isEmbedded(): bool; /** - * Whether the IP is reserved for link-local usage, according to - * RFC 3927/RFC 4291 (IPv4/IPv6). + * Whether the IP is reserved for link-local usage, according to RFC 3927 + * (IPv4) or RFC 4291 § 2.5.6 (IPv6). */ public function isLinkLocal(): bool; /** - * Whether the IP is a loopback address, according to RFC 2373/RFC 3330 - * (IPv4/IPv6). + * Whether the IP is a loopback address, according to RFC 1122 § 3.2.1.3 + * (IPv4) or RFC 4291 § 2.5.3 (IPv6). */ public function isLoopback(): bool; /** - * Whether the IP is a multicast address, according to RFC 3171/RFC 2373 - * (IPv4/IPv6). + * Whether the IP is a multicast address, according to RFC 5771 (IPv4) or + * RFC 4291 § 2.7 (IPv6). */ public function isMulticast(): bool; @@ -112,18 +122,22 @@ public function isMulticast(): bool; */ public function isPrivateUse(): bool; - /** Whether the IP is unspecified, according to RFC 5735/RFC 2373 (IPv4/IPv6). */ + /** + * Whether the IP is unspecified ("this host on this network"), according + * to RFC 1122 § 3.2.1.3 (IPv4) or RFC 4291 § 2.5.2 (IPv6). + */ public function isUnspecified(): bool; /** * Whether the IP is reserved for network devices benchmarking, according - * to RFC 2544/RFC 5180 (IPv4/IPv6). + * to RFC 2544 (IPv4) or RFC 5180 (IPv6). The IPv6 block printed in RFC 5180 + * itself is wrong; RFC Errata 1752 corrects it to `2001:2::/48`. */ public function isBenchmarking(): bool; /** - * Whether the IP is in range designated for documentation, according to - * RFC 5737/RFC 3849 (IPv4/IPv6). + * Whether the IP is in a range designated for documentation, according to + * RFC 5737 and RFC 5771 § 9.2 (IPv4), or RFC 3849 and RFC 9637 (IPv6). */ public function isDocumentation(): bool; @@ -132,7 +146,7 @@ public function isDocumentation(): bool; * the IANA Special-Purpose Address Registry documents. * * @see https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml - * @see https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv6-special-registry.xhtml + * @see https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml */ public function isPublicUse(): bool; diff --git a/src/Version/IPv4.php b/src/Version/IPv4.php index a5e0d67..f4dc3bc 100644 --- a/src/Version/IPv4.php +++ b/src/Version/IPv4.php @@ -96,28 +96,53 @@ public function isBenchmarking(): bool public function isDocumentation(): bool { + // The three TEST-NET blocks (RFC 5737), plus MCAST-TEST-NET + // 233.252.0.0/24 (RFC 5771 § 9.2), which is assigned for use in + // documentation and example code and MUST NOT appear on the public + // Internet. return $this->inRange(new self(Binary::fromHex('c0000200')), 24) || $this->inRange(new self(Binary::fromHex('c6336400')), 24) - || $this->inRange(new self(Binary::fromHex('cb007100')), 24); + || $this->inRange(new self(Binary::fromHex('cb007100')), 24) + || $this->inRange(new self(Binary::fromHex('e9fc0000')), 24); } public function isPublicUse(): bool { - // Both 192.0.0.9 and 192.0.0.10 are globally routable, despite being in the future reserved block. + // The PCP anycast address `192.0.0.9` (RFC 7723) and the TURN anycast + // address `192.0.0.10` (RFC 8155) are globally routable, despite being + // within the IETF Protocol Assignments block. if (in_array(Binary::toHex($this->getBinary()), ['c0000009', 'c000000a'], true)) { return true; } - - // The whole 0.0.0.0/8 block is not for public use. + // The whole "this network" block `0.0.0.0/8` (RFC 791 § 3.2) is not + // globally reachable. if ($this->inRange(new self(Binary::fromHex('00000000')), 8)) { return false; } - - // Addresses reserved for future protocols are not globally routable (different to reserved for future use). + // Addresses reserved for future protocols are not globally routable. + // The IETF Protocol Assignments block `192.0.0.0/24` (RFC 6890 § 2.1) + // is different to "reserved for future use". if ($this->inRange(new self(Binary::fromHex('c0000000')), 24)) { return false; } + // The `6a44`-relay anycast address `192.88.99.2/32` (RFC 6751) is + // listed as not globally reachable. Note that the surrounding 6to4 + // Relay Anycast block `192.88.99.0/24` was deprecated by RFC 7526 and + // its registry entry terminated (2015-03) with every attribute column + // left blank and is NOT listed as "globally reachable: false". + // The rest of that block falls through as globally reachable (when in + // doubt, do what the Rust standard library does). + if ($this->getBinary() === Binary::fromHex('c0586302')) { + return false; + } + // Note: IPv4 multicast (`224.0.0.0/4`) is deliberately NOT excluded + // here. It is absent from the IANA IPv4 special-purpose address + // registry (which defines "globally reachable"). Unlike IPv6, IPv4 + // multicast carries no in-address scope field so it cannot be + // scope-classified the way IPv6 multicast is via `getMulticastScope()`. + // > `239.0.0.0/8` is administratively scoped (RFC 2365), configured at + // > boundary routers rather than encoded in the address. return !$this->isPrivateUse() && !$this->isLoopback() && !$this->isLinkLocal() @@ -140,6 +165,9 @@ public function isShared(): bool public function isFutureReserved(): bool { + // `255.255.255.255` is carved out of `240.0.0.0/4` (RFC 1112 § 4): the + // IANA special-purpose registry lists the limited broadcast address as + // its own entry (RFC 8190, RFC 919 § 7). return $this->getBinary() !== Binary::fromHex('ffffffff') && $this->inRange(new self(Binary::fromHex('f0000000')), 4); } diff --git a/src/Version/IPv6.php b/src/Version/IPv6.php index 0be944d..2575d2e 100644 --- a/src/Version/IPv6.php +++ b/src/Version/IPv6.php @@ -122,7 +122,11 @@ public function isBenchmarking(): bool public function isDocumentation(): bool { - return $this->inRange(new self(Binary::fromHex('20010db8000000000000000000000000')), 32); + // Two blocks are reserved for documentation: `2001:db8::/32` (RFC 3849) + // and `3fff::/20` (RFC 9637, which updates RFC 3849). Both are listed + // as not globally reachable in the IANA special-purpose registry. + return $this->inRange(new self(Binary::fromHex('20010db8000000000000000000000000')), 32) + || $this->inRange(new self(Binary::fromHex('3fff0000000000000000000000000000')), 20); } public function isPublicUse(): bool @@ -147,7 +151,90 @@ public function isUnicastGlobal(): bool && !$this->isLinkLocal() && !$this->isUniqueLocal() && !$this->isUnspecified() - && !$this->isDocumentation(); + // IPv4-mapped addresses (`::ffff:0:0/96`, RFC 4291 § 2.5.5.2) are + // listed as not globally reachable in the IANA special-purpose + // registry. + && !$this->isMapped() + && !$this->isDocumentation() + && !$this->isBenchmarking() + && !$this->isIetfProtocolAssignment() + // The IANA special-purpose registry lists the 6to4 (derived) block + // `2002::/16` (RFC 3056) with a globally-reachable value of "N/A" + // rather than "true"; when in doubt, do what the Rust standard + // library does. + && !$this->isDerived() + && !$this->isNat64LocalUse() + && !$this->isDiscardOnly() + && !$this->isDummyPrefix() + && !$this->isSegmentRoutingSid(); + } + + /** + * The IANA special-purpose registry lists the IETF Protocol Assignments + * block `2001::/23` (RFC 2928) as not globally reachable "unless allowed + * by a more specific allocation"; the allocations within it marked as + * globally reachable are the PCP anycast address `2001:1::1/128` (RFC + * 7723), the TURN anycast address `2001:1::2/128` (RFC 8155), the DNS-SD + * SRP anycast address `2001:1::3/128` (RFC 9665), AMT `2001:3::/32` (RFC + * 7450), AS112-v6 `2001:4:112::/48` (RFC 7535), ORCHIDv2 `2001:20::/28` + * (RFC 7343), and Drone Remote ID Protocol Entity Tags `2001:30::/28` (RFC + * 9374). + * Everything else in the block is treated as not globally reachable; + * including TEREDO `2001::/32` (whose globally-reachable value is "N/A") + * and the terminated ORCHID entry `2001:10::/28`. + */ + private function isIetfProtocolAssignment(): bool + { + return $this->inRange(new self(Binary::fromHex('20010000000000000000000000000000')), 23) + && !in_array(Binary::toHex($this->getBinary()), [ + '20010001000000000000000000000001', + '20010001000000000000000000000002', + '20010001000000000000000000000003', + ], true) + && !$this->inRange(new self(Binary::fromHex('20010003000000000000000000000000')), 32) + && !$this->inRange(new self(Binary::fromHex('20010004011200000000000000000000')), 48) + && !$this->inRange(new self(Binary::fromHex('20010020000000000000000000000000')), 28) + && !$this->inRange(new self(Binary::fromHex('20010030000000000000000000000000')), 28); + } + + /** + * The IANA special-purpose registry lists `64:ff9b:1::/48` as not globally + * reachable. + */ + private function isNat64LocalUse(): bool + { + // RFC 8215 reserves the /48 block for local use, but operators + // subdivide it into Network-Specific Prefixes of any RFC 6052 § 2.2 + // length that fits within a /48 (ie, /48, /56, /64, or /96), and each + // length places the IPv4 bytes at a different offset when embedding. + return $this->inRange(new self(Binary::fromHex('0064ff9b000100000000000000000000')), 48); + } + + /** + * The IANA special-purpose registry lists the Discard-Only block `100::/64` + * (RFC 6666) as not globally reachable. + */ + private function isDiscardOnly(): bool + { + return $this->inRange(new self(Binary::fromHex('01000000000000000000000000000000')), 64); + } + + /** + * The IANA special-purpose registry lists the Dummy Prefix `100:0:0:1::/64` + * (RFC 9780) as not globally reachable. + */ + private function isDummyPrefix(): bool + { + return $this->inRange(new self(Binary::fromHex('01000000000000010000000000000000')), 64); + } + + /** + * The IANA special-purpose registry lists the Segment Routing (SRv6) SID + * block `5f00::/16` (RFC 9602) as not globally reachable. + */ + private function isSegmentRoutingSid(): bool + { + return $this->inRange(new self(Binary::fromHex('5f000000000000000000000000000000')), 16); } public function __toString(): string diff --git a/src/Version/Version4Interface.php b/src/Version/Version4Interface.php index ac71a9a..c851f57 100644 --- a/src/Version/Version4Interface.php +++ b/src/Version/Version4Interface.php @@ -20,7 +20,7 @@ interface Version4Interface extends IpInterface public function getDotAddress(): string; /** - * Whether the IP is a broadcast address, according to RFC 919. + * Whether the IP is a broadcast address, according to RFC 919 § 7. * * @throws \Darsyn\IP\Exception\WrongVersionException */ @@ -34,7 +34,9 @@ public function isBroadcast(): bool; public function isShared(): bool; /** - * Whether the IP is reserved for future use, according to RFC 1112. + * Whether the IP is reserved for future use, according to RFC 1112 § 4 + * (excluding the limited broadcast address, which is a separate + * special-purpose registry entry per RFC 8190). * * @throws \Darsyn\IP\Exception\WrongVersionException */ diff --git a/src/Version/Version6Interface.php b/src/Version/Version6Interface.php index bc33026..144579d 100644 --- a/src/Version/Version6Interface.php +++ b/src/Version/Version6Interface.php @@ -8,6 +8,9 @@ interface Version6Interface extends IpInterface { + // Multicast scope field values, as defined by RFC 7346 § 2 (which updated + // the original RFC 4291 § 2.7 table; realm-local scope 3 is only defined + // in RFC 7346). public const MULTICAST_INTERFACE_LOCAL = 1; public const MULTICAST_LINK_LOCAL = 2; public const MULTICAST_REALM_LOCAL = 3; @@ -39,7 +42,7 @@ public function getExpandedAddress(): string; /** * Returns the IP address’s multicast scope if the address is multicast, * null otherwise. Return values are integers mapped to the MULTICAST_* - * constants on this interface. + * constants on this interface, with scope values defined by RFC 7346 § 2. */ public function getMulticastScope(): ?int; @@ -51,7 +54,7 @@ public function isUnicast(): bool; /** * Whether the IP is a globally routable unicast address, according to - * RFC 2941. + * RFC 4291 § 2.5.4. */ public function isUnicastGlobal(): bool; } diff --git a/tests/DataProvider/IPv4.php b/tests/DataProvider/IPv4.php index 41e7c1b..f4c156f 100644 --- a/tests/DataProvider/IPv4.php +++ b/tests/DataProvider/IPv4.php @@ -231,6 +231,9 @@ public static function getCategorizedIpAddresses() '198.51.100.0' => self::DOCUMENTATION, '203.0.113.0' => self::DOCUMENTATION, '203.2.113.0' => self::PUBLIC_USE, + '192.88.99.1' => self::PUBLIC_USE, + '192.88.99.2' => 0, + '233.252.0.1' => self::MULTICAST_IPV4 | self::DOCUMENTATION, '255.255.255.255' => self::BROADCAST, '198.18.0.0' => self::BENCHMARKING, '198.18.54.2' => self::BENCHMARKING, diff --git a/tests/DataProvider/IPv6.php b/tests/DataProvider/IPv6.php index 00f7c4f..e64ec4e 100644 --- a/tests/DataProvider/IPv6.php +++ b/tests/DataProvider/IPv6.php @@ -288,19 +288,26 @@ public static function getCategorizedIpAddresses() 'ff08::' => self::MULTICAST_ORGANIZATION_LOCAL, 'ff0e::' => self::PUBLIC_USE_V6 | self::MULTICAST_GLOBAL, '2001:db8:85a3::8a2e:370:7334' => self::DOCUMENTATION | self::UNICAST_OTHER, - '2001:2::ac32:23ff:21' => self::PUBLIC_USE_V6 | self::BENCHMARKING | self::UNICAST_GLOBAL, + '2001:2::ac32:23ff:21' => self::BENCHMARKING | self::UNICAST_OTHER, + '2001:1::1' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL, + '2001:10::' => self::UNICAST_OTHER, + '3fff::1' => self::DOCUMENTATION | self::UNICAST_OTHER, + '64:ff9b:1::1' => self::UNICAST_OTHER, + '100::1' => self::UNICAST_OTHER, + '100:0:0:1::1' => self::UNICAST_OTHER, + '5f00::1' => self::UNICAST_OTHER, '102:304:506:708:90a:b0c:d0e:f10' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL, 'fd00::' => self::PRIVATE_USE | self::UNIQUE_LOCAL | self::UNICAST_OTHER, 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => self::PRIVATE_USE | self::UNIQUE_LOCAL | self::UNICAST_OTHER, 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' => self::MULTICAST_OTHER, - '::ffff:1:0' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::MAPPED, - '::ffff:7f00:1' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::MAPPED | self::LOOPBACK_MAPPED, - '::ffff:1234:5678' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::MAPPED, - '0000:0000:0000:0000:0000:ffff:7f00:a001' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::MAPPED | self::LOOPBACK_MAPPED, - '2002::' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::DERIVED, - '2002:7f00:1::' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::DERIVED | self::LOOPBACK_DERIVED, - '2002:7f00:1:1::1' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::DERIVED | self::LOOPBACK_DERIVED, - '2002:1234:4321:0:00:000:0000::' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::DERIVED, + '::ffff:1:0' => self::UNICAST_OTHER | self::MAPPED, + '::ffff:7f00:1' => self::UNICAST_OTHER | self::MAPPED | self::LOOPBACK_MAPPED, + '::ffff:1234:5678' => self::UNICAST_OTHER | self::MAPPED, + '0000:0000:0000:0000:0000:ffff:7f00:a001' => self::UNICAST_OTHER | self::MAPPED | self::LOOPBACK_MAPPED, + '2002::' => self::UNICAST_OTHER | self::DERIVED, + '2002:7f00:1::' => self::UNICAST_OTHER | self::DERIVED | self::LOOPBACK_DERIVED, + '2002:7f00:1:1::1' => self::UNICAST_OTHER | self::DERIVED | self::LOOPBACK_DERIVED, + '2002:1234:4321:0:00:000:0000::' => self::UNICAST_OTHER | self::DERIVED, '::7f00:1' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::COMPATIBLE | self::LOOPBACK_COMPATIBLE, '::12.34.56.78' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::COMPATIBLE, '0::000:0000:b12:cab' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::COMPATIBLE,