从CIDR前缀计算IPv6范围?

9
我可以使用来自各种在线资源的代码片段在IPv4上完成此操作。我想知道是否有一种方法可以在IPv6上完成此操作。 基本上,我只需要一个表单,可以输入IPv6地址和前缀(例如:f080:42d2:581a::0/68),然后它会计算网络地址、第一个可用地址、最后一个可用地址和广播地址。然后只需打印到屏幕上。暂时不需要将其存储在数据库或其他任何地方。
5个回答

9
首先,IPv6没有网络和广播地址。您可以在前缀中使用所有地址。其次,在局域网上,前缀长度始终为/64(99.x%的情况下)。路由/68会破坏IPv6功能,如无状态自动配置。
以下是IPv6前缀计算器的详细实现:
<?php

/*
 * This is definitely not the fastest way to do it!
 */

// An example prefix
$prefix = '2001:db8:abc:1400::/54';

// Split in address and prefix length
list($firstaddrstr, $prefixlen) = explode('/', $prefix);

// Parse the address into a binary string
$firstaddrbin = inet_pton($firstaddrstr);

// Convert the binary string to a string with hexadecimal characters
# unpack() can be replaced with bin2hex()
# unpack() is used for symmetry with pack() below
$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
# Using pack() here
# Newer PHP version can use hex2bin()
$lastaddrbin = pack('H*', $lastaddrhex);

// And create an IPv6 address from the binary string
$lastaddrstr = inet_ntop($lastaddrbin);

// Report to user
echo "Prefix: $prefix\n";
echo "First: $firstaddrstr\n";
echo "Last: $lastaddrstr\n";

?>

它应该输出:
Prefix: 2001:db8:abc:1400::/54
First: 2001:db8:abc:1400::
Last: 2001:db8:abc:17ff:ffff:ffff:ffff:ffff

1
实际上,你的代码正好符合我所需的方式 :). 我需要的更改是由于我缺乏知识造成的 :). 再次感谢! - Damainman
请注意,块中的第一个地址(即带有掩码/64的2001:db8:abc:1400::)是一种任播地址,所有网络上的路由器都会自动回答。不要将此地址分配给任何主机。所有其他地址都可用。 - Jeremy Visser
1
关于这个问题,我看到你的回答对PhpMyAdmin代码做出了贡献 ;) https://github.com/phpmyadmin/phpmyadmin/blob/cf2de639f7f9e3cfca6c4a7262f717353afde130/libraries/ip_allow_deny.lib.php#L158 - baptx
1
就记录而言,IPv6地址中确实有网络地址——它的前3组(或48位)表示。 - durrrr
我只是想指出这可能不是最好的表达方式。IPv6没有网络地址,因为有3个块保留给网络(网络地址),一个用于子网(子网地址)和最后4个用于用户设备。对我来说,这更有意义 :) - durrrr
显示剩余7条评论

7

这是对得到认可的答案的修复,该答案错误地假设“第一个地址”应与输入的字符串相同。相反,需要通过使用其掩码的AND运算符进行修改来修改其值。

为了演示问题,考虑以下示例输入:2001:db8:abc:1403::/54

期望结果:

First: 2001:db8:abc:1400::

实际结果:

First: 2001:db8:abc:1403::

计算给定4位序列的掩码所需的相关数学如下:

// Calculate the subnet mask. min() prevents the comparison from being negative
$mask = 0xf << (min(4, $flexbits));

// AND the original against its mask
$newval = $origval & $mask;

完整代码

<?php

/*
 * This is definitely not the fastest way to do it!
 */

// An example prefix
$prefix = '2001:db8:abc:1403::/54';

// Split in address and prefix length
list($addr_given_str, $prefixlen) = explode('/', $prefix);

// Parse the address into a binary string
$addr_given_bin = inet_pton($addr_given_str);

// Convert the binary string to a string with hexadecimal characters
$addr_given_hex = bin2hex($addr_given_bin);

// Overwriting first address string to make sure notation is optimal
$addr_given_str = inet_ntop($addr_given_bin);

// Calculate the number of 'flexible' bits
$flexbits = 128 - $prefixlen;

// Build the hexadecimal strings of the first and last addresses
$addr_hex_first = $addr_given_hex;
$addr_hex_last = $addr_given_hex;

// We start at the end of the string (which is always 32 characters long)
$pos = 31;
while ($flexbits > 0) {
    // Get the characters at this position
    $orig_first = substr($addr_hex_first, $pos, 1);
    $orig_last = substr($addr_hex_last, $pos, 1);

    // Convert them to an integer
    $origval_first = hexdec($orig_first);
    $origval_last = hexdec($orig_last);

    // First address: calculate the subnet mask. min() prevents the comparison from being negative
    $mask = 0xf << (min(4, $flexbits));

    // AND the original against its mask
    $new_val_first = $origval_first & $mask;

    // Last address: OR it with (2^flexbits)-1, with flexbits limited to 4 at a time
    $new_val_last = $origval_last | (pow(2, min(4, $flexbits)) - 1);

    // Convert them back to hexadecimal characters
    $new_first = dechex($new_val_first);
    $new_last = dechex($new_val_last);

    // And put those character back in their strings
    $addr_hex_first = substr_replace($addr_hex_first, $new_first, $pos, 1);
    $addr_hex_last = substr_replace($addr_hex_last, $new_last, $pos, 1);

    // We processed one nibble, move to previous position
    $flexbits -= 4;
    $pos -= 1;
}

// Convert the hexadecimal strings to a binary string
$addr_bin_first = hex2bin($addr_hex_first);
$addr_bin_last = hex2bin($addr_hex_last);

// And create an IPv6 address from the binary string
$addr_str_first = inet_ntop($addr_bin_first);
$addr_str_last = inet_ntop($addr_bin_last);

// Report to user
echo "Prefix: $prefix\n";
echo "First: $addr_str_first\n";
echo "Last: $addr_str_last\n";

输出:

Prefix: 2001:db8:abc:1403::/54
First: 2001:db8:abc:1400::
Last: 2001:db8:abc:17ff:ffff:ffff:ffff:ffff


3
对于那些遇到这个问题的人,你可以更有效地使用dtr_ptondtr_ntop函数以及GitHub上找到的dTRIP类来完成。
我们还注意到PHP中IPv6方面缺乏关注和工具,因此我们编写了这篇文章http://www.highonphp.com/5-tips-for-working-with-ipv6-in-php,希望能帮助其他人。 函数源代码 这将IP地址转换为二进制表示形式:
/**
 * dtr_pton
 *
 * Converts a printable IP into an unpacked binary string
 *
 * @author Mike Mackintosh - mike@bakeryphp.com
 * @param string $ip
 * @return string $bin
 */
function dtr_pton( $ip ){

    if(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){
        return current( unpack( "A4", inet_pton( $ip ) ) );
    }
    elseif(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){
        return current( unpack( "A16", inet_pton( $ip ) ) );
    }

    throw new \Exception("Please supply a valid IPv4 or IPv6 address");

    return false;
}

这将二进制表示转换为可打印的IP地址:
/**
 * dtr_ntop
 *
 * Converts an unpacked binary string into a printable IP
 *
 * @author Mike Mackintosh - mike@bakeryphp.com
 * @param string $str
 * @return string $ip
 */
function dtr_ntop( $str ){
    if( strlen( $str ) == 16 OR strlen( $str ) == 4 ){
        return inet_ntop( pack( "A".strlen( $str ) , $str ) );
    }

    throw new \Exception( "Please provide a 4 or 16 byte string" );

    return false;
}

例子

使用 dtr_pton 函数,您可以:

$ip = dtr_pton("fe80:1:2:3:a:bad:1dea:dad");
$mask = dtr_pton("ffff:ffff:ffff:ffff:ffff:fff0::");

获取您的网络和广播:

var_dump( dtr_ntop( $ip & $mask ) );
var_dump( dtr_ntop( $ip | ~ $mask ) );

你的输出将是:

string(18) "fe80:1:2:3:a:ba0::"
string(26) "fe80:1:2:3:a:baf:ffff:ffff"

2
IPv6不支持广播,因此没有广播地址的概念。 - Steve-o
@Steve-o 没错,但是由于广播术语在网络领域已经使用了很长时间,因此在强调IPv6范围时,人们更容易理解。这类似于子网段,但许多网络远远超出了这个段。 - Mike Mackintosh
博客文章的链接已经失效了。请问您能否更新或者删除它? - Michael Cordingley

1

为了记录,我在这里添加我的代码。同时也要感谢帮助我完成这个ipv6/ip2country脚本的人们。

这段代码受到@mikemacintosh和@Sander Steffann发布的代码的一些启发,并稍作改进(仅有愿望)。它会返回一个包含您需要或不需要的所有数据的对象:

/**
* This:
* <code>
* Ipv6_Prefix2Range('2001:43f8:10::/48');
* </code>
* returns this:
* <code>
* object(stdClass)#2 (4) {
*   ["Prefix"]=>
*   string(17) "2001:43f8:10::/48"
*   ["FirstHex"]=>
*   string(32) "200143f8001000000000000000000000"
*   ["LastHex"]=>
*   string(32) "200143f80010ffffffffffffffffffff"
*   ["MaskHex"]=>
*   string(32) "ffffffffffff00000000000000000000"
*   // Optional bin equivalents available
* }
* </code>
* 
* Tested against:
* @link https://www.ultratools.com/tools/ipv6CIDRToRange
* 
* @param string $a_Prefix
* @param bool $a_WantBins
* @return object
*/
function Ipv6_Prefix2Range($a_Prefix, $a_WantBins = false){
    // Validate input superficially with a RegExp and split accordingly
    if(!preg_match('~^([0-9a-f:]+)[[:punct:]]([0-9]+)$~i', trim($a_Prefix), $v_Slices)){
        return false;
    }
    // Make sure we have a valid ipv6 address
    if(!filter_var($v_FirstAddress = $v_Slices[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){
        return false;
    }
    // The /## end of the range
    $v_PrefixLength = intval($v_Slices[2]);
    if($v_PrefixLength > 128){
        return false; // kind'a stupid :)
    }
    $v_SuffixLength = 128 - $v_PrefixLength;

    // Convert the binary string to a hexadecimal string
    $v_FirstAddressBin = inet_pton($v_FirstAddress);
    $v_FirstAddressHex = bin2hex($v_FirstAddressBin);

    // Build the hexadecimal string of the network mask
    // (if the manually formed binary is too large, base_convert() chokes on it... so we split it up)
    $v_NetworkMaskHex = str_repeat('1', $v_PrefixLength) . str_repeat('0', $v_SuffixLength);
    $v_NetworkMaskHex_parts = str_split($v_NetworkMaskHex, 8);
    foreach($v_NetworkMaskHex_parts as &$v_NetworkMaskHex_part){
        $v_NetworkMaskHex_part = base_convert($v_NetworkMaskHex_part, 2, 16);
        $v_NetworkMaskHex_part = str_pad($v_NetworkMaskHex_part, 2, '0', STR_PAD_LEFT);
    }
    $v_NetworkMaskHex = implode(null, $v_NetworkMaskHex_parts);
    unset($v_NetworkMaskHex_part, $v_NetworkMaskHex_parts);
    $v_NetworkMaskBin = inet_pton(implode(':', str_split($v_NetworkMaskHex, 4)));

    // We have the network mask so we also apply it to First Address
    $v_FirstAddressBin &= $v_NetworkMaskBin;
    $v_FirstAddressHex = bin2hex($v_FirstAddressBin);

    // Convert the last address in hexadecimal
    $v_LastAddressBin = $v_FirstAddressBin | ~$v_NetworkMaskBin;
    $v_LastAddressHex =  bin2hex($v_LastAddressBin);

    // Return a neat object with information
    $v_Return = array(
        'Prefix'    => "{$v_FirstAddress}/{$v_PrefixLength}",
        'FirstHex'  => $v_FirstAddressHex,
        'LastHex'   => $v_LastAddressHex,
        'MaskHex'   => $v_NetworkMaskHex,
    );
    // Bins are optional...
    if($a_WantBins){
        $v_Return = array_merge($v_Return, array(
            'FirstBin'  => $v_FirstAddressBin,
            'LastBin'   => $v_LastAddressBin,
            'MaskBin'   => $v_NetworkMaskBin,
        ));
    }
    return (object)$v_Return;
}

我喜欢函数和类,不喜欢实现无法重复使用的代码。 PS:如果您发现问题,请与我联系。我对IPv6并不是很精通。

这是完美的一个,与下面的URL转换相匹配 http://www.gestioip.net/cgi-bin/subnet_calculator.cgi - MANOJ

0
    public static function rangeToCIDRv6($startIP, $endIP) {
        //转换成bin字符串格式
        $startBinary = inet_pton($startIP);
        $startBin = '';
        $bits = 15;
        while($bits >= 0) {
            $bin = sprintf("%08b",(ord($startBinary[$bits])));
            $startBin = $bin.$startBin;
            $bits--;
        }

        $endBinary = inet_pton($endIP);
        $endBin = '';
        $bits = 15;
        while($bits >= 0) {
            $bin = sprintf("%08b",(ord($endBinary[$bits])));
            $endBin = $bin.$endBin;
            $bits--;
        }

        //按位查询
        $cidrArray = array();
        $mask = 127;
        $diffFirst = false;
        while($mask>0){
            //位数不同
            if($startBin[$mask]!=$endBin[$mask]){
                while($startBin[$mask]!=$endBin[$mask]){
                    if($diffFirst){
                        $ipBin = str_pad(substr($startBin, 0, $mask).'1', 128, '0', STR_PAD_RIGHT);
                        $ip = '';
                        $Offset = 0;
                        while ($Offset <= 7) {
                            $bin_part = substr($ipBin, ($Offset*16), 16);
                            $ip .= dechex(bindec($bin_part));
                            if($Offset !=7){
                                $ip .= ":";
                            }
                            $Offset++;
                        }
                        $cidrArray[] = inet_ntop(inet_pton($ip)).'/'.($mask+1);
                    }
                    $mask--;
                }
                //首次不同
                if($diffFirst==false){
                    $diffFirst = true;

                    $ipBin = str_pad(substr($startBin, 0, $mask).'1', 128, '0', STR_PAD_RIGHT);
                    $ip = '';
                    $Offset = 0;
                    while ($Offset <= 7) {
                        $bin_part = substr($ipBin, ($Offset*16), 16);
                        $ip .= dechex(bindec($bin_part));
                        if($Offset !=7){
                            $ip .= ":";
                        }
                        $Offset++;
                    }
                    $cidrArray[] = inet_ntop(inet_pton($ip)).'/'.($mask+1);
                }
            }else{
                $mask--;
            }
        }
        return $cidrArray;
    }

你的回答可以通过附加支持性信息来改进。请编辑以添加更多细节,例如引用或文档,以便他人可以确认你的回答是否正确。您可以在帮助中心上找到有关如何撰写良好回答的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接