Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
42 changes: 28 additions & 14 deletions src/IpInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand Down
40 changes: 34 additions & 6 deletions src/Version/IPv4.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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);
}
Expand Down
91 changes: 89 additions & 2 deletions src/Version/IPv6.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Comment thread
greptile-apps[bot] marked this conversation as resolved.
&& !$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
Expand Down
6 changes: 4 additions & 2 deletions src/Version/Version4Interface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand Down
7 changes: 5 additions & 2 deletions src/Version/Version6Interface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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;
}
3 changes: 3 additions & 0 deletions tests/DataProvider/IPv4.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 16 additions & 9 deletions tests/DataProvider/IPv6.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down