From 5dc786b937e084ea2d25b2af181844d8cfce1f5b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 10 Jun 2026 11:30:31 +0200 Subject: [PATCH] fix: handle additional ipv6/ipv4 translation mechanisms when checking if remote is local Signed-off-by: Robin Appelman # Conflicts: # tests/lib/Net/IpAddressClassifierTest.php --- lib/private/Net/IpAddressClassifier.php | 49 +++++++++++++++++++++-- tests/lib/Net/IpAddressClassifierTest.php | 25 ++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/lib/private/Net/IpAddressClassifier.php b/lib/private/Net/IpAddressClassifier.php index 9d701b21e41..ce69bf65d7a 100644 --- a/lib/private/Net/IpAddressClassifier.php +++ b/lib/private/Net/IpAddressClassifier.php @@ -13,6 +13,8 @@ use IPLib\Address\IPv4; use IPLib\Address\IPv6; use IPLib\Factory; use IPLib\ParseStringFlag; +use IPLib\Range\RangeInterface; +use IPLib\Range\Subnet; use Symfony\Component\HttpFoundation\IpUtils; use function filter_var; @@ -27,6 +29,41 @@ class IpAddressClassifier { '192.0.0.0/24', // See RFC 6890 ]; + private RangeInterface $nat64Range; + private RangeInterface $rfc8215; + private RangeInterface $teredo; + private RangeInterface $ipv4Compatible; + + public function __construct() { + $this->nat64Range = Subnet::parseString('64:ff9b::/96'); + $this->rfc8215 = Subnet::parseString('64:ff9b:1::/48'); + $this->teredo = Subnet::parseString('2001::/32'); + $this->ipv4Compatible = Subnet::parseString('::0:0/96'); + } + + /** + * Get the ipv4 that an ipv6 address maps to, if any. + * + * Note that this is not just ipv6 representations of ipv4 addresses, + * but also any NAT or proxy style translation addresses + */ + public function getMappedIpv4(IPv6 $ip): ?IPv4 { + $ipv4 = $ip->toIPv4(); + $ipv6Bytes = $ip->getBytes(); + if ($ipv4) { + return $ipv4; + } elseif ($this->nat64Range->contains($ip)) { + return IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4)); + } elseif ($this->ipv4Compatible->contains($ip)) { + return IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4)); + } elseif ($this->teredo->contains($ip)) { + $xorBytes = array_slice($ipv6Bytes, -4, 4); + return IPv4::fromBytes(array_map(fn (int $byte) => $byte ^ 0xFF, $xorBytes)); + } + + return null; + } + /** * Check host identifier for local IPv4 and IPv6 address ranges * @@ -43,12 +80,16 @@ class IpAddressClassifier { } /* Replace by normalized form */ if ($parsedIp instanceof IPv6) { - $ipv4 = $parsedIp->toIPv4(); - $ipv6Bytes = $parsedIp->getBytes(); + // rfc8215 is a generic reservation for ipv6/ipv4 translation mechanisms, + // no assumptions can be made about how ipv4 addresses are encoded within. + // + // Thus the only thing we can do is treat them all as local + if ($this->rfc8215->contains($parsedIp)) { + return true; + } + $ipv4 = $this->getMappedIpv4($parsedIp); if ($ipv4) { $ip = (string)$ipv4; - } elseif (array_slice($ipv6Bytes, 0, 4) === [0x00, 0x64, 0xFF, 0x9B]) { - $ip = (string)IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4)); } else { $ip = (string)$parsedIp; } diff --git a/tests/lib/Net/IpAddressClassifierTest.php b/tests/lib/Net/IpAddressClassifierTest.php index 4af6c399c61..d4f19f5d337 100644 --- a/tests/lib/Net/IpAddressClassifierTest.php +++ b/tests/lib/Net/IpAddressClassifierTest.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace lib\Net; +use IPLib\Address\IPv4; +use IPLib\Address\IPv6; use OC\Net\IpAddressClassifier; use Test\TestCase; @@ -53,6 +55,7 @@ class IpAddressClassifierTest extends TestCase { ['100.100.100.200'], ['192.0.0.1'], ['64:ff9b::a9fe:a9fe'], // NAT64 of 169.254.169.254 + ['64:ff9b:1::a9fe:a9fe'], // rfc8215 ['::ffff:127.0.0.1'], ['2130706433'], ['0177.0.0.1'], @@ -66,4 +69,26 @@ class IpAddressClassifierTest extends TestCase { self::assertTrue($isLocal); } + + public static function mappedAddresses(): array { + return [ + ['64:ff9b::a9fe:a9fe', '169.254.169.254'], + ['::ffff:7f00:1', '127.0.0.1'], + ['::127.0.0.1', '127.0.0.1'], + ['::7f00:1', '127.0.0.1'], + ['2001:0000:4136:e378:8000:63bf:3fff:fdd2', '192.0.2.45'], + ['2001:4860:4860::8888', null], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('mappedAddresses')] + public function testMappedAddresses(string $ipv6, ?string $ipv4): void { + $mapped = $this->classifier->getMappedIpv4(IPv6::parseString($ipv6)); + + if ($ipv4 === null) { + self::assertEquals(null, $mapped); + } else { + self::assertEquals(IPv4::parseString($ipv4), $mapped); + } + } }