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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
- Bugfix: classify full `fc00::/7` block as private use.
- Add (or update) community standards: `SECURITY.md`, `CODE_OF_CONDUCT.md` and
`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.

## `6.0.0`

Expand Down
2 changes: 1 addition & 1 deletion docs/05-strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ for the main three:

| Strategy Name | Implementation | Format |
|-----------------|---------------------------------|-------------------------------------------|
| 6to4-derived | `Darsyn\IP\Strategy\Derived` | `2002:XXXX:XXXX:0000:0000:0000:0000:0000` |
| 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` |

Expand Down
6 changes: 6 additions & 0 deletions src/Strategy/Compatible.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 28 additions & 3 deletions src/Strategy/Derived.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,28 @@
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") § 2.
*
* Note: `isEmbedded()` detects any address within the 6to4 block (`2002::/16`,
* RFC 3056) with the IPv4 address embedded in bits 16-47. The canonical address
* has the remaining 80 bits zeroed, whereas non-canonical addresses embed the
* SLA ID (subnet) and interface ID.
*
* Consequently, version-aware operations on Multi-type IPs treat every 6to4
* host address as an embedded IPv4 address, even though only the canonical
* form can round-trip through extraction.
*
* N.B. Legacy, but not formally deprecated (only 6to4 anycast was deprecated
* via RFC 7526).
*/
class Derived implements EmbeddingStrategyInterface
{
public function isEmbedded(string $binary): bool
{
return 16 === MbString::getLength($binary)
&& MbString::subString($binary, 0, 2) === Binary::fromHex('2002')
&& "\0\0\0\0\0\0\0\0\0\0" === MbString::subString($binary, 6, 10);
&& MbString::subString($binary, 0, 2) === Binary::fromHex('2002');
}

public function extract(string $binary): string
Expand All @@ -25,10 +40,20 @@ public function extract(string $binary): string
throw new StrategyException\ExtractionException($binary, $this);
}

/**
* Warning: packing is not the inverse of extraction for every address that
* `isEmbedded()` accepts. Extraction keeps only the IPv4 address carried
* in bits 16-47 of a 6to4 address.
* The SLA ID and interface ID bits are lost so a direct pass-through
* (extract-pack) reconstructs the canonical Derived address
* (`2002:XXXX:XXXX::`), not the original.
*/
public function pack(string $binary): string
{
if (4 === MbString::getLength($binary)) {
return Binary::fromHex('2002') . $binary . "\0\0\0\0\0\0\0\0\0\0";
// Zero the SLA ID (subnet) and interface ID fields.
$subnetInterface = "\0\0\0\0\0\0\0\0\0\0";
return Binary::fromHex('2002') . $binary . $subnetInterface;
}
throw new StrategyException\PackingException($binary, $this);
}
Expand Down
6 changes: 6 additions & 0 deletions src/Strategy/Mapped.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/DataProvider/IPv6.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ public static function getCategorizedIpAddresses()
'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,
'::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,
Expand Down
12 changes: 12 additions & 0 deletions tests/DataProvider/Strategy/Derived.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ public static function getValidSequences()
];
}

/** @return list<array{string, string, string}> */
public static function getNonCanonicalSequences()
{
return [
// [ non-canonical 6to4 binary, embedded IPv4 (bits 16-47), canonical 6to4 binary ].
// Non-zero SLA ID (subnet), zeroed interface ID.
[pack('H*', '2002c000020112340000000000000000'), pack('H*', 'c0000201'), pack('H*', '2002c000020100000000000000000000')],
// Zeroed SLA ID, non-zero interface ID.
[pack('H*', '2002c00002010000dead00000000beef'), pack('H*', 'c0000201'), pack('H*', '2002c000020100000000000000000000')],
];
}

/** @return list<array{string}> */
public static function getInvalidSequences()
{
Expand Down
34 changes: 34 additions & 0 deletions tests/Strategy/DerivedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,38 @@ public function testSequenceCorrectlyPackedIntoIpBinaryFromIpBinary(string $ipv6
{
$this->assertSame($ipv6, $this->strategy->pack($ipv4));
}

/**
* @test
* @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getNonCanonicalSequences()
*/
#[PHPUnit\Test]
#[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getNonCanonicalSequences')]
public function testNonCanonical6to4IsEmbedded(string $ipv6, string $ipv4, string $canonical): void
{
$this->assertTrue($this->strategy->isEmbedded($ipv6));
}

/**
* @test
* @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getNonCanonicalSequences()
*/
#[PHPUnit\Test]
#[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getNonCanonicalSequences')]
public function testCorrectSequenceExtractedFromNonCanonical6to4(string $ipv6, string $ipv4, string $canonical): void
{
$this->assertSame($ipv4, $this->strategy->extract($ipv6));
}

/**
* @test
* @dataProvider \Darsyn\IP\Tests\DataProvider\Strategy\Derived::getNonCanonicalSequences()
*/
#[PHPUnit\Test]
#[PHPUnit\DataProviderExternal(DerivedDataProvider::class, 'getNonCanonicalSequences')]
public function testNonCanonical6to4CanonicalisedByExtractThenPack(string $ipv6, string $ipv4, string $canonical): void
{
$this->assertNotSame($canonical, $ipv6);
$this->assertSame($canonical, $this->strategy->pack($this->strategy->extract($ipv6)));
}
}