diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4ebe9..f435320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - Add NAT64 (RFC 6052 § 2.2) embedding strategy, with named constructors for the Well-known Prefix (`64:ff9b::/96`), operator Network-specific Prefixes, and the RFC 8215 Local-use prefix (`64:ff9b:1::/48`). +- Add `Teredo` embedding strategy for `2001::/32` (according to RFC 4380 § 4). ## `6.0.0` diff --git a/docs/05-strategies.md b/docs/05-strategies.md index e9c701f..013ddba 100644 --- a/docs/05-strategies.md +++ b/docs/05-strategies.md @@ -5,18 +5,18 @@ When using version 4 and version 6 addresses interchangeably (via the 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 four: +address into version 6, so this library offers various strategy implementations: > In the formats below, `X` marks the embedded version 4 address, and `?` marks > bits that play no part in detection/extraction. | Strategy Name | Implementation | Format | |-----------------|---------------------------------|-------------------------------------------| -| 6to4-derived | `Darsyn\IP\Strategy\Derived` | `2002:XXXX:XXXX:????:????:????:????:????` | -| 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` | +| 6to4-derived | `Darsyn\IP\Strategy\Derived` | `2002:XXXX:XXXX:????:????:????:????:????` | | NAT64 | `Darsyn\IP\Strategy\Nat64` | (see below) | +| Teredo | `Darsyn\IP\Strategy\Teredo` | `2001:0000:????:????:????:????:XXXX:XXXX` | +| IPv4-compatible | `Darsyn\IP\Strategy\Compatible` | `0000:0000:0000:0000:0000:0000:XXXX:XXXX` | Each embedding strategy implements the `Darsyn\IP\Strategy\EmbeddingStrategyInterface` which defines methods to: diff --git a/src/Strategy/Teredo.php b/src/Strategy/Teredo.php new file mode 100644 index 0000000..733e654 --- /dev/null +++ b/src/Strategy/Teredo.php @@ -0,0 +1,63 @@ + self::GLOBALLY_REACHABLE_V6 | self::UNICAST_GLOBAL | self::COMPATIBLE | self::LOOPBACK_COMPATIBLE, '::12.34.56.78' => self::GLOBALLY_REACHABLE_V6 | self::UNICAST_GLOBAL | self::COMPATIBLE, '0::000:0000:b12:cab' => self::GLOBALLY_REACHABLE_V6 | self::UNICAST_GLOBAL | self::COMPATIBLE, + '2001:0:4136:e378:8000:63bf:80ff:fffe' => self::UNICAST_OTHER | self::TEREDO | self::LOOPBACK_TEREDO, '1cc9:7d7f:2a9f:cabd:9186:2be5:bef1:6a54' => self::GLOBALLY_REACHABLE_V6 | self::UNICAST_GLOBAL, 'b638:cc70:716:c4d4:f69c:4ee3:6c65:a0b2' => self::GLOBALLY_REACHABLE_V6 | self::UNICAST_GLOBAL, '140c:12f1:6e6f:c0bb:980e:3816:3e52:1193' => self::GLOBALLY_REACHABLE_V6 | self::UNICAST_GLOBAL, diff --git a/tests/DataProvider/IpDataProviderInterface.php b/tests/DataProvider/IpDataProviderInterface.php index b780628..932215c 100644 --- a/tests/DataProvider/IpDataProviderInterface.php +++ b/tests/DataProvider/IpDataProviderInterface.php @@ -37,9 +37,11 @@ interface IpDataProviderInterface 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 TEREDO = 1 << 26; + public const LOOPBACK_MAPPED = 1 << 27; + public const LOOPBACK_COMPATIBLE = 1 << 28; + public const LOOPBACK_DERIVED = 1 << 29; + public const LOOPBACK_TEREDO = 1 << 30; // Combinations public const GLOBALLY_REACHABLE = 0 @@ -48,7 +50,8 @@ interface IpDataProviderInterface public const LOOPBACK_EMBEDDED = 0 | self::LOOPBACK_MAPPED | self::LOOPBACK_COMPATIBLE - | self::LOOPBACK_DERIVED; + | self::LOOPBACK_DERIVED + | 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 4bd1238..fab1a77 100644 --- a/tests/DataProvider/Multi.php +++ b/tests/DataProvider/Multi.php @@ -239,6 +239,15 @@ public static function getDerivedLoopbackIpAddresses() ); } + /** @return list */ + public static function getTeredoLoopbackIpAddresses() + { + return array_merge( + IPv4::getLoopbackIpAddresses(), + IPv6::getCategoryOfIpAddresses(IPv6::LOOPBACK | IPv6::LOOPBACK_TEREDO) + ); + } + /** @return list */ public static function getMulticastIpAddresses() { diff --git a/tests/DataProvider/Strategy/Teredo.php b/tests/DataProvider/Strategy/Teredo.php new file mode 100644 index 0000000..a63289f --- /dev/null +++ b/tests/DataProvider/Strategy/Teredo.php @@ -0,0 +1,83 @@ + */ + 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 [ + // Well-known 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')], + // Canonical all-zero server, flags, and port. + [pack('H*', '20010000000000000000000080fffffe'), pack('H*', '7f000001')], + ]; + } + + /** + * `pack()` reconstructs only the canonical Teredo form (zeroed server, + * flags, and UDP port). These pairs map an IPv4 address to the address + * `pack()` produces, not necessarily the address it was extracted from (see + * getValidSequences). + * + * @return list + */ + public static function getValidPackSequences() + { + return [ + [pack('H*', 'c000022d'), pack('H*', '2001000000000000000000003ffffdd2')], + [pack('H*', '7f000001'), pack('H*', '20010000000000000000000080fffffe')], + [pack('H*', '12345678'), pack('H*', '200100000000000000000000edcba987')], + ]; + } + + /** @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/TeredoTest.php b/tests/Strategy/TeredoTest.php new file mode 100644 index 0000000..be34190 --- /dev/null +++ b/tests/Strategy/TeredoTest.php @@ -0,0 +1,91 @@ +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); + } + + /** + * @test + * @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Teredo::getValidPackSequences() + */ + #[PHPUnit\Test] + #[PHPUnit\DataProviderExternal(TeredoDataProvider::class, 'getValidPackSequences')] + public function testSequenceCorrectlyPackedIntoIpBinaryFromIpBinary(string $ipv4, string $ipv6): void + { + $this->assertSame($ipv6, $this->strategy->pack($ipv4)); + } +} diff --git a/tests/Version/MultiTest.php b/tests/Version/MultiTest.php index 5e7fca8..4058ed0 100644 --- a/tests/Version/MultiTest.php +++ b/tests/Version/MultiTest.php @@ -327,6 +327,18 @@ public function testIsLoopbackDerived(string $value, bool $isLoopback): void $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()); + } + /** * @test * @dataProvider \Darsyn\IP\Tests\DataProvider\Multi::getMulticastIpAddresses()