Change IPv4 to IPv6 string - php

Sander Steffann mentioned in a previous question of mine:
Addresses like 0000:0000:0000:0000:0000:0000:192.168.0.1 are written as
0000:0000:0000:0000:0000:0000:c0a8:0001 which is exactly the same address
but in hex notation.
How do I detect in PHP if an address was written like eg.: ::0000:192.168.0.1 or 0000::0000:192.168.0.1 or 0000:0000:0000:0000:0000:0000:192.168.0.1 etc.? Is it enough to check if an IP-based string has '.' AND ':' ?
And how do I change this to the full string 0000:0000:0000:0000:0000:0000:c0a8:0001?
Am I correct, to change this to IPv4 will be something like:
<?php
$strIP = '0000:0000:0000:0000:0000:0000:192.168.0.1';
$strResult = substr($strIP, strrpos($strIP, ':'));
echo $strResult; //192.168.0.1 ?
?>
... or are correct IP string representations more complex than what this snippet could do?

I can't believe I wrote this all out in one go and it worked the first time.
$strIP = '0000:0000:0000:0000:0000:0000:192.168.0.1';
$arrIP = explode(':', $strIP);
if( preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $arrIP[count($arrIP)-1]) ) {
$ip4parts = explode('.', $arrIP[count($arrIP)-1]);
$ip6trans = sprintf("%02x%02x:%02x%02x", $ip4parts[0], $ip4parts[1], $ip4parts[2], $ip4parts[3]);
$arrIP[count($arrIP)-1] = $ip6trans;
$strIP = implode(':', $arrIP);
}
echo $strIP; //output: 0000:0000:0000:0000:0000:0000:c0a8:0001
Basically:
Explode the string on :
Check if the last quad is formatted like an IP4 address
Explode the last quad on .
Re-print the IP4 octets into two hex quads
Replace the IP4 quad with the new ones
Implode the array on :.

Your best bet is to not do this manually, but instead call inet_pton to get a binary representation, and then convert that to the format you wish to have.
$foo = inet_pton("::1");
for ($i = 0 ; $i < 8 ; $i++)
$arr[$i] = sprintf("%02x%02x", ord($foo[$i * 2]), ord($foo[$i * 2 + 1]));
$addr = implode(":", $arr);

First of all: why would you care how the address is written? inet_pton() will parse all variations for you and give you a consistent result, which you can then transform into binary, hex, or whatever you want.
All the code for converting things like ::192.168.0.1 to 0000:0000:0000:0000:0000:0000:c0a8:0001 was actually in my post. That's exactly what my example function does.
If you feed 0000:0000:0000:0000:0000:0000:192.168.0.1 to inet_pton() and then to inet_ntop() you'll get the canonical IPv6 notation, which is ::192.168.0.1 in this case. If that string begins with :: and the rest contains no : and three dots then you can be pretty sure it's an IPv4 address ;-)
To combine the answer to your previous question with this question:
function expand_ip_address($addr_str) {
/* First convert to binary, which also does syntax checking */
$addr_bin = #inet_pton($addr_str);
if ($addr_bin === FALSE) {
return FALSE;
}
$addr_hex = bin2hex($addr_bin);
/* See if this is an IPv4-Compatible IPv6 address (deprecated) or an
IPv4-Mapped IPv6 Address (used when IPv4 connections are mapped to
an IPv6 sockets and convert it to a normal IPv4 address */
if (strlen($addr_bin) == 16
&& substr($addr_hex, 0, 20) == str_repeat('0', 20)) {
/* First 80 bits are zero: now see if bits 81-96 are either all 0 or all 1 */
if (substr($addr_hex, 20, 4) == '0000')
|| substr($addr_hex, 20, 4) == 'ffff')) {
/* Remove leading bits so only the IPv4 bits remain */
$addr_bin = substr($addr_hex, 12);
}
}
/* Then differentiate between IPv4 and IPv6 */
if (strlen($addr_bin) == 4) {
/* IPv4: print each byte as 3 digits and add dots between them */
$ipv4_bytes = str_split($addr_bin);
$ipv4_ints = array_map('ord', $ipv4_bytes);
return vsprintf('%03d.%03d.%03d.%03d', $ipv4_ints);
} else {
/* IPv6: print as hex and add colons between each group of 4 hex digits */
return implode(':', str_split($addr_hex, 4));
}
}

Related

PHP - Get range boundaries from IPv6 and cidr [duplicate]

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.
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
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
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"
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.

How to calculate the next ipv6 address?

I've got stuck on a hopefully simple task: I want to get the next ip address.
Here are some of my tests:
//$binaryIp = inet_pton('192.168.1.1');
$binaryIp = inet_pton('2001:cdba::1');
$verySimple = inet_ntop(
$binaryIp++
);
var_dump($verySimple); //'2001:cdba::1'
$simpleMaths = inet_ntop(
$binaryIp + inet_pton('0.0.0.1')
);
var_dump($simpleMaths); //inet_ntop(): Invalid in_addr value
$aLittleBitOfSuccess = long2ip(
ip2long(inet_ntop($binaryIp)) + 1
);
var_dump($aLittleBitOfSuccess); //'0.0.0.1' but with IPv4 '192.168.1.2'
OK, until here it's obvious that my tries are more nonsense than a real aproach to my problem but what else can I try? I've searched the web and found some solutions for subnet calculations and stuff like that but nothing for simple addition or subtraction.
My next try would be to split the string from inet_ntop() and fiddle around with the hex-values but there must be a simple solution to add 1 to an in6_addr!
With a few exceptions, an IPv6 address is divided into two 64-bit parts: the Network/Subnet and the Interface ID. You should be interested in the 64 bits of the Interface ID.
The easiest thing to do is to parse the address into the two 64-bit unsigned integers, increment the Interface ID, then recombine the two parts into a 128-bit address.
I've choosen the hex-way and made this function:
protected function binaryIncrement($binaryIp, $increment = 1) {
//inet_pton creates values where each "character" is one ip-address-byte
//we are splitting the string so we can handle every byte for itselve.
$binaryIpArrayIn = str_split($binaryIp);
$binaryIpArrayOut = array();
$carry = 0 + $increment;
//reverse array because our following addition is done from right to left.
foreach (array_reverse($binaryIpArrayIn) as $binaryByte) {
//transforming on byte from our ip address to decimal
$decIp = hexdec(bin2hex($binaryByte));
$tempValue = $decIp + $carry;
$tempValueHex = dechex($tempValue);
//check if we have to deal with a carry
if (strlen($tempValueHex) > 2) {
//split $tempValueHex in carry and result
//str_pad because hex2bin only accepts even character counts
$carryHex = str_pad(substr($tempValueHex,0,1),2,'0',STR_PAD_LEFT);
$tempResultHex = str_pad(substr($tempValueHex,1,2),2,'0',STR_PAD_LEFT);
$carry = hexdec($carryHex);
} else {
$carry = 0;
$tempResultHex = str_pad($tempValueHex,2,'0',STR_PAD_LEFT);
}
//fill our result array
$binaryIpArrayOut[] = hex2bin($tempResultHex);
}
//we have to reverse our arry back to normal order and building a string
$binaryIpOut = implode(array_reverse($binaryIpArrayOut));
return $binaryIpOut;
}
$binaryIpV6In = inet_pton('2001:cdba::FFFF');
$binaryIpV6Out = $this->binaryIncrement($binaryIpV6In);
var_dump(inet_ntop($binaryIpV6Out));
$binaryIpV4In = inet_pton('192.168.1.1');
$binaryIpV4Out = $this->binaryIncrement($binaryIpV4In, 256);
var_dump(inet_ntop($binaryIpV4Out));
This way I can use the same method for IPv4 and IPv6.

Is inet_pton() broken for some IPv6 addresses that "look like" IPv4 addresses?

I'm using PHP version 5.2.17, and I see that the following works as expected:
$x = inet_pton('::F');
$y = inet_ntop($x);
print "::F -> $y\n";
Output: ::F -> ::f
But the following does not:
$a = inet_pton('::FEEF:1886');
$b = inet_ntop($a);
print "::FEEF:1886 -> $b\n";
Output: ::FEEF:1886 -> ::254.239.24.134
I would have expected the second code snippet to produce this output:
::FEEF:1886 -> ::feef:1886
What is it about the IPv6 address ::FEEF:1886 that makes PHP think it is really an IPv4 address? The inet_ntop/inet_pton conversion works correctly with other addresses having 0 in the "high" 96 bits (e.g. ::F).
EDIT: My first thought was that this might be a bug in my version of PHP, but using this online PHP sandbox I see the same behavior for PHP versions up through 5.6.2. So either this is deliberate (in which case I would dearly like to know the reason for this behavior) or a bug which persists in modern versions of PHP.
ADDENDUM: I opened PHP Bug 69232 on March 12, 2015 for this apparent inconsistency in the behavior of inet_ntop() for addresses in ::/96.
The textual representation of IPv6 addresses permit for multiple different valid representations of every IPv6 address.
This means that all of these valid textual representations of an IPv6 address each map to the same binary 128 bit string when passed through inet_pton.
However when converting the binary 128 bit string to textual representation using inet_ntop, it can obviously only output one of the many valid strings representing that IP address. The one it chose is called the canonical representation.
It is always valid to write the last 32 bits of an IPv6 address using IPv4 notation. However only a few classes of IPv6 addresses used that format as their canonical representation.
::/96 has been deprecated, but that just means those addresses are not supposed to be used anymore, it doesn't affect how they are treated by inet_pton and inet_ntop.
::ffff:0.0.0.0/96 is another prefix, which use IPv4 notation in their canonical representation. That prefix is used for IPv4 compatibility in the socket API, but those are never send on the wire, because they are for situations where the traffic on the wire will be IPv4.
What you're looking at is an IPv4 address being represented (incorrectly) as an IPv6 address. This practice was officially deprecated in 2006 by RFC 4291:
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.
Try this out :
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;
}
And you can call this function instead of 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
To summarize what I've head so far in the answers & comments:
IPv6 addresses have a canonical format which is what inet_ntop() returns.
The ::/96 address range is deprecated, but ::ffff/80 is not.
Although it would make sense for all ::/96 addresses to be rendered as ::/IPv4-address by inet_ntop() it appears that inet_ntop() renders ::/112 addresses and "higher" in ::/96 as ::/IPv4-dotted-quad (e.g. ::254.239.24.134) and renders ::/96 addresses "lower" than ::/112 as "normal" IPv6 addresses.
If you want inet_ntop() to render all IPv6 addresses the same way (i.e. 8 hex words with the usual zero compression rules) then you need to write your own method to achieve this.
My own workaround is to extend inet_ntop() by rewriting any IPv4 dotted quads as hexwords (and I exploded the logic into multiple methods to make it easier for me to keep track of what I was doing):
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);
}
Although arguably much less elegant than the _inet_ntop() function proposed by JC Sama, it runs about 25% faster over my (essentially random) test cases.
Answers provided by kasperd, diskwuff, and JC Sama offer both helpful information and workarounds which are likely to be useful to other SO readers, so I have upvoted them all. But they do not address my original question directly, so I'm adding this answer:
The behavior of the PHP function inet_pton() is correct. The problem is that inet_ntop() does not treat IPv6 address in ::/96 consistently. This is a bug in PHP.

Ipv4 mapped Ipv6 validation

In the rfc4291
said, that first 80 bits of this type of Ipv4 mapped Ipv6 address is 0.
How to check that with PHP?
I need more quick and safe way to do that instead of regexp.
The easiest way is to convert the address from printable to binary form with inet_pton. That will give you a string where every charter corresponds to 8 bits of the address. Checking if the first 80 bits are zero is then as simple as checking the first 10 characters of the returned string:
$addr = '::10.1.2.3';
$bytes = inet_pton($addr);
if (substr($bytes, 0, 10) == "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") {
echo "Yes\n";
} else {
echo "No\n";
}

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.
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
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
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"
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.

Categories