diff --git a/CHANGELOG.md b/CHANGELOG.md index 2978643..26f3bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,20 @@ - Bugfix: classify full `fc00::/7` block as private use. - Add (or update) community standards: `SECURITY.md`, `CODE_OF_CONDUCT.md` and `CONTRIBUTING.md` +- Add `Nat64` embedding strategy for the NAT64 well-known prefix `64:ff9b::/96` + (according to RFC 6052 § `2.1`). +- Add `Composite` embedding strategy that delegates to multiple underlying + embedding strategies; `Composite::all()` covers all unambiguous, + non-deprecated embeddings. +- Bugfix: classify IPv4-embedded IPv6 addresses (mapped, 6to4, NAT64) according + to the IPv4 address they embed in `isUnicastGlobal()` and `isPublicUse()`, + preventing non-global embedded addresses from being reported as global. + Inspired by [CVE-2026-48736](https://symfony.com/blog/cve-2026-48736-iputils-private-subnets-omits-ipv6-transition-forms-ssrf-bypass-in-noprivatenetworkhttpclient). +- Add `Teredo` embedding strategy for `2001::/32` (according to RFC 4380 § `4`, + extraction-only). Classification now also canonicalises Teredo embeddings via + `Composite::all()`. +- Classify the NAT64 local-use block `64:ff9b:1::/48` (according to RFC 8215) as + not globally reachable. ## `6.0.0` diff --git a/docs/05-strategies.md b/docs/05-strategies.md index b851dac..e4db204 100644 --- a/docs/05-strategies.md +++ b/docs/05-strategies.md @@ -1,18 +1,20 @@ # Embedding Strategies -When using version 4 and version 6 addresses interchangeably (via the +When using version 4 and version 6 addresses interchangeably (via the `Multi` class), version 4 addresses are *embedded* into version 6 addresses so that both versions are stored as 16-byte binary sequences. Unfortunately there are several different strategies for embedding a version 4 address into version 6, so this library offers various strategy implementations -for the main three: +for the main four (and one deprecated): -| Strategy Name | Implementation | Format | -|-----------------|---------------------------------|-------------------------------------------| -| 6to4-derived | `Darsyn\IP\Strategy\Derived` | `2002:XXXX:XXXX:0000:0000:0000:0000:0000` | -| IPv4-compatible | `Darsyn\IP\Strategy\Compatible` | `0000:0000:0000:0000:0000:0000:XXXX:XXXX` | -| IPv4-mapped | `Darsyn\IP\Strategy\Mapped` | `0000:0000:0000:0000:0000:ffff:XXXX:XXXX` | +| Strategy Name | Implementation | Format | Notes | +|-----------------|---------------------------------|-------------------------------------------|-----------------------| +| IPv4-mapped | `Darsyn\IP\Strategy\Mapped` | `0000:0000:0000:0000:0000:ffff:XXXX:XXXX` | Default | +| NAT64 | `Darsyn\IP\Strategy\Nat64` | `0064:ff9b:0000:0000:0000:0000:XXXX:XXXX` | Translator | +| 6to4-derived | `Darsyn\IP\Strategy\Derived` | `2002:XXXX:XXXX:0000:0000:0000:0000:0000` | Relay | +| Teredo | `Darsyn\IP\Strategy\Teredo` | `2001:0000:xxxx:xxxx:xxxx:xxxx:XXXX:XXXX` | Tunnel (extract-only) | +| IPv4-compatible | `Darsyn\IP\Strategy\Compatible` | `0000:0000:0000:0000:0000:0000:XXXX:XXXX` | Deprecated | Each embedding strategy implements the `Darsyn\IP\Strategy\EmbeddingStrategyInterface` which defines methods to: @@ -40,3 +42,61 @@ IP::setDefaultEmbeddingStrategy(new Strategy\Compatible); // But for this specific instance use the 6to4-derived embedding strategy. $ip = IP::factory('127.0.0.1', new Strategy\Derived); ``` + +## Composite + +The `Composite` strategy accepts one or more embedding strategies to verify +address embedding against. But only the first strategy supplied is used to embed +IPv4 into IPv6 (the "packer"). + +```php +getDotAddress(); // string("127.0.0.1") +$nat64 = IP::factory('64:ff9b::7f00:1', $strategy); +$nat64->getDotAddress(); // string("127.0.0.1") + +// But only the first argument (in this example, Mapped) is used to pack an +// IPv4 address into IPv6. +IP::factory('127.0.0.1', $strategy)->getCompactedAddress(); // string("::ffff:7f00:1") +// Existing IPv6 addresses retain their scheme and don't get "re-packed" into +// the first strategy. +IP::factory('64:ff9b::7f00:1', $strategy)->getCompactedAddress(); // string("64:ff9b::7f00:1") +``` + +### Named Constructor + +`Composite::all()` returns a composite of every **unambiguous, non-deprecated** +strategy: +- IPv4-mapped (`::ffff:0:0/96`) as the packer, +- 6to4 (`2002::/16`), +- the NAT64 Well-Known Prefix (`64:ff9b::/96`), and +- Teredo (`2001::/32`). + +```php +getProtocolAppropriateAddress(); // string("127.0.0.1") +IP::factory('2002:7f00:1::', $strategy)->getProtocolAppropriateAddress(); // string("127.0.0.1") +IP::factory('64:ff9b::7f00:1', $strategy)->getProtocolAppropriateAddress(); // string("127.0.0.1") +IP::factory('2001:0:4136:e378:8000:63bf:80ff:fffe', $strategy)->getProtocolAppropriateAddress(); // string("127.0.0.1") +``` + +> **Note:** `all()` deliberately **excludes** the ambiguous, deprecated +> IPv4-compatible (`Compatible`, `::/96`) strategy. diff --git a/src/Strategy/Compatible.php b/src/Strategy/Compatible.php index 9a6a602..8d3f73b 100644 --- a/src/Strategy/Compatible.php +++ b/src/Strategy/Compatible.php @@ -7,6 +7,12 @@ use Darsyn\IP\Exception\Strategy as StrategyException; use Darsyn\IP\Util\MbString; +/** + * Embeds an IPv4 address within the IPv4-compatible prefix `::/96`, as defined + * by RFC 4291 ("IP Version 6 Addressing Architecture"), § 2.5.5.1 + * + * Note: this format is deprecated and retained only for backwards compatibility. + */ class Compatible implements EmbeddingStrategyInterface { public function isEmbedded(string $binary): bool diff --git a/src/Strategy/Composite.php b/src/Strategy/Composite.php new file mode 100644 index 0000000..eeee9ec --- /dev/null +++ b/src/Strategy/Composite.php @@ -0,0 +1,61 @@ + */ + private $strategies; + + /** @var EmbeddingStrategyInterface */ + private $packer; + + public function __construct( + EmbeddingStrategyInterface $packer, + EmbeddingStrategyInterface ...$additional + ) { + $this->packer = $packer; + $this->strategies = array_values(array_merge([$packer], $additional)); + } + + /** + * Named helper constructor for matching all unambiguous, non-deprecated + * embedding strategies. + */ + public static function all(): self + { + // Explicitly NOT including the ambiguous, deprecated "Compatible" + // embedding strategy, and using Mapped as the canonical strategy for + // packing. + return new self(new Mapped(), new Derived(), new Nat64(), new Teredo()); + } + + public function isEmbedded(string $binary): bool + { + foreach ($this->strategies as $strategy) { + if ($strategy->isEmbedded($binary)) { + return true; + } + } + return false; + } + + public function extract(string $binary): string + { + foreach ($this->strategies as $strategy) { + if ($strategy->isEmbedded($binary)) { + return $strategy->extract($binary); + } + } + throw new StrategyException\ExtractionException($binary, $this); + } + + public function pack(string $binary): string + { + return $this->packer->pack($binary); + } +} diff --git a/src/Strategy/Derived.php b/src/Strategy/Derived.php index d99503a..7b6e145 100644 --- a/src/Strategy/Derived.php +++ b/src/Strategy/Derived.php @@ -8,6 +8,13 @@ use Darsyn\IP\Util\Binary; use Darsyn\IP\Util\MbString; +/** + * Embeds an IPv4 address within the 6to4 prefix `2002::/16`, as defined by RFC + * 3056 ("Connection of IPv6 Domains via IPv4 Clouds"). + * + * Legacy, but not formally deprecated (only 6to4 anycast was deprecated via + * RFC 7526). + */ class Derived implements EmbeddingStrategyInterface { public function isEmbedded(string $binary): bool diff --git a/src/Strategy/Mapped.php b/src/Strategy/Mapped.php index 4d42094..ff0e56e 100644 --- a/src/Strategy/Mapped.php +++ b/src/Strategy/Mapped.php @@ -8,6 +8,12 @@ use Darsyn\IP\Util\Binary; use Darsyn\IP\Util\MbString; +/** + * Embeds an IPv4 address within the IPv4-mapped prefix `::ffff:0:0/96`, as + * defined by RFC 4291 ("IP Version 6 Addressing Architecture"), § 2.5.5.2 + * + * Reserved by protocol, and not globally reachable (as an IPv6 address). + */ class Mapped implements EmbeddingStrategyInterface { public function isEmbedded(string $binary): bool diff --git a/src/Strategy/Nat64.php b/src/Strategy/Nat64.php new file mode 100644 index 0000000..2ad3e09 --- /dev/null +++ b/src/Strategy/Nat64.php @@ -0,0 +1,44 @@ +isEmbedded($this->getBinary())) { + return (new IPv4($strategy->extract($this->getBinary())))->isPublicUse(); + } return $this->isUnicast() && !$this->isLoopback() && !$this->isLinkLocal() && !$this->isUniqueLocal() && !$this->isUnspecified() - && !$this->isDocumentation(); + && !$this->isDocumentation() + && !$this->isNat64LocalUse(); + } + + /** 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 length (/48, /56, + // /64, /96…), and each length places the IPv4 bytes at a different offset + // when embedding. + return $this->inRange(new self(Binary::fromHex('0064ff9b000100000000000000000000')), 48); } public function __toString(): string diff --git a/tests/DataProvider/IPv6.php b/tests/DataProvider/IPv6.php index 61a4a67..2dd3896 100644 --- a/tests/DataProvider/IPv6.php +++ b/tests/DataProvider/IPv6.php @@ -293,13 +293,27 @@ public static function getCategorizedIpAddresses() '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, + // An IPv4-embedded address is only globally routable if the address + // it stands for is; the embedded loopback / "this-network" rows below + // are therefore unicast-but-not-global (see IPv6::isUnicastGlobal()). + '::ffff:1:0' => self::UNICAST_OTHER | self::MAPPED, + '::ffff:7f00:1' => self::UNICAST_OTHER | 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, + '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:1234:4321:0:00:000:0000::' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL | self::DERIVED, + // NAT64 (64:ff9b::/96): excluded from the Compatible-only ambiguity, + // recognised by Composite::all(), so it canonicalises like the others. + '64:ff9b::1234:5678' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL, + '64:ff9b::7f00:1' => self::UNICAST_OTHER | self::NAT64 | self::LOOPBACK_NAT64, + // Teredo (2001::/32). + '2001:0:4136:e378:8000:63bf:edcb:a987' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL, + '2001:0:4136:e378:8000:63bf:80ff:fffe' => self::UNICAST_OTHER | self::TEREDO | self::LOOPBACK_TEREDO, + // NAT64 local-use (64:ff9b:1::/48, RFC 8215): never globally reachable. + '64:ff9b:1::1' => self::UNICAST_OTHER, + '64:ff9b:1:ffff:ffff:ffff:ffff:ffff' => self::UNICAST_OTHER, + '64:ff9b:2::' => self::PUBLIC_USE_V6 | self::UNICAST_GLOBAL, '::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, diff --git a/tests/DataProvider/IpDataProviderInterface.php b/tests/DataProvider/IpDataProviderInterface.php index 1480f39..f88d9be 100644 --- a/tests/DataProvider/IpDataProviderInterface.php +++ b/tests/DataProvider/IpDataProviderInterface.php @@ -36,10 +36,14 @@ interface IpDataProviderInterface public const UNICAST_OTHER = 1 << 22; public const MAPPED = 1 << 23; public const DERIVED = 1 << 24; - public const COMPATIBLE = 1 << 25; - public const LOOPBACK_MAPPED = 1 << 26; - public const LOOPBACK_COMPATIBLE = 1 << 27; - public const LOOPBACK_DERIVED = 1 << 28; + public const NAT64 = 1 << 25; + public const COMPATIBLE = 1 << 26; + public const LOOPBACK_MAPPED = 1 << 27; + public const LOOPBACK_COMPATIBLE = 1 << 28; + public const LOOPBACK_DERIVED = 1 << 29; + public const LOOPBACK_NAT64 = 1 << 30; + public const TEREDO = 1 << 31; + public const LOOPBACK_TEREDO = 1 << 32; // Combinations public const PUBLIC_USE = 0 @@ -48,7 +52,9 @@ interface IpDataProviderInterface public const LOOPBACK_EMBEDDED = 0 | self::LOOPBACK_MAPPED | self::LOOPBACK_COMPATIBLE - | self::LOOPBACK_DERIVED; + | self::LOOPBACK_DERIVED + | self::LOOPBACK_NAT64 + | self::LOOPBACK_TEREDO; public const MULTICAST = 0 | self::MULTICAST_IPV4 | self::MULTICAST_INTERFACE_LOCAL diff --git a/tests/DataProvider/Multi.php b/tests/DataProvider/Multi.php index a0ef8fe..6af6ad2 100644 --- a/tests/DataProvider/Multi.php +++ b/tests/DataProvider/Multi.php @@ -239,6 +239,24 @@ public static function getDerivedLoopbackIpAddresses() ); } + /** @return list */ + public static function getNat64LoopbackIpAddresses() + { + return array_merge( + IPv4::getLoopbackIpAddresses(), + IPv6::getCategoryOfIpAddresses(IPv6::LOOPBACK | IPv6::LOOPBACK_NAT64) + ); + } + + /** @return list */ + public static function getTeredoLoopbackIpAddresses() + { + // Teredo is an extraction-only strategy (pack() always throws), so + // IPv4 addresses cannot be merged in like the other strategies above: + // Multi::factory() would fail trying to pack them into 16 bytes. + return IPv6::getCategoryOfIpAddresses(IPv6::LOOPBACK | IPv6::LOOPBACK_TEREDO); + } + /** @return list */ public static function getMulticastIpAddresses() { diff --git a/tests/DataProvider/Strategy/Composite.php b/tests/DataProvider/Strategy/Composite.php new file mode 100644 index 0000000..348608a --- /dev/null +++ b/tests/DataProvider/Strategy/Composite.php @@ -0,0 +1,71 @@ + */ + public static function getValidIpAddresses() + { + $valid = array_map(static function (array $row) { + $row[1] = true; + return $row; + }, self::getValidSequences()); + $invalid = array_map(static function (array $row) { + $row[1] = false; + return $row; + }, self::getInvalidSequences()); + return array_merge($valid, $invalid); + } + + /** @return list */ + public static function getInvalidIpAddresses() + { + // Strings that are neither 4 nor 16 bytes are rejected identically by + // every embedding strategy. + return Mapped::getInvalidIpAddresses(); + } + + /** + * Composite::all() recognises the Mapped, Derived (6to4), NAT64 and + * Teredo embeddings, so its valid sequences are the union of all four. + * + * @return list + */ + public static function getValidSequences() + { + return array_merge( + Mapped::getValidSequences(), + Derived::getValidSequences(), + Nat64::getValidSequences(), + Teredo::getValidSequences() + ); + } + + /** + * Composite::all() deliberately excludes the deprecated "Compatible" + * embedding, so addresses embedded only under that scheme must NOT be + * recognised by the composite. + * + * @return list + */ + public static function getInvalidSequences() + { + return array_map(static function (array $row) { + return [$row[0]]; + }, Compatible::getValidSequences()); + } + + /** + * Composite always packs using its canonical (Mapped) strategy, so only + * Mapped sequences round-trip through pack(). + * + * @return list + */ + public static function getPackableSequences() + { + return Mapped::getValidSequences(); + } +} diff --git a/tests/DataProvider/Strategy/Nat64.php b/tests/DataProvider/Strategy/Nat64.php new file mode 100644 index 0000000..dc3cb19 --- /dev/null +++ b/tests/DataProvider/Strategy/Nat64.php @@ -0,0 +1,56 @@ + */ + public static function getValidIpAddresses() + { + $valid = array_map(static function (array $row) { + $row[1] = true; + return $row; + }, self::getValidSequences()); + $invalid = array_map(static function (array $row) { + $row[1] = false; + return $row; + }, self::getInvalidSequences()); + return array_merge($valid, $invalid); + } + + /** @return list */ + public static function getInvalidIpAddresses() + { + return [ + [pack('H*', '20010db8000000000a608a2e037073')], + [pack('H*', '20010db8000000000a608a2e0370734556')], + ['12345678901234567'], + ['123456789012345'], + ]; + } + + /** @return list */ + public static function getValidSequences() + { + return [ + [pack('H*', '0064ff9b000000000000000000010000'), pack('H*', '00010000')], + [pack('H*', '0064ff9b00000000000000007f000001'), pack('H*', '7f000001')], + [pack('H*', '0064ff9b000000000000000012345678'), pack('H*', '12345678')], + [pack('H*', '0064ff9b00000000000000007f00a001'), pack('H*', '7f00a001')], + ]; + } + + /** @return list */ + public static function getInvalidSequences() + { + return [ + [pack('H*', '000000000000000000000fff00010000')], + [pack('H*', '00010000000000000000ffff0b120cab')], + [pack('H*', '0064ff9c00000000000000007f000001')], + [pack('H*', '00000000000000000000ffff7f00a001')], + [pack('H*', '20010db8000000000a608a2e03707334')], + ]; + } +} diff --git a/tests/DataProvider/Strategy/Teredo.php b/tests/DataProvider/Strategy/Teredo.php new file mode 100644 index 0000000..7900033 --- /dev/null +++ b/tests/DataProvider/Strategy/Teredo.php @@ -0,0 +1,67 @@ + */ + public static function getValidIpAddresses() + { + $valid = array_map(static function (array $row) { + $row[1] = true; + return $row; + }, self::getValidSequences()); + $invalid = array_map(static function (array $row) { + $row[1] = false; + return $row; + }, self::getInvalidSequences()); + return array_merge($valid, $invalid); + } + + /** @return list */ + public static function getInvalidIpAddresses() + { + return [ + [pack('H*', '20010db8000000000a608a2e037073')], + [pack('H*', '20010db8000000000a608a2e0370734556')], + ['12345678901234567'], + ['123456789012345'], + ]; + } + + /** + * The client's public IPv4 address sits in the last 4 bytes XOR'd with + * `0xffffffff`, so the extracted sequence is the bitwise NOT of the last + * 4 bytes of the IPv6 address. + * + * @return list + */ + public static function getValidSequences() + { + return [ + // RFC 4380's worked example: server 65.54.227.120, port 40000, + // client 192.0.2.45. + [pack('H*', '200100004136e378800063bf3ffffdd2'), pack('H*', 'c000022d')], + [pack('H*', '200100004136e378800063bf80fffffe'), pack('H*', '7f000001')], + [pack('H*', '200100004136e378800063bfedcba987'), pack('H*', '12345678')], + // Degenerate all-zero server, flags, and port. + [pack('H*', '20010000000000000000000080fffffe'), pack('H*', '7f000001')], + ]; + } + + /** @return list */ + public static function getInvalidSequences() + { + return [ + // Benchmarking (2001:2::/48), just outside the Teredo /32. + [pack('H*', '20010002000000000000000000000001')], + // Documentation (2001:db8::/32). + [pack('H*', '20010db8000000000a608a2e03707334')], + [pack('H*', '00000000000000000000ffff7f000001')], + [pack('H*', '0064ff9b00000000000000007f000001')], + [pack('H*', '20027f00000100000000000000000000')], + ]; + } +} diff --git a/tests/Strategy/CompositeTest.php b/tests/Strategy/CompositeTest.php new file mode 100644 index 0000000..0f59f39 --- /dev/null +++ b/tests/Strategy/CompositeTest.php @@ -0,0 +1,91 @@ +strategy = Composite::all(); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getInvalidIpAddresses')] + public function testIsEmbeddedReturnsFalseForAStringOtherThan16BytesLong(string $value): void + { + $this->assertFalse($this->strategy->isEmbedded($value)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getValidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getValidIpAddresses')] + public function testIsEmbedded(string $value, bool $isEmbedded): void + { + $this->assertSame($isEmbedded, $this->strategy->isEmbedded($value)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getInvalidIpAddresses')] + public function testExceptionIsThrownWhenTryingToExtractFromStringsNot16Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\ExtractionException::class); + $this->strategy->extract($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getValidSequences')] + public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv4, $this->strategy->extract($ipv6)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getInvalidIpAddresses')] + public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->pack($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Composite::getPackableSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(CompositeDataProvider::class, 'getPackableSequences')] + public function testSequenceCorrectlyPackedIntoIpBinaryFromIpBinary(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv6, $this->strategy->pack($ipv4)); + } +} diff --git a/tests/Strategy/Nat64Test.php b/tests/Strategy/Nat64Test.php new file mode 100644 index 0000000..74fc4bd --- /dev/null +++ b/tests/Strategy/Nat64Test.php @@ -0,0 +1,91 @@ +strategy = new Nat64(); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getInvalidIpAddresses')] + public function testIsEmbeddedReturnsFalseForAStringOtherThan16BytesLong(string $value): void + { + $this->assertFalse($this->strategy->isEmbedded($value)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getValidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getValidIpAddresses')] + public function testIsEmbedded(string $value, bool $isEmbedded): void + { + $this->assertSame($isEmbedded, $this->strategy->isEmbedded($value)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getInvalidIpAddresses')] + public function testExceptionIsThrownWhenTryingToExtractFromStringsNot16Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\ExtractionException::class); + $this->strategy->extract($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getValidSequences')] + public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv4, $this->strategy->extract($ipv6)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getInvalidIpAddresses')] + public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->pack($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Nat64::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(Nat64DataProvider::class, 'getValidSequences')] + public function testSequenceCorrectlyPackedIntoIpBinaryFromIpBinary(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv6, $this->strategy->pack($ipv4)); + } +} diff --git a/tests/Strategy/TeredoTest.php b/tests/Strategy/TeredoTest.php new file mode 100644 index 0000000..a3763ea --- /dev/null +++ b/tests/Strategy/TeredoTest.php @@ -0,0 +1,96 @@ +strategy = new Teredo(); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getInvalidIpAddresses')] + public function testIsEmbeddedReturnsFalseForAStringOtherThan16BytesLong(string $value): void + { + $this->assertFalse($this->strategy->isEmbedded($value)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getValidIpAddresses')] + public function testIsEmbedded(string $value, bool $isEmbedded): void + { + $this->assertSame($isEmbedded, $this->strategy->isEmbedded($value)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getInvalidIpAddresses')] + public function testExceptionIsThrownWhenTryingToExtractFromStringsNot16Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\ExtractionException::class); + $this->strategy->extract($value); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getValidSequences')] + public function testCorrectSequenceExtractedFromIpBinary(string $ipv6, string $ipv4): void + { + $this->assertSame($ipv4, $this->strategy->extract($ipv6)); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getInvalidIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getInvalidIpAddresses')] + public function testExceptionIsThrownWhenTryingToPackStringsNot4Bytes(string $value): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->pack($value); + } + + /** + * Teredo is an extraction-only strategy: a Teredo address cannot be + * constructed from an IPv4 address alone, so pack() throws even for valid + * 4-byte sequences. + * + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getValidSequences')] + public function testExceptionIsThrownWhenTryingToPackValidSequences(string $ipv6, string $ipv4): void + { + $this->expectException(\Darsyn\IP\Exception\Strategy\PackingException::class); + $this->strategy->pack($ipv4); + } +} diff --git a/tests/Version/MultiTest.php b/tests/Version/MultiTest.php index c3f5324..44d5381 100644 --- a/tests/Version/MultiTest.php +++ b/tests/Version/MultiTest.php @@ -327,6 +327,43 @@ public function testIsLoopbackDerived(string $value, bool $isLoopback): void $this->assertSame($isLoopback, $ip->isLoopback()); } + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Multi::getNat64LoopbackIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(MultiDataProvider::class, 'getNat64LoopbackIpAddresses')] + public function testIsLoopbackNat64(string $value, bool $isLoopback): void + { + $ip = IP::factory($value, new Strategy\Nat64()); + $this->assertSame($isLoopback, $ip->isLoopback()); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Multi::getTeredoLoopbackIpAddresses() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(MultiDataProvider::class, 'getTeredoLoopbackIpAddresses')] + public function testIsLoopbackTeredo(string $value, bool $isLoopback): void + { + $ip = IP::factory($value, new Strategy\Teredo()); + $this->assertSame($isLoopback, $ip->isLoopback()); + } + + /** + * Teredo is an extraction-only strategy, so an IPv4 address cannot be + * packed into a Multi instance with it. + * + * @test + */ + #[PHPUnit\Test] + public function testExceptionIsThrownWhenInstantiatingFromIpv4WithTeredoStrategy(): void + { + $this->expectException(InvalidIpAddressException::class); + IP::factory('127.0.0.1', new Strategy\Teredo()); + } + /** * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Multi::getMulticastIpAddresses()