Is inet_pton() broken for some IPv6 addresses that “look like” IPv4 addresses?

元气小坏坏 提交于 2019-12-08 05:07:34

问题


I'm using PHP version 5.2.17, and I see that the following works as expected:

$x = inet_pton('::F');
$y = inet_ntop($x);
print "::F -> $y\n";

Output: ::F -> ::f

But the following does not:

$a = inet_pton('::FEEF:1886');
$b = inet_ntop($a);
print "::FEEF:1886 -> $b\n";

Output: ::FEEF:1886 -> ::254.239.24.134

I would have expected the second code snippet to produce this output:

::FEEF:1886 -> ::feef:1886

What is it about the IPv6 address ::FEEF:1886 that makes PHP think it is really an IPv4 address? The inet_ntop/inet_pton conversion works correctly with other addresses having 0 in the "high" 96 bits (e.g. ::F).

EDIT: My first thought was that this might be a bug in my version of PHP, but using this online PHP sandbox I see the same behavior for PHP versions up through 5.6.2. So either this is deliberate (in which case I would dearly like to know the reason for this behavior) or a bug which persists in modern versions of PHP.

ADDENDUM: I opened PHP Bug 69232 on March 12, 2015 for this apparent inconsistency in the behavior of inet_ntop() for addresses in ::/96.


回答1:


The textual representation of IPv6 addresses permit for multiple different valid representations of every IPv6 address.

This means that all of these valid textual representations of an IPv6 address each map to the same binary 128 bit string when passed through inet_pton.

However when converting the binary 128 bit string to textual representation using inet_ntop, it can obviously only output one of the many valid strings representing that IP address. The one it chose is called the canonical representation.

It is always valid to write the last 32 bits of an IPv6 address using IPv4 notation. However only a few classes of IPv6 addresses used that format as their canonical representation.

::/96 has been deprecated, but that just means those addresses are not supposed to be used anymore, it doesn't affect how they are treated by inet_pton and inet_ntop.

::ffff:0.0.0.0/96 is another prefix, which use IPv4 notation in their canonical representation. That prefix is used for IPv4 compatibility in the socket API, but those are never send on the wire, because they are for situations where the traffic on the wire will be IPv4.




回答2:


What you're looking at is an IPv4 address being represented (incorrectly) as an IPv6 address. This practice was officially deprecated in 2006 by RFC 4291:

The "IPv4-Compatible IPv6 address" is now deprecated because the current IPv6 transition mechanisms no longer use these addresses. New or updated implementations are not required to support this address type.




回答3:


Try this out :

function _inet_ntop($ip) {

  if (strlen($ip) == 4) { // For IPv4
    list(, $ip) = unpack('N', $ip);
    $ip = long2ip($ip);
  }
  elseif(strlen($ip) == 16) { // For IPv6
    $ip = bin2hex($ip);
    $ip = substr(chunk_split($ip, 4, ':'), 0, -1);
    $ip = explode(':', $ip);
    $res = '';

    foreach($ip as $index => $seg) {
      while ($seg {0} == '0')
        $seg = substr($seg, 1);

      if ($seg != '') {
        $res .= $seg;
        if ($index < count($ip) - 1)
          $res .= $res == '' ? '' : ':';
      } else {
        if (strpos($res, '::') === false)
          $res .= ':';

      }
    }
    $ip = $res;
  }

  return $ip;
}

And you can call this function instead of inet_ntop :

$a = inet_pton('::FEEF:1886');
$b = _inet_ntop($a);
print "::FEEF:1886 -> $b\n";
// Output => ::FEEF:1886 -> ::feef:1886

$x = inet_pton('::F');
$y = _inet_ntop($x);
print "::F -> $y\n";
// Output => ::F -> ::f



回答4:


To summarize what I've head so far in the answers & comments:

  • IPv6 addresses have a canonical format which is what inet_ntop() returns.
  • The ::/96 address range is deprecated, but ::ffff/80 is not.
  • Although it would make sense for all ::/96 addresses to be rendered as ::/IPv4-address by inet_ntop() it appears that inet_ntop() renders ::/112 addresses and "higher" in ::/96 as ::/IPv4-dotted-quad (e.g. ::254.239.24.134) and renders ::/96 addresses "lower" than ::/112 as "normal" IPv6 addresses.
  • If you want inet_ntop() to render all IPv6 addresses the same way (i.e. 8 hex words with the usual zero compression rules) then you need to write your own method to achieve this.

My own workaround is to extend inet_ntop() by rewriting any IPv4 dotted quads as hexwords (and I exploded the logic into multiple methods to make it easier for me to keep track of what I was doing):

function _inet_ntop($addr) {
    return fix_ipv4_compatible_ipv6(inet_ntop($addr));
}

/**
 * If $str looks like ::/IPv4-dotted-quad then rewrite it as
 * a "pure" IPv6 address, otherwise return it unchanged.
 */
function fix_ipv4_compatible_ipv6($str) {
    if (
        ($str[0] == ':') &&
        ($str[1] == ':') &&
        preg_match('/^::(\S+\.\S+)$/', $str, $match)
    ) {
        $chunks = explode('.', $match[1]);
        return self::ipv4_zones_to_ipv6(
            $chunks[0],
            $chunks[1],
            $chunks[2],
            $chunks[3]
        );
    } else {
        return $str;
    }
}

/**
 * Return a "pure" IPv6 address printable string representation
 * of the ::/96 address indicated by the 4 8-bit "zones" of an
 * IPv4 address (e.g. (254, 239, 24, 134) -> ::feef:1886).
 */
function ipv4_zones_to_ipv6($q1, $q2, $q3, $q4) {
    if ($q1 == 0) {
        if ($q2 == 0) {
            if ($q3 == 0) {
                if ($q4 == 0) {
                    return '::0';
                } else {
                    return '::' . self::inflate_hexbit_pair($q4);
                }
            } else {
                return '::' . self::inflate_hex_word($q3, $q4);
            }
        } else {
            return '::' . self::inflate_hexbit_pair($q2) . ':' . self::inflate_hex_word($q3, $q4);
        }
    } else {
        return '::' . self::inflate_hex_word($q1, $q2) . ':' . self::inflate_hex_word($q3, $q4);
    }
}

/**
 * Convert two 8-bit IPv4 "zones" into a single 16-bit hexword,
 * stripping leading 0s as needed, e.g.:
 * (254, 239) -> feef
 * (0,1) -> 1
 */
function inflate_hex_word($hb1, $hb2) {
    $w = self::inflate_hexbit_pair($hb1) . self::inflate_hexbit_pair($hb2);
    return ltrim($w, '0');
}

/**
 * Convert one 8-bit IPv4 "zone" into two hexadecimal digits,
 * (hexits) padding with a leading zero if necessary, e.g.:
 * 254 -> fe
 * 2 -> 02
 */
function inflate_hexbit_pair($hb) {
    return str_pad(dechex($hb), 2, '0', STR_PAD_LEFT);
}

Although arguably much less elegant than the _inet_ntop() function proposed by JC Sama, it runs about 25% faster over my (essentially random) test cases.




回答5:


Answers provided by kasperd, diskwuff, and JC Sama offer both helpful information and workarounds which are likely to be useful to other SO readers, so I have upvoted them all. But they do not address my original question directly, so I'm adding this answer:

The behavior of the PHP function inet_pton() is correct. The problem is that inet_ntop() does not treat IPv6 address in ::/96 consistently. This is a bug in PHP.



来源:https://stackoverflow.com/questions/28905707/is-inet-pton-broken-for-some-ipv6-addresses-that-look-like-ipv4-addresses

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!