我需要检查访问者的 IPv6 是否在给定的前缀中(我有一些列入白名单的 IPv6 前缀)。我使用 StackOverflow 中的两个不同函数调整了以下函数(一个是将 IPv6
我需要检查访问者 IPv6 是否在给定的前缀中(我有一些白名单 IPv6 前缀)。
我使用来自 StackOverflow 的两个不同函数调整了下面的函数(一个是 将 ipv6 前缀转换为第一个和最后一个地址 , 另一个答案是测试给定的 IP 是否在给定的第一个+最后一个 ip 范围内 ),但查看代码,有很多我不熟悉的函数,再加上 ipv6 让我感到困惑。
有人能告诉我这段代码是否正确吗?根据我的测试,它验证了我发送给它的 IP:
<?php
if(!function_exists('is_ipv6_in_prefix')){
function is_ipv6_in_prefix($ipv6_ip_to_check, $ipv6_prefix_to_check){
$ipv6_ip_to_check = inet_pton($ipv6_ip_to_check);
// Split in address and prefix length
list($firstaddrstr, $prefixlen) = explode('/', $ipv6_prefix_to_check);
// Parse the address into a binary string
$firstaddrbin = inet_pton($firstaddrstr);
// Convert the binary string to a string with hexadecimal characters
if(function_exists('bin2hex')){
$firstaddrhex = bin2hex($firstaddrbin);
} else {
// older php versions can use pack/unpack
$firstaddrhex = reset(unpack('H*', $firstaddrbin));
}
// Overwriting first address string to make sure notation is optimal
$firstaddrstr = inet_ntop($firstaddrbin);
// Calculate the number of 'flexible' bits
$flexbits = 128 - $prefixlen;
// Build the hexadecimal string of the last address
$lastaddrhex = $firstaddrhex;
// We start at the end of the string (which is always 32 characters long)
$pos = 31;
while ($flexbits > 0) {
// Get the character at this position
$orig = substr($lastaddrhex, $pos, 1);
// Convert it to an integer
$origval = hexdec($orig);
// OR it with (2^flexbits)-1, with flexbits limited to 4 at a time
$newval = $origval | (pow(2, min(4, $flexbits)) - 1);
// Convert it back to a hexadecimal character
$new = dechex($newval);
// And put that character back in the string
$lastaddrhex = substr_replace($lastaddrhex, $new, $pos, 1);
// We processed one nibble, move to previous position
$flexbits -= 4;
$pos -= 1;
}
// Convert the hexadecimal string to a binary string
if(function_exists('hex2bin')){
$lastaddrbin = hex2bin($lastaddrhex);
} else {
// older PHP versions can use pack/unpack
$lastaddrbin = pack('H*', $lastaddrhex);
}
// And create an IPv6 address from the binary string
$lastaddrstr = inet_ntop($lastaddrbin);
// print string values for user
// echo "\nPrefix: $ipv6_prefix_to_check";
// echo "\nFirst: $firstaddrstr";
// echo "\nLast: $lastaddrstr";
if ((strlen($ipv6_ip_to_check) == strlen($firstaddrbin)) && ($ipv6_ip_to_check >= $firstaddrbin && $ipv6_ip_to_check <= $lastaddrbin)) {
// In range
return true;
} else {
// Not in range
return false;
}
}
}
if(is_ipv6_in_prefix('2a03:2880:f800:4::', '2a03:2880:f800::/48')){
echo "\nTRUE";
} else {
echo "\nFALSE";
}
?>
我不确定,因为 另一个答案附带了更正 ,并说:
这是对已接受答案的修复,该答案错误地假设“第一个地址”应该与输入的字符串相同。相反,它需要通过 AND 运算符根据其掩码修改其值。
为了演示该问题,请考虑以下示例输入:
2001:db8:abc:1403::/54
预期结果:
First: 2001:db8:abc:1400:: Actual result:
First: 2001:db8:abc:1403::
[...] 修复的完整代码 [...]
当我尝试应用上面“更正”中的代码时,它无法匹配 2a03:2880:f800:4::
此前缀内的 IP:。也许 2a03:2880:f800::/48
我错误地应用了修复,但是,最后提到的这个更正是否适用于我上面的最终代码?
您的整个过程都是倒退的。 无需 计算前缀的第一个和最后一个地址,甚至无需计算“灵活位”,即可进行检查 - 您需要做的就是比较给定地址和前缀的初始 X 位(即 非 “灵活”位)。
(二进制到十六进制的转换通常也是没有意义的 - 如果您需要对每个半字节进行 hex2bin() 操作,那么您最好首先保留整个字符串二进制,因为无论您是在 4 位还是 8 位单元上工作,甚至在 32 位单元上工作都没有区别。)
记住诸如此类的老式网络掩码的 255.255.128.0
工作原理很有用 – 术语“掩码”意味着它将直接按位与地址进行“与”运算,然后比较结果;也就是说 (address & mask) == net
。这不会因 CIDR 前缀而改变;唯一改变的是写下相同网络掩码的语法。(实际上,一些系统在内部将 \'/48\' 预扩展为完整掩码,例如 \'ffff:ffff:ffff::\',以便更快地进行比较。)
例子:
function ip_cidr($host, $mask) {
list ($net, $len) = explode("/", $mask, 2);
$host = inet_pton($host);
$net = inet_pton($net);
if ($host === false || $net === false || (strlen($len) && !is_numeric($len)))
throw new \InvalidArgumentException();
elseif (strlen($host) !== strlen($net))
return false; /* mismatching address families not an error */
$nbits = strlen($host) * 8;
$len = strlen($len) ? intval($len) : $nbits;
if ($len < 0 || $len > $nbits)
throw new \InvalidArgumentException();
$host = unpack("C*", $host);
$net = unpack("C*", $net);
/* loop over each byte and compare them */
for ($i = 1; $i <= count($net) && $len > 0; $i++) {
$bits = min($len, 8);
$len -= 8;
/* if less than 8 bits left to check, generate an 'AND' mask for them */
$bmask = (0xFF00 >> $bits) & 0xFF;
//printf("host[%d] = %02x, net[%d] = %02x, bits = %d, bmask = %02x\n",
// $i, $host[$i], $i, $net[$i], $bits, $bmask);
if (($host[$i] & $bmask) !== ($net[$i] & $bmask))
return false;
}
return true;
}
如您所见,大多数功能只是预检查,以按位与比较作为实际检查( if (($host[$i] ^ $net[$i]) & $bmask)
如果您希望少一个操作,可以进行此操作)。
相同的功能适用于 IPv4 和 IPv6。
如果出于其他目的需要,可以使用与(后者是按位非运算)来 net & mask
计算 net | ~mask
前缀的第一个或最后一个地址
这 几乎 与您找到的原始“范围计算器”中的过程相同,只是向前循环而不是向后循环(在我看来,最初的“无用”迭代非常值得降低复杂性),以及使用按位运算而不是 2 位-1来计算 AND/OR 掩码:
function ip_range($mask) {
@list ($net, $len) = explode("/", $mask, 2);
$net = inet_pton($net);
if ($net === false || !is_numeric("0$len"))
throw new \InvalidArgumentException();
$nbits = strlen($net) * 8;
$len = strlen($len) ? intval($len) : $nbits;
if ($len < 0 || $len > $nbits)
throw new \InvalidArgumentException();
$net = unpack("C*", $net);
$first = [];
$last = [];
for ($i = 1; $i <= count($net); $i++) {
$bits = min($len, 8);
$len = max($len - 8, 0);
$bmask = (0xFF00 >> $bits) & 0xFF;
$first[$i] = $net[$i] & $bmask;
$last[$i] = $net[$i] | ~$bmask & 0xFF;
}
$first = inet_ntop(pack("C*", ...$first));
$last = inet_ntop(pack("C*", ...$last));
return [$first, $last];
}
(我想让它 $len -= $bits
在两个函数中同样有效,但是由于某种原因,我睡眠不足的大脑拒绝相信它。我确信 它 是正确的,但 max() 似乎仍然更具可读性。)
当我尝试应用上述“修正”中的代码时,它无法匹配此前缀 2a03:2880:f800:4:: 内的 IP:2a03:2880:f800::/48。也许我错误地应用了修复,但是,最后提到的修正是否适用于我上面的最终代码?
它适用于前缀设置了“主机”位的输入,例如 2a03:2880:f800::/32
– 这是表达网络接口配置(主机地址 + 网络掩码)的有效方式,但严格来说作为前缀并不有效。(此示例中的前缀为 2a03:2880::/32。)
因此,如果您可以确保您的“受信任前缀”列表已经是规范形式(没有设置任何“主机”位),那么该函数 可以 假定前缀等于该前缀的第一个地址,并且实际上不需要应用更正。
但是,无论哪种方式,该函数都应该返回相同的结果(假设输入规范),因此对于你的情况,我认为你一定是错误地应用了修正。你没有展示更新后的代码,但 Allen Ellis 的原始帖子似乎是正确的——基本上整个“最后地址”的计算都必须重复并反转。
(在我的示例 ip_range() 函数中, $first[i] = $net[i] & $bmask
执行的是相同的 AND 运算。)
如果您想尝试调试代码,我 至少 会将其分成一个单独的“计算第一个-最后一个”函数,以便您可以单独测试它(确保它始终计算正确的第一个地址等),并且只有在您使其正常工作后,才用单独的“检查是否在 ab 范围内”函数包裹它。
但我不会花任何时间来尝试改进当前代码。正如开头提到的,从 的角度实现前缀检查 first <= addr <= last
确实有点落后,还有一种更直接的方法可以做到这一点,这种方法本质上不会对非规范前缀产生任何问题。