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 @@ -2,6 +2,8 @@

## `6.x`

- Split embedding strategies into canonical and non-canonical packers via the
new `Strategy\CanonicalEmbeddingInterface` bridge.
- Add strict parsing methods to the new `Contracts\FactoryInterface` interface:
try/from protocol, binary, hex.
- Deprecate `factory()` in favour of the strict named constructors
Expand Down
46 changes: 45 additions & 1 deletion docs/05-strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Each embedding strategy implements the
- Detect whether a version 4 address is embedded into a version 6 address,
- Extracting a version 4 address from a version 6 address, and
- Packing a version 4 address into a version 6 address according to the given
strategy.
strategy (see [Canonical and Non-Canonical Packing](#canonical-and-non-canonical-packing)).

## Specifying a Strategy

Expand All @@ -45,6 +45,50 @@ IP::setDefaultEmbeddingStrategy(new Strategy\Compatible);
$ip = IP::factory('127.0.0.1', new Strategy\Derived);
```

## Canonical and Non-Canonical Packing

Some embedding strategies (6to4-derived, Teredo, and NAT64 with a prefix shorter
than `/96`) carry bits *outside* the embedded version 4 address — a Teredo
address, for example, also carries the tunnel server's address, flags, and the
client's UDP port. The original `pack()` always produces the **canonical** form,
zeroing every such bit; this is the right behaviour when constructing an address
from a bare version 4 address, but it silently discards information when re-packing
an existing version 6 address.

`Darsyn\IP\Strategy\CanonicalEmbeddingInterface` provides methods (and
deprecates `pack()`):
- `packIntoCanonical(string $ipv4): string` is identical to `pack()`: every bit
outside the embedded version 4 address is normalised/zeroed.
- `packIntoNonCanonical(string $ipv6, string $ipv4): string` replaces only the
embedded version 4 bit positions of `$ipv6`, preserving every other bit. A
`PackingException` is thrown if `$ipv6` is not recognised by the strategy.

> `CanonicalEmbeddingInterface` is a _temporary scaffolding_ (a bridge interface)
> to maintain backwards compatibility for `EmbeddingStrategyInterface` on the
> `6.x` branch. Both interfaces will be combined back into `EmbeddingStrategyInterface`
> with `pack()` removed on the next major version bump.
>
> Type-hint `EmbeddingStrategyInterface` and use feature detection
> (`instanceof CanonicalEmbeddingInterface`) if you wish to use the new methods
> in custom strategies.

```php
<?php
use Darsyn\IP\Strategy\Teredo;

$strategy = new Teredo;
// A Teredo address carrying server 65.54.227.120, flags, and client port 40000.
$ipv6 = pack('H*', '200100004136e378800063bf3ffffdd2');
$ipv4 = pack('H*', '7f000001'); // 127.0.0.1

// Canonical packing keeps only the client address, zeroing server/flags/port.
bin2hex($strategy->packIntoCanonical($ipv4)); // string("20010000000000000000000080fffffe")

// Non-canonical packing embeds the new client but preserves the server, flags,
// and port carried by the original address.
bin2hex($strategy->packIntoNonCanonical($ipv6, $ipv4)); // string("200100004136e378800063bf80fffffe")
```

## NAT64 (RFC 6052)
Unlike the other strategies, `Nat64` has no public constructor — it is
instantiated via named constructors only:
Expand Down
36 changes: 36 additions & 0 deletions src/Strategy/CanonicalEmbeddingInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Darsyn\IP\Strategy;

/**
* Temporary scaffolding that splits embedding into a canonical and a
* non-canonical packer; it will be folded into `EmbeddingStrategyInterface` in
* the next major version, to avoid breaking user-defined embedding strategies
* that already implement EmbeddingStrategyInterface.
*/
interface CanonicalEmbeddingInterface extends EmbeddingStrategyInterface
{
/**
* Convert the supplied IPv4 binary string into the canonical embedded IPv6
* binary string, according to the implemented embedding strategy. Every bit
* outside the embedded IPv4 address is normalised (the prefix is written
* and all other fields are zeroed). This is the behaviour of the deprecated
* `EmbeddingStrategyInterface::pack()`.
*
* @throws \Darsyn\IP\Exception\Strategy\PackingException
*/
public function packIntoCanonical(string $ipv4): string;

/**
* Embed the supplied IPv4 binary string into the supplied IPv6 binary
* string, replacing only the embedded-IPv4 bit positions and preserving
* every other bit of the IPv6 address
*
* @throws \Darsyn\IP\Exception\Strategy\PackingException
* When the IPv6 binary string is not recognised by the strategy, or the
* IPv4 binary string is not 4 bytes long.
*/
public function packIntoNonCanonical(string $ipv6, string $ipv4): string;
}
24 changes: 20 additions & 4 deletions src/Strategy/Compatible.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*
* Note: this format is deprecated and retained only for backwards compatibility.
*/
class Compatible implements EmbeddingStrategyInterface
class Compatible implements CanonicalEmbeddingInterface
{
public function isEmbedded(string $binary): bool
{
Expand All @@ -29,11 +29,27 @@ public function extract(string $binary): string
throw new StrategyException\ExtractionException($binary, $this);
}

/** @deprecated Use packIntoCanonical() instead. */
public function pack(string $binary): string
{
if (4 === MbString::getLength($binary)) {
return "\0\0\0\0\0\0\0\0\0\0\0\0" . $binary;
return $this->packIntoCanonical($binary);
}

public function packIntoCanonical(string $ipv4): string
{
if (4 === MbString::getLength($ipv4)) {
return "\0\0\0\0\0\0\0\0\0\0\0\0" . $ipv4;
}
throw new StrategyException\PackingException($ipv4, $this);
}

public function packIntoNonCanonical(string $ipv6, string $ipv4): string
{
if (!$this->isEmbedded($ipv6)) {
throw new StrategyException\PackingException($ipv6, $this);
}
throw new StrategyException\PackingException($binary, $this);
// The prefix (bytes 0-11) + the IPv4 address (bytes 12-15) occupy the
// entire IPv6 address space; defer to canonical.
return $this->packIntoCanonical($ipv4);
}
}
33 changes: 31 additions & 2 deletions src/Strategy/Composite.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* several embedding schemes on input while always producing a single canonical
* form on output.
*/
class Composite implements EmbeddingStrategyInterface
class Composite implements CanonicalEmbeddingInterface
{
/** @var EmbeddingStrategyInterface $packer */
private $packer;
Expand Down Expand Up @@ -54,8 +54,37 @@ public function extract(string $binary): string
throw new StrategyException\ExtractionException($binary, $this);
}

/** @deprecated Use packIntoCanonical() instead. */
public function pack(string $binary): string
{
return $this->packer->pack($binary);
return $this->packIntoCanonical($binary);
}

public function packIntoCanonical(string $ipv4): string
{
if ($this->packer instanceof CanonicalEmbeddingInterface) {
return $this->packer->packIntoCanonical($ipv4);
}
// Graceful degradation for a userland packer predating the bridge.
/** @phpstan-ignore method.deprecated */
return $this->packer->pack($ipv4);
}

/**
* Delegate to the first strategy (in constructor order) that recognises the
* supplied IPv6 address, mirroring extract().
*/
public function packIntoNonCanonical(string $ipv6, string $ipv4): string
{
foreach ($this->strategies as $strategy) {
if ($strategy->isEmbedded($ipv6)) {
if ($strategy instanceof CanonicalEmbeddingInterface) {
return $strategy->packIntoNonCanonical($ipv6, $ipv4);
}
/** @phpstan-ignore method.deprecated */
return $strategy->pack($ipv4);
}
}
throw new StrategyException\PackingException($ipv6, $this);
}
}
34 changes: 29 additions & 5 deletions src/Strategy/Derived.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* N.B. Legacy, but not formally deprecated (only 6to4 anycast was deprecated
* via RFC 7526).
*/
class Derived implements EmbeddingStrategyInterface
class Derived implements CanonicalEmbeddingInterface
{
public function isEmbedded(string $binary): bool
{
Expand All @@ -46,15 +46,39 @@ public function extract(string $binary): string
* 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.
* (`2002:XXXX:XXXX::`), not the original. Use `packIntoNonCanonical()` to
* preserve the SLA ID and interface ID bits.
*
* @deprecated Use packIntoCanonical() instead.
*/
public function pack(string $binary): string
{
if (4 === MbString::getLength($binary)) {
return $this->packIntoCanonical($binary);
}

public function packIntoCanonical(string $ipv4): string
{
if (4 === MbString::getLength($ipv4)) {
// 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;
return Binary::fromHex('2002') . $ipv4 . $subnetInterface;
}
throw new StrategyException\PackingException($ipv4, $this);
}

/**
* Replace only the embedded IPv4 address (bits 16-47); the 6to4 prefix
* (bits 0-15) and the SLA ID and interface ID fields (bits 48-127) of the
* supplied IPv6 address pass through unchanged.
*/
public function packIntoNonCanonical(string $ipv6, string $ipv4): string
{
if (!$this->isEmbedded($ipv6)) {
throw new StrategyException\PackingException($ipv6, $this);
}
if (4 !== MbString::getLength($ipv4)) {
throw new StrategyException\PackingException($ipv4, $this);
}
throw new StrategyException\PackingException($binary, $this);
return MbString::subString($ipv6, 0, 2) . $ipv4 . MbString::subString($ipv6, 6, 10);
}
}
4 changes: 4 additions & 0 deletions src/Strategy/EmbeddingStrategyInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public function extract(string $binary): string;
* Convert the supplied IPv4 binary string into an embedded IPv6 binary
* string, according to the implemented embedding strategy.
*
* @deprecated Implement `CanonicalEmbeddingInterface` and use
* `packIntoCanonical()` (identical behaviour) or `packIntoNonCanonical()`
* instead. This method will be replaced by the two split packers in the
* next major version.
* @throws \Darsyn\IP\Exception\Strategy\PackingException
*/
public function pack(string $binary): string;
Expand Down
24 changes: 20 additions & 4 deletions src/Strategy/Mapped.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*
* Reserved by protocol, and not globally reachable (as an IPv6 address).
*/
class Mapped implements EmbeddingStrategyInterface
class Mapped implements CanonicalEmbeddingInterface
{
public function isEmbedded(string $binary): bool
{
Expand All @@ -30,11 +30,27 @@ public function extract(string $binary): string
throw new StrategyException\ExtractionException($binary, $this);
}

/** @deprecated Use packIntoCanonical() instead. */
public function pack(string $binary): string
{
if (4 === MbString::getLength($binary)) {
return Binary::fromHex('00000000000000000000ffff') . $binary;
return $this->packIntoCanonical($binary);
}

public function packIntoCanonical(string $ipv4): string
{
if (4 === MbString::getLength($ipv4)) {
return Binary::fromHex('00000000000000000000ffff') . $ipv4;
}
throw new StrategyException\PackingException($ipv4, $this);
}

public function packIntoNonCanonical(string $ipv6, string $ipv4): string
{
if (!$this->isEmbedded($ipv6)) {
throw new StrategyException\PackingException($ipv6, $this);
}
throw new StrategyException\PackingException($binary, $this);
// The prefix (bytes 0-11) + the IPv4 address (bytes 12-15) occupy the
// entire IPv6 address space; defer to canonical.
return $this->packIntoCanonical($ipv4);
}
}
49 changes: 43 additions & 6 deletions src/Strategy/Nat64.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* within the Well-Known Prefix; the restriction does not apply to
* Network-Specific Prefixes. This library deliberately does not enforce it.
*/
class Nat64 implements EmbeddingStrategyInterface
class Nat64 implements CanonicalEmbeddingInterface
{
/** Hex representation of the Well-Known Prefix `64:ff9b::/96` (RFC 6052 § 2.1). */
public const WELL_KNOWN_PREFIX = '0064ff9b000000000000000000000000';
Expand Down Expand Up @@ -165,29 +165,66 @@ public function extract(string $binary): string
* address; the reserved octet (bits 64 to 71) and the suffix are
* zero-filled, so a direct pass-through (extract-pack) of a non-canonical
* address reconstructs the canonical form, not the original.
*
* @deprecated Use packIntoCanonical() instead.
*/
public function pack(string $binary): string
{
return $this->packIntoCanonical($binary);
}

public function packIntoCanonical(string $ipv4): string
{
// Note: non-global IPv4 addresses should not be packed into the
// Well-Known Prefix (the restriction does not apply to
// Network-Specific Prefixes), but is not enforced here. It is down to
// the user of this library to know when to use which embedding
// strategy.
if (4 !== MbString::getLength($binary)) {
throw new StrategyException\PackingException($binary, $this);
if (4 !== MbString::getLength($ipv4)) {
throw new StrategyException\PackingException($ipv4, $this);
}
$bytes = \intdiv($this->length, 8);
$prefix = MbString::subString($this->prefix, 0, $bytes);
if (96 === $this->length) {
return $prefix . $binary;
return $prefix . $ipv4;
}
// The reserved octet (bits 64 to 71) must be zero (RFC 6052 § 2.2),
// and the suffix should be zero; zero-fill both.
$split = 8 - $bytes;
$withoutSuffix = $prefix
. MbString::subString($binary, 0, $split)
. MbString::subString($ipv4, 0, $split)
. "\0"
. MbString::subString($binary, $split);
. MbString::subString($ipv4, $split);
return MbString::padString($withoutSuffix, 16, "\0");
}

/**
* Replace only the embedded IPv4 address; the configured prefix and the
* suffix bits of the supplied IPv6 address pass through unchanged. The
* reserved octet (bits 64 to 71) is NOT preservation space: RFC 6052 § 2.2
* mandates it be zero, so it stays zero regardless of the supplied address.
*/
public function packIntoNonCanonical(string $ipv6, string $ipv4): string
{
if (!$this->isEmbedded($ipv6)) {
throw new StrategyException\PackingException($ipv6, $this);
}
if (4 !== MbString::getLength($ipv4)) {
throw new StrategyException\PackingException($ipv4, $this);
}
$bytes = \intdiv($this->length, 8);
$prefix = MbString::subString($this->prefix, 0, $bytes);
if (96 === $this->length) {
// No reserved octet and no suffix at /96: identical to canonical.
return $prefix . $ipv4;
}
// The reserved octet (bits 64 to 71) stays zero; the suffix (the bits
// after the embedded address) is taken from the supplied IPv6 address.
$split = 8 - $bytes;
return $prefix
. MbString::subString($ipv4, 0, $split)
. "\0"
. MbString::subString($ipv4, $split)
. MbString::subString($ipv6, $bytes + 5);
}
}
Loading