9

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.

miken32
  • 42,008
  • 16
  • 111
  • 154
Damainman
  • 515
  • 9
  • 21

5 Answers5

9

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
Sander Steffann
  • 9,509
  • 35
  • 40
  • Exactly What I needed!!. I have been trying to wrap my head around this for days and I am currently in a cisco course but we haven't reached ipv6 yet so trying to grasp the concept on my own. I have been looking at the code, how much of a code change would need to be done if I also wanted the 2nd IP and the IP just before the last one? I have been trying to figure it out but out of my scope of php knowledge :( – Damainman Apr 11 '12 at 08:15
  • Well, the second address has a 1 instead of 0 as the last character, and the second to last has an e instead of an f. – Sander Steffann Apr 12 '12 at 18:32
  • Sorry it has been a very busy few days and I was trying to get a an answer for the second IP and second to last lol... Still learning ipv6 but definitely learned a lot from your comments. Thank you again! and sorry for the delay in my replies. – Damainman Apr 14 '12 at 21:15
  • You're welcome! Let me know if I can help! IPv6 stuff is my full-time occupation these days :-) – Sander Steffann Apr 16 '12 at 23:20
  • 1
    Actually your code works exactly the way I needed :). The changes I was looking for was due to my lack of knowledge :). Thanks again! – Damainman Apr 22 '12 at 03:01
  • Please note that the first address in the block (i.e. 2001:db8:abc:1400:: with mask /64) is an anycast address that is automatically answered by all routers on the network. Do not assign this address to any host. All other addresses are usable. – Jeremy Visser Aug 19 '13 at 09:29
  • 1
    For information, I saw that your answer has contributed to PhpMyAdmin code ;) https://github.com/phpmyadmin/phpmyadmin/blob/cf2de639f7f9e3cfca6c4a7262f717353afde130/libraries/ip_allow_deny.lib.php#L158 – baptx Sep 24 '14 at 12:42
  • 1
    For the record, IPv6 address DOES have network address in it - the first 3 groups (or 48 bits) of it. – durrrr Jul 28 '15 at 12:58
  • @durrrr In IPv4 the first address in a subnet was called the network address and unusable for hosts. That does not exist in IPv6. The first address is however reserved as anycast address for the default gateway. PS: Not all end-users get a /48 so you cannot rely on all addresses in a /48 belonging together either. – Sander Steffann Jul 28 '15 at 16:22
  • I actually just wanted to point out that this might not be the best way of saying it. That IPv6 doesn't have a network address. Because there is 3 blocks reserved for network (network address), one for subnet (subnet address) and last 4 for users' devices. That makes more sense, to me at least :) – durrrr Jul 28 '15 at 16:53
  • That is a common way of address planning, but it doesn't have to be. A home user might only get a /56 (3.5 'block') which leaves only 8 bits (0.5 'block') for subnetting. – Sander Steffann Jul 28 '15 at 17:22
  • for this `$firstaddrhex = reset(unpack('H*', $firstaddrbin));` will create php7 notice , so u need to change to 2 lines. `unpack_value = unpack('H*', $firstaddrbin); $firstaddrhex = reset($unpack_value)` – Jasonw Apr 21 '22 at 05:36
5

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

Allen Ellis
  • 269
  • 2
  • 9
3

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

This converts a binary representation to printable 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;
}

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"
Mike Mackintosh
  • 13,917
  • 6
  • 60
  • 87
  • 2
    IPv6 does not support broadcast, there is thus no concept of a broadcast address. – Steve-o Apr 02 '13 at 19:58
  • @Steve-o Exactly, but since the term broadcast has been used in networking for a very long time, when emphasizing a range in IPv6, it is easier for people to understand. This is similar to the subnet segment, but many networks extend far past this segment.. – Mike Mackintosh Apr 03 '13 at 02:29
  • The link to the blog post is dead. Can you please update or remove it? – Michael Cordingley May 10 '18 at 17:29
1

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.

CodeAngry
  • 12,760
  • 3
  • 50
  • 57
  • This is the perfect one which is matching with below urls conversion http://www.gestioip.net/cgi-bin/subnet_calculator.cgi – MANOJ Aug 20 '18 at 11:37
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;
    }
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 01 '23 at 09:29