1313use IPLib \Address \IPv6 ;
1414use IPLib \Factory ;
1515use IPLib \ParseStringFlag ;
16+ use IPLib \Range \RangeInterface ;
17+ use IPLib \Range \Subnet ;
1618use Symfony \Component \HttpFoundation \IpUtils ;
1719use 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 }
0 commit comments