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,12 +29,48 @@ 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 *
3370 * Hostnames are not considered local. Use the HostnameClassifier for those.
3471 */
3572 public function isLocalAddress (string $ ip ): bool {
73+ $ ip = rtrim ($ ip , '. ' );
3674 $ parsedIp = Factory::parseAddressString (
3775 $ ip ,
3876 ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED | ParseStringFlag::MAY_INCLUDE_ZONEID
@@ -43,12 +81,16 @@ public function isLocalAddress(string $ip): bool {
4381 }
4482 /* Replace by normalized form */
4583 if ($ parsedIp instanceof IPv6) {
46- $ ipv4 = $ parsedIp ->toIPv4 ();
47- $ ipv6Bytes = $ parsedIp ->getBytes ();
84+ // rfc8215 is a generic reservation for ipv6/ipv4 translation mechanisms,
85+ // no assumptions can be made about how ipv4 addresses are encoded within.
86+ //
87+ // Thus the only thing we can do is treat them all as local
88+ if ($ this ->rfc8215 ->contains ($ parsedIp )) {
89+ return true ;
90+ }
91+ $ ipv4 = $ this ->getMappedIpv4 ($ parsedIp );
4892 if ($ ipv4 ) {
4993 $ ip = (string )$ ipv4 ;
50- } elseif (array_slice ($ ipv6Bytes , 0 , 4 ) === [0x00 , 0x64 , 0xFF , 0x9B ]) {
51- $ ip = (string )IPv4::fromBytes (array_slice ($ ipv6Bytes , -4 , 4 ));
5294 } else {
5395 $ ip = (string )$ parsedIp ;
5496 }
0 commit comments