Skip to content

Commit 5dc786b

Browse files
committed
fix: handle additional ipv6/ipv4 translation mechanisms when checking if remote is local
Signed-off-by: Robin Appelman <robin@icewind.nl> # Conflicts: # tests/lib/Net/IpAddressClassifierTest.php
1 parent e68d903 commit 5dc786b

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

lib/private/Net/IpAddressClassifier.php

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use IPLib\Address\IPv6;
1414
use IPLib\Factory;
1515
use IPLib\ParseStringFlag;
16+
use IPLib\Range\RangeInterface;
17+
use IPLib\Range\Subnet;
1618
use Symfony\Component\HttpFoundation\IpUtils;
1719
use function filter_var;
1820

@@ -27,6 +29,41 @@ class IpAddressClassifier {
2729
'192.0.0.0/24', // See RFC 6890
2830
];
2931

32+
private RangeInterface $nat64Range;
33+
private RangeInterface $rfc8215;
34+
private RangeInterface $teredo;
35+
private RangeInterface $ipv4Compatible;
36+
37+
public function __construct() {
38+
$this->nat64Range = Subnet::parseString('64:ff9b::/96');
39+
$this->rfc8215 = Subnet::parseString('64:ff9b:1::/48');
40+
$this->teredo = Subnet::parseString('2001::/32');
41+
$this->ipv4Compatible = Subnet::parseString('::0:0/96');
42+
}
43+
44+
/**
45+
* Get the ipv4 that an ipv6 address maps to, if any.
46+
*
47+
* Note that this is not just ipv6 representations of ipv4 addresses,
48+
* but also any NAT or proxy style translation addresses
49+
*/
50+
public function getMappedIpv4(IPv6 $ip): ?IPv4 {
51+
$ipv4 = $ip->toIPv4();
52+
$ipv6Bytes = $ip->getBytes();
53+
if ($ipv4) {
54+
return $ipv4;
55+
} elseif ($this->nat64Range->contains($ip)) {
56+
return IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4));
57+
} elseif ($this->ipv4Compatible->contains($ip)) {
58+
return IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4));
59+
} elseif ($this->teredo->contains($ip)) {
60+
$xorBytes = array_slice($ipv6Bytes, -4, 4);
61+
return IPv4::fromBytes(array_map(fn (int $byte) => $byte ^ 0xFF, $xorBytes));
62+
}
63+
64+
return null;
65+
}
66+
3067
/**
3168
* Check host identifier for local IPv4 and IPv6 address ranges
3269
*
@@ -43,12 +80,16 @@ public function isLocalAddress(string $ip): bool {
4380
}
4481
/* Replace by normalized form */
4582
if ($parsedIp instanceof IPv6) {
46-
$ipv4 = $parsedIp->toIPv4();
47-
$ipv6Bytes = $parsedIp->getBytes();
83+
// rfc8215 is a generic reservation for ipv6/ipv4 translation mechanisms,
84+
// no assumptions can be made about how ipv4 addresses are encoded within.
85+
//
86+
// Thus the only thing we can do is treat them all as local
87+
if ($this->rfc8215->contains($parsedIp)) {
88+
return true;
89+
}
90+
$ipv4 = $this->getMappedIpv4($parsedIp);
4891
if ($ipv4) {
4992
$ip = (string)$ipv4;
50-
} elseif (array_slice($ipv6Bytes, 0, 4) === [0x00, 0x64, 0xFF, 0x9B]) {
51-
$ip = (string)IPv4::fromBytes(array_slice($ipv6Bytes, -4, 4));
5293
} else {
5394
$ip = (string)$parsedIp;
5495
}

tests/lib/Net/IpAddressClassifierTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
namespace lib\Net;
1111

12+
use IPLib\Address\IPv4;
13+
use IPLib\Address\IPv6;
1214
use OC\Net\IpAddressClassifier;
1315
use Test\TestCase;
1416

@@ -53,6 +55,7 @@ public static function localIpAddressData(): array {
5355
['100.100.100.200'],
5456
['192.0.0.1'],
5557
['64:ff9b::a9fe:a9fe'], // NAT64 of 169.254.169.254
58+
['64:ff9b:1::a9fe:a9fe'], // rfc8215
5659
['::ffff:127.0.0.1'],
5760
['2130706433'],
5861
['0177.0.0.1'],
@@ -66,4 +69,26 @@ public function testLocalAddress(string $ip): void {
6669

6770
self::assertTrue($isLocal);
6871
}
72+
73+
public static function mappedAddresses(): array {
74+
return [
75+
['64:ff9b::a9fe:a9fe', '169.254.169.254'],
76+
['::ffff:7f00:1', '127.0.0.1'],
77+
['::127.0.0.1', '127.0.0.1'],
78+
['::7f00:1', '127.0.0.1'],
79+
['2001:0000:4136:e378:8000:63bf:3fff:fdd2', '192.0.2.45'],
80+
['2001:4860:4860::8888', null],
81+
];
82+
}
83+
84+
#[\PHPUnit\Framework\Attributes\DataProvider('mappedAddresses')]
85+
public function testMappedAddresses(string $ipv6, ?string $ipv4): void {
86+
$mapped = $this->classifier->getMappedIpv4(IPv6::parseString($ipv6));
87+
88+
if ($ipv4 === null) {
89+
self::assertEquals(null, $mapped);
90+
} else {
91+
self::assertEquals(IPv4::parseString($ipv4), $mapped);
92+
}
93+
}
6994
}

0 commit comments

Comments
 (0)