对于 "look like" IPv4 地址的某些 IPv6 地址,inet_pton() 是否损坏?
Is inet_pton() broken for some IPv6 addresses that "look like" IPv4 addresses?
我使用的是 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
我本以为第二个代码片段会产生这个输出:
::FEEF:1886 -> ::feef:1886
IPv6 地址::FEEF:1886 是什么让 PHP 认为它真的是一个 IPv4 地址? inet_ntop/inet_pton 转换与在 "high" 96 位(例如 ::F)中具有 0 的其他地址一起正常工作。
编辑: 我的第一个想法是这可能是我的 PHP 版本中的错误,但是使用 this online PHP sandbox 我在 5.6.2 之前的 PHP 版本中看到相同的行为。所以这要么是故意的(在这种情况下我非常想知道这种行为的原因)要么是现代版本 PHP.
中持续存在的错误
附录: 我在 2015 年 3 月 12 日打开了 PHP Bug 69232,因为 [=39 的行为明显不一致=]() 用于 ::/96.
中的地址
您看到的是 IPv4 地址被(错误地)表示为 IPv6 地址。 RFC 4291:
于 2006 年正式弃用了这种做法
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.
试试这个:
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
IPv6 地址的文本表示允许每个 IPv6 地址有多个不同的有效表示。
这意味着当通过 inet_pton
.
传递时,所有这些有效的 IPv6 地址文本表示都映射到相同的二进制 128 位字符串
然而,当使用 inet_ntop
将二进制 128 位字符串转换为文本表示时,它显然只能输出表示该 IP 地址的众多有效字符串之一。它选择的一种称为规范表示。
使用 IPv4 表示法写入 IPv6 地址的最后 32 位始终有效。然而,只有少数 类 个 IPv6 地址使用该格式作为它们的规范表示。
::/96
已被弃用,但这仅意味着不应再使用这些地址,它不会影响 inet_pton
和 inet_ntop
对它们的处理方式。
::ffff:0.0.0.0/96
是另一个前缀,在其规范表示中使用 IPv4 表示法。该前缀用于套接字中的 IPv4 兼容性 API,但它们永远不会在线上发送,因为它们适用于在线流量将是 IPv4 的情况。
总结一下我目前为止在回答和评论中的想法:
- IPv6 地址具有规范格式,即 inet_ntop() returns.
- 不推荐使用 ::/96 地址范围,但不推荐使用 ::ffff/80。
- 虽然 all ::/96 地址被 inet_ntop() 呈现为 ::/IPv4-address 似乎 inet_ntop() 将 ::/96 中的 ::/112 地址和 "higher" 呈现为 ::/IPv4-dotted-quad(例如 ::254.239.24.134)并呈现 ::/96 地址 "lower" 比 ::/112 作为 "normal" IPv6 地址。
- 如果您希望 inet_ntop() 以相同的方式呈现所有 IPv6 地址(即具有通常的零压缩规则的 8 个十六进制字),那么您需要编写自己的方法来实现此目的。
我自己的解决方法是通过将任何 IPv4 点分四边形重写为十六进制字来扩展 inet_ntop()(我将逻辑分解为多种方法,以便更容易跟踪我在做什么) :
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% .
kasperd、diskwuff 和 JC Sama 提供的答案既提供了有用的信息,也提供了可能对其他 SO 读者有用的变通方法,因此我对它们都投了赞成票。但是他们没有直接解决我原来的问题,所以我添加了这个答案:
PHP 函数 inet_pton() 的行为是正确的。问题是 inet_ntop() 不一致地处理 ::/96 中的 IPv6 地址。 This is a bug in PHP.
我使用的是 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
我本以为第二个代码片段会产生这个输出:
::FEEF:1886 -> ::feef:1886
IPv6 地址::FEEF:1886 是什么让 PHP 认为它真的是一个 IPv4 地址? inet_ntop/inet_pton 转换与在 "high" 96 位(例如 ::F)中具有 0 的其他地址一起正常工作。
编辑: 我的第一个想法是这可能是我的 PHP 版本中的错误,但是使用 this online PHP sandbox 我在 5.6.2 之前的 PHP 版本中看到相同的行为。所以这要么是故意的(在这种情况下我非常想知道这种行为的原因)要么是现代版本 PHP.
中持续存在的错误附录: 我在 2015 年 3 月 12 日打开了 PHP Bug 69232,因为 [=39 的行为明显不一致=]() 用于 ::/96.
中的地址您看到的是 IPv4 地址被(错误地)表示为 IPv6 地址。 RFC 4291:
于 2006 年正式弃用了这种做法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.
试试这个:
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
IPv6 地址的文本表示允许每个 IPv6 地址有多个不同的有效表示。
这意味着当通过 inet_pton
.
然而,当使用 inet_ntop
将二进制 128 位字符串转换为文本表示时,它显然只能输出表示该 IP 地址的众多有效字符串之一。它选择的一种称为规范表示。
使用 IPv4 表示法写入 IPv6 地址的最后 32 位始终有效。然而,只有少数 类 个 IPv6 地址使用该格式作为它们的规范表示。
::/96
已被弃用,但这仅意味着不应再使用这些地址,它不会影响 inet_pton
和 inet_ntop
对它们的处理方式。
::ffff:0.0.0.0/96
是另一个前缀,在其规范表示中使用 IPv4 表示法。该前缀用于套接字中的 IPv4 兼容性 API,但它们永远不会在线上发送,因为它们适用于在线流量将是 IPv4 的情况。
总结一下我目前为止在回答和评论中的想法:
- IPv6 地址具有规范格式,即 inet_ntop() returns.
- 不推荐使用 ::/96 地址范围,但不推荐使用 ::ffff/80。
- 虽然 all ::/96 地址被 inet_ntop() 呈现为 ::/IPv4-address 似乎 inet_ntop() 将 ::/96 中的 ::/112 地址和 "higher" 呈现为 ::/IPv4-dotted-quad(例如 ::254.239.24.134)并呈现 ::/96 地址 "lower" 比 ::/112 作为 "normal" IPv6 地址。
- 如果您希望 inet_ntop() 以相同的方式呈现所有 IPv6 地址(即具有通常的零压缩规则的 8 个十六进制字),那么您需要编写自己的方法来实现此目的。
我自己的解决方法是通过将任何 IPv4 点分四边形重写为十六进制字来扩展 inet_ntop()(我将逻辑分解为多种方法,以便更容易跟踪我在做什么) :
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% .
kasperd、diskwuff 和 JC Sama 提供的答案既提供了有用的信息,也提供了可能对其他 SO 读者有用的变通方法,因此我对它们都投了赞成票。但是他们没有直接解决我原来的问题,所以我添加了这个答案:
PHP 函数 inet_pton() 的行为是正确的。问题是 inet_ntop() 不一致地处理 ::/96 中的 IPv6 地址。 This is a bug in PHP.