Damainman
Damainman

Reputation: 525

Calculate an IPv6 range from a CIDR prefix?

I am able to do this with IPv4 using code snippets from various online sources. I was wondering if there was a way to do it with IPv6.

Basically I just need a form that I can enter an IPv6 address and prefix (ex: f080:42d2:581a::0/68) and it calculates the network address, first useable address, last useable address, and broadcast address. Then just prints to screen. Not looking to store it in a database or anything yet.

Upvotes: 10

Views: 10745

Answers (5)

forthxu
forthxu

Reputation: 1

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

Upvotes: 0

Allen Ellis
Allen Ellis

Reputation: 318

This is a fix to the accepted answer, which incorrectly assumes the "first address" should be identical to the inputted string. Rather, it needs to have its value modified via an AND operator against its mask.

To demonstrate the problem, consider this example input: 2001:db8:abc:1403::/54

Expected result:

First: 2001:db8:abc:1400::

Actual result:

First: 2001:db8:abc:1403::

The relevant math to calculate the mask for a given 4-bit sequence is:

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

Full code

<?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";

Outputs:

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

Upvotes: 8

CodeAngry
CodeAngry

Reputation: 12985

Well, for posterity, I'm adding my code here. And also as a thanks to you guys who helped me nail this down as I needed it for an ipv6/ip2country script.

It's slightly inspired by code posted here by @mikemacintosh and @Sander Steffann, slightly improved (whishful thinking) and returns a nice object packing all the data you do/don't need:

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

I like functions and classes and dislike non-reusable code where reusable functionality is implemented.

PS: If you find issues with it, please get back to me. I'm far from an expert in IPv6.

Upvotes: 1

Mike Mackintosh
Mike Mackintosh

Reputation: 14237

For those who stumble upon this question, you can do this more effectively using the dtr_pton and dtr_ntop functions and dTRIP class found on GitHub.

We also have noticed a lack of focus and tools with IPv6 in PHP, and put together this article, http://www.highonphp.com/5-tips-for-working-with-ipv6-in-php, which may be of help to others.

Function Source

This converts and IP to a binary representation:

/**
 * dtr_pton
 *
 * Converts a printable IP into an unpacked binary string
 *
 * @author Mike Mackintosh - [email protected]
 * @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;
}

This converts a binary representation to printable IP:

/**
 * dtr_ntop
 *
 * Converts an unpacked binary string into a printable IP
 *
 * @author Mike Mackintosh - [email protected]
 * @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;
}

Examples

Using the dtr_pton function you can:

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

Get your Network and Broadcast:

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

And your output would be:

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

Upvotes: 3

Sander Steffann
Sander Steffann

Reputation: 9978

First of all: IPv6 doesn't have network and broadcast addresses. You can use all addresses in a prefix. Second: On a LAN the prefix length is always (well, 99.x% of the time) a /64. Routing a /68 would break IPv6 features like stateless auto configuration.

Below is a verbose implementation of an IPv6 prefix calculator:

<?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";

?>

It should output:

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

Upvotes: 10

Related Questions