【问题标题】:Is inet_pton() broken for some IPv6 addresses that "look like" IPv4 addresses?对于某些“看起来像” IPv4 地址的 IPv6 地址,inet_pton() 是否损坏?
【发布时间】:2015-03-06 18:54:27
【问题描述】:

我使用的是 PHP 5.2.17 版,我看到以下内容按预期工作:

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

Output: ::F -> ::f

但以下不是:

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

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

我本来希望第二个代码 sn-p 产生这个输出:

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

是什么关于 IPv6 地址 ::FEEF:1886 让 PHP 认为它真的是一个 IPv4 地址? inet_ntop/inet_pton 转换与“高” 96 位中具有 0 的其他地址(例如 ::F)一起正常工作。

编辑: 我的第一个想法是这可能是我的 PHP 版本中的一个错误,但使用 this online PHP sandbox 我看到 PHP 版本的相同行为5.6.2.所以要么这是故意的(在这种情况下,我非常想知道这种行为的原因),要么是现代版本的 PHP 中存在的错误。

附录:我于 2015 年 3 月 12 日打开 PHP Bug 69232,原因是 inet_ntop() 对 ::/96 中地址的行为明显不一致。

【问题讨论】:

    标签: php ipv6


    【解决方案1】:

    IPv6 地址的文本表示允许每个 IPv6 地址有多种不同的有效表示。

    这意味着当通过inet_pton 传递时,IPv6 地址的所有这些有效文本表示都映射到相同的二进制 128 位字符串。

    但是,当使用inet_ntop 将二进制 128 位字符串转换为文本表示时,它显然只能输出代表该 IP 地址的众多有效字符串之一。它选择的那个称为规范表示。

    使用 IPv4 表示法写入 IPv6 地址的最后 32 位始终有效。然而,只有少数几类 IPv6 地址使用该格式作为其规范表示。

    ::/96 已被弃用,但这只是意味着这些地址不应再使用,它​​不会影响inet_ptoninet_ntop 处理它们的方式。

    ::ffff:0.0.0.0/96 是另一个前缀,在其规范表示中使用 IPv4 表示法。该前缀用于套接字 API 中的 IPv4 兼容性,但这些前缀永远不会在线发送,因为它们用于在线流量为 IPv4 的情况。

    【讨论】:

    • 这一切都很好(尽管RFC 4291 表示 IPv4 映射的 IPv6 地址在 ::FFFF/96 范围内)。在这一点上让我感到困惑的是为什么 PHP inet_pton() 不会以相同的方式处理所有 /96 地址。 +1 用于调用规范表示(尽管它再次让我感到困惑,为什么两个不同的 /96 地址会有如此不同的规范表示)。
    • @Peter 这两个 /96 在其规范表示中使用 IPv4 表示法,因为它们用于 IPv4 向后兼容性,在这些情况下,IPv6 地址的最后一部分始终是 IPv4 地址。有两个这样的 /96 而不仅仅是一个的原因是,它需要两次尝试来决定 IPv4 兼容性应该是什么样子,因此两者中的第一个已被弃用。
    • RFC 4291 表明 ::/96 是“与 IPv4 兼容的 IPv6 地址”,实际上已被弃用,而 ::ffff/96 是“IPv4 映射的 IPv6 地址”,但并非如此。我的意思很简单,PHP inet_ntop() 函数对 ::/96 中的地址的行为不一致 - 我应该得到 ::f 和 ::feef:1886,或者 ::0.0.0.15 和 :: 254.239.24.134.
    • @Peter 你是对的,这确实看起来不一致。但与 IPv4 兼容的 IPv6 地址并未涵盖所有 ::/96。 :: 和 ::1 有其他用途。我还没有找到任何标准文档,具体说明 ::/96 中有多少不是与 IPv4 兼容的 IPv6 地址。但是,由于 0.0.0.0/8 是保留的,这意味着 ::/104 不能真正用作与 IPv4 兼容的 IPv6 地址。所以人们会期望这条线在 ::/104 或 ::/127 处绘制。由于某种原因,这条线似乎是在 ::/112 处绘制的,这看起来有点奇怪,但不应该真正破坏任何东西。
    【解决方案2】:

    您正在查看的是一个 IPv4 地址被(错误地)表示为 IPv6 地址。这种做法在 2006 年被 RFC 4291 正式弃用:

    “与 IPv4 兼容的 IPv6 地址”现已弃用,因为 当前的 IPv6 转换机制不再使用这些地址。 不需要新的或更新的实现来支持这一点 地址类型。

    【讨论】:

    • 感谢 RFC 链接,这绝对有助于从正确的历史角度看待问题。但是,如果 PHP inet_ntop() 函数始终如一地处理这种“已弃用”的情况,那么 all “top” 96 位 = 0 的 IPv4 地址将作为带有双冒号前缀的 IPv4 地址返回,对?然而,当 ::F 通过 inet_pton() 转换为压缩字符串,然后通过 inet_ntop() 再次转换回可打印字符串时,变为 ::f,而不是 ::0.0.0.15。这让我担心这些函数的 PHP 实现是错误的。
    • PHP 只使用底层的 C 库。 ::f::0.0.0.15 都是同一 IPv6 地址的有效表示。以 IPv4 表示法显示最后 32 位是一项允许但不是强制性的功能。见tools.ietf.org/html/rfc5952#section-5。我同意为了保持一致性,它应该显示 IPv4 符号,即使嵌入的 IPv4 地址无效/保留/等。
    • @Peter 请记住,0.0.0.15 并不是真正有效的 IPv4 地址; 0.0.0.0/8 被保留。所以我真的不能责怪inet_pton() 不知道如何处理它。
    • @SanderSteffann,感谢另一个很好的链接。很高兴知道,PHP inet_ntop() 至少没有使用相同的规则来准备 /96 中所有地址的可打印版本,这让我感到惊讶的不止我一个人。
    • @duskwuff 我知道 0/8 是为广播保留的,这意味着我不能将 IPv4 地址 0.0.0.15 分配给例如主机。但它一个完全有效的地址,不是吗?
    【解决方案3】:

    试试这个:

    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;
    }
    

    你可以调用这个函数而不是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
    

    【讨论】:

    • 不错!我自己的解决方法是为 inet_ntop() 编写一个“包装器”函数,当字符串表示以 IPv4 地址字符串结尾并将其转换回几个十六进制字(或一个,或没有,取决于是否存在)时“通知”前导零被前导双冒号吸收)。 +1 用于 unpack() 和 bin2hex()。不过,我仍然很好奇为什么 PHP inet_ntop() 函数不会以相同的方式处理所有 /96 地址。
    • 我注意到将 _inet_ntop() 函数应用于打包字符串 inet_pton('02a1:89ea:58bf:f4b2:0fd2:5631:0000:0000') 会得到 2a1:89ea:58bf:f4b2: fd2:5631: 而不是 2a1:89ea:58bf:f4b2:fd2:5631:: (即终端双冒号呈现为单冒号)。
    • 是的,您的新代码适用于我所有的测试用例,但运行速度比我自己的解决方案慢一些——我想我应该在这里发布。
    【解决方案4】:

    总结一下到目前为止我在答案和 cmets 中的内容:

    • IPv6 地址具有规范格式,这是 inet_ntop() 返回的内容。
    • 不推荐使用 ::/96 地址范围,但不推荐使用 ::ffff/80。
    • 尽管 inet_ntop() 将 all ::/96 地址呈现为 ::/IPv4 地址是有意义的,但似乎 inet_ntop() 呈现 ::/112 地址和“ ::/96 中的“更高”为 ::/IPv4-dotted-quad(例如 ::254.239.24.134),并将 ::/96 地址“低于” ::/112 呈现为“普通”IPv6 地址。
    • 如果您希望 inet_ntop() 以相同的方式呈现所有 IPv6 地址(即使用通常的零压缩规则的 8 个十六进制字),那么您需要编写自己的方法来实现这一点。

    我自己的解决方法是扩展 inet_ntop(),将任何 IPv4 点四边形重写为十六进制字(我将逻辑分解为多种方法,以便我更容易跟踪我在做什么):

    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);
    }
    

    虽然可以说不如 JC Sama 提出的 _inet_ntop() 函数优雅,但它的运行速度比我的(基本上是随机的)测试用例快 25%。

    【讨论】:

      【解决方案5】:

      由 kasperd、diskwuff 和 JC Sama 提供的答案提供了可能对其他 SO 读者有用的有用信息和解决方法,因此我对它们都投了赞成票。但他们没有直接解决我原来的问题,所以我添加了这个答案:

      PHP 函数 inet_pton() 的行为是正确的。问题是 inet_ntop() 不会一致地处理 ::/96 中的 IPv6 地址。 This is a bug in PHP.

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-09-18
        • 2012-12-25
        • 2021-07-30
        • 1970-01-01
        • 1970-01-01
        • 2011-02-16
        • 2015-01-17
        • 2020-03-25
        相关资源
        最近更新 更多