18

In Ipv4 we can use ip2long to convert it to number,

How to convert ipv6 compressed to number in PHP?

I tried inet_pton and it's not working.

$ip_1='2001:0db8:85a3:0000:0000:8a2e:0370:7334'; 
$ip_2='2001:11ff:ffff:f';//Compressed
echo inet_pton($ip_1); 
//OUTPUT  ИЃ.ps4
echo inet_pton($ip_2);
//OUTPUT Warning: inet_pton(): Unrecognized address 2001:11ff:ffff:f
Ben
  • 2,562
  • 8
  • 37
  • 62

7 Answers7

14

Use:

$ip  = 'fe80:0:0:0:202:b3ff:fe1e:8329';
$dec = ip2long_v6($ip);
$ip2 = long2ip_v6($dec);

// $ip  = fe80:0:0:0:202:b3ff:fe1e:8329
// $dec = 338288524927261089654163772891438416681
// $ip2 = fe80::202:b3ff:fe1e:8329

Functions:

With enabled GMP or BCMATH extension.

function ip2long_v6($ip) {
    $ip_n = inet_pton($ip);
    $bin = '';
    for ($bit = strlen($ip_n) - 1; $bit >= 0; $bit--) {
        $bin = sprintf('%08b', ord($ip_n[$bit])) . $bin;
    }

    if (function_exists('gmp_init')) {
        return gmp_strval(gmp_init($bin, 2), 10);
    } elseif (function_exists('bcadd')) {
        $dec = '0';
        for ($i = 0; $i < strlen($bin); $i++) {
            $dec = bcmul($dec, '2', 0);
            $dec = bcadd($dec, $bin[$i], 0);
        }
        return $dec;
    } else {
        trigger_error('GMP or BCMATH extension not installed!', E_USER_ERROR);
    }
}

function long2ip_v6($dec) {
    if (function_exists('gmp_init')) {
        $bin = gmp_strval(gmp_init($dec, 10), 2);
    } elseif (function_exists('bcadd')) {
        $bin = '';
        do {
            $bin = bcmod($dec, '2') . $bin;
            $dec = bcdiv($dec, '2', 0);
        } while (bccomp($dec, '0'));
    } else {
        trigger_error('GMP or BCMATH extension not installed!', E_USER_ERROR);
    }

    $bin = str_pad($bin, 128, '0', STR_PAD_LEFT);
    $ip = array();
    for ($bit = 0; $bit <= 7; $bit++) {
        $bin_part = substr($bin, $bit * 16, 16);
        $ip[] = dechex(bindec($bin_part));
    }
    $ip = implode(':', $ip);
    return inet_ntop(inet_pton($ip));
}

demo

Glavić
  • 42,781
  • 13
  • 77
  • 107
13

Note that all answers will result in incorrect results for big IP addresses or are going through a highly complicated process to receive the actual numbers. Retrieving the actual integer value from an IPv6 address requires two things:

  1. IPv6 support
  2. GMP extension (--with-gmp)

With both prerequisites in place conversion is as simple as:

$ip = 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff';
$int = gmp_import(inet_pton($ip));

echo $int; // 340282366920938463463374607431768211455

The binary numeric packed in_addr representation that is returned by inet_pton is already an integer and can directly be imported to GMP, as illustrated above. There is no need for special conversions or anything.

Note that the other way around is equally simple:

$int = '340282366920938463463374607431768211455';
$ip = inet_ntop(str_pad(gmp_export($int), 16, "\0", STR_PAD_LEFT));

echo $ip; // ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff

Hence building the two desired functions is as simple as:

function ipv6_to_integer($ip) {
    return (string) gmp_import(inet_pton($ip));
}

function ipv6_from_integer($integer) {
    return inet_ntop(str_pad(gmp_export($integer), 16, "\0", STR_PAD_LEFT));
}
Fleshgrinder
  • 15,703
  • 4
  • 47
  • 56
8

$ip_2 is not a valid IPv6 address. You need "::" somewhere in there, to indicate the zero omission point.

If you have it as one of

$ip_2='2001::11ff:ffff:f';
$ip_2='2001:11ff::ffff:f';
$ip_2='2001:11ff:ffff::f';

then inet_pton() should work fine.

As already hinted, PHP doesn't have a 128 integer type, so the best you can get is a numeric string out of the binary string inet_pton() gives you... yes, that's what it is, and that's why it looks weird. If you look at the bits of that strings, you'll see they're exactly what you'd expect.

Here's how to expand the binary string into a numeric string (argument "0" was missing from str_pad() originally):

/**
 * @param string $ip A human readable IPv4 or IPv6 address.
 * @return string Decimal number, written out as a string due to limits on the size of int and float.
 */
function ipv6_numeric($ip) {
    $binNum = '';
    foreach (unpack('C*', inet_pton($ip)) as $byte) {
        $binNum .= str_pad(decbin($byte), 8, "0", STR_PAD_LEFT);
    }
    // $binNum is now a human readable string, but in binary.
    // If you have the gmp PHP extension, you can convert it to decimal
    return gmp_strval(gmp_init(ltrim($binNum, '0'), 2), 10);
}
boen_robot
  • 1,450
  • 12
  • 24
  • so what should I store this in database? I try to find ip location from db,something like INET_ATON("123.243.51.83") BETWEEN INET_ATON(ip_start) AND INET_ATON(ip_end) – Ben Aug 16 '13 at 15:43
  • @Ben Well, it depends. You could just store the binary string from inet_pton(). The numeric string is really only useful if you need to later do some "math" over the address. If you're simply canonizing IP notations, comparing inet_pton()'s output of two notations is enough. – boen_robot Aug 16 '13 at 15:46
  • @Ben Oh, so you want to check ranges... well, that's a kind of "math", so yeah. You may want to use a function like the above, and then pass the results to something like the GMP functions to do the math. – boen_robot Aug 16 '13 at 15:48
  • getting confuse, I current store ipv4 & ipv6 at same column varbinary(16), and i try to convert this column to another column in number (int10), should I separate ipv4 & ipv6 in different column? – Ben Aug 16 '13 at 15:51
  • @Ben You don't need to convert them to a base 10 integer to do a comparison. In fact, if the ip_start and ip_end columns already contain VARBINARY(16), then all you need to do is "INET6_ATON("123.243.51.83") BETWEEN ip_start AND ip_end". VARBINARY is sort of a numeric type, in that it does such kind of computations in the way you'd expect it to. – boen_robot Aug 16 '13 at 15:57
  • I tried INET_ATON, the query took me 14sec, thats why i try to convert it to number to see will this help – Ben Aug 16 '13 at 15:59
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/35609/discussion-between-boen-robot-and-ben-wong) – boen_robot Aug 16 '13 at 16:01
  • 2
    @boen_robot: function `ipv6_numeric` doesn't work correctly. For `2001:11ff:ffff::f` it outputs `42540853245347496466006288282062604240` which is invalid, it should `42540853245347499564798372846648688655`, like [here](http://www.v6decode.com/#address=2001%3A11ff%3Affff%3A%3Af). – Glavić Oct 21 '13 at 14:16
  • 2
    This is because `base_convert` suffers from an overflow, there is also a huge red box on the PHP manual that warns about exactly that. See my answer. – Fleshgrinder May 16 '16 at 09:20
  • Voted down as this answer is incorrect due to integer overflow in `base_convert`. – Geoffrey Jul 10 '18 at 04:43
  • I've fixed that problem by requiring gmp... People who might not be able to install it can look at the comment, and maybe alter the code to not do the final step, and just work with the binary version somehow. – boen_robot Jun 03 '21 at 10:27
2

Try this

function inet6_to_int64($addr)
{
    /* Expand the address if necessary */
    if (strlen($addr) != 39) {
        $addr = inet6_expand($addr);
        if ($addr == false) return false;
    } // if
    $addr = str_replace(':', '', $addr);
    $p1 = '0x' . substr($addr, 0, 16);
    $p2 = '0x' . substr($addr, 16);
    $p1 = gmp_init($p1);
    $p2 = gmp_init($p2);
    $result = array(gmp_strval($p1), gmp_strval($p2));
    return $result;
} // inet6_to_int64()

For more functions or details please visit http://www.soucy.org/project/inet6/

usman allam
  • 274
  • 1
  • 5
2

Here's two functions that convert hex to decimal and decimal to hex. This only works with IPv6 hex and IPv6 integer representations (so, use ip2long() and long2ip() for IPv4 numbers). Theoretically, one could convert an IPv4 dotted notation number to hex values and use these, but that would probably be overkill.

This will:

  • Convert all complete IPv6 numbers to a stringified long int (up to 39 characters, left padded for sorting if the flag is set to true.
  • Convert a stringified "long" back to a hexidecimal IPv6 representation, left padding as necessary to the full 32 bit hex string. Colons are optionally placed, right to left, if the appropriate flag is set to true.

These can be modified to handle virtually any length hex value or virtually any length integer, and placement of colons and padding can be adjusted accordingly.

HEX to DECIMAL

    function bchexdec($hex,$padl)
    // Input: A hexadecimal number as a String.
    // Output: The equivalent decimal number as a String.
    // - if padl==true then pad left to fill 39 characters for string sort

    {
        if (strlen($hex) != 39) 
        {
            $hex = inet6_expand($hex);
            if ($hex == false) return false;
        }

        $hex=str_replace(":","",$hex);
        $dec = 0;
        $len = strlen($hex);
        for ($i = 1; $i <= $len; $i++) 
        {
            $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
        }

        if ($padl==true)
        {
            $dec=str_pad($dec,39,"0",STR_PAD_LEFT);
        }
        return $dec;

    }

DECIMAL to HEX

    function bcdechex($dec,$colon) 
    // Input: A decimal as a String.
    // Output: The equivalent hex value.
    // - if $colon==true then add colons.   
    {
        $hex = '';

        // RFC 5952, A Recommendation for IPv6 Address Text Representation, Section 4.3 Lowercase specifies letters should be lowercase. Though that generally doesn't matter, use lowercase
        //   to conform with the RFC for those rare systems that do so. 
        do 
        {    
            $last = bcmod($dec, 16);
            $hex = dechex($last).$hex;
            $dec = bcdiv(bcsub($dec, $last), 16);
        } while($dec>0);
        $hex=str_pad($hex,32,"0",STR_PAD_LEFT);
        // Add colons if $colon==true
        if ($colon==true)
        {
            $hex=strrev($hex);
            $chunks=str_split($hex,4);
            $hex=implode(":", $chunks);
            $hex=strrev($hex);
        }

        return $hex;
    }

This is based off of ideas and examples found in a variety of places, as well as my own needs for easily sortable and easily storable IPv6 addresses.

Robert Mauro
  • 566
  • 8
  • 17
  • 1
    If you have any skipped zeros or `::` in address you'll get unexpected wrong result converting it to number string. `str_replace(":","",$hex)` would just lower the most significant address values. Make sure you are using full IPv6 address with the function above. – Carl di Ortus May 03 '19 at 06:59
  • @CarldiOrtus yes, I noted that in the first bullet point when I wrote it: "Convert all *complete* IPv6 numbers to a stringified long int (up to 39 characters, left padded for sorting if the flag is set to true." - perhaps there is better wording I could use? – Robert Mauro May 03 '19 at 14:16
  • 1
    yeah I probably have just missed one word in your sentence, discovered this only after trying it. Maybe you could improve your answer with adding the same info in the paragraph above the bullet points? This answer is good for those unlucky ones having no `gmp` support. To expand IPv6 address to full take a look at the link provided by user `usman allam` answer. – Carl di Ortus May 06 '19 at 08:59
1

OK, some revelations from chat with Ben Wong... The REAL issue is optimizing lookups in a DB about a geoIP service.

The DB layout offered by this geoIP database is too slow to just have plain ol' BETWEEN applied on the start and end, even though the storage is the most economic one you can get.

The proposal I first outlined in the chat was to fragment the IP address into 4 ints which are compared sequentially, but on second though, that may not be enough, since you're still searching over the entire DB, which is over 1 million rows. I also had an idea about doing matches with subnet masks, but given that some ranges are not within large masks, doing this may, again, not be enough.

I'll see what I can do, and I'll edit this answer. But I'm posting this in the meantime for anyone else willing to assist in this.

boen_robot
  • 1,450
  • 12
  • 24
1

A version of the answer by boen_robot which avoids the overflow issue, using BC Math if available.

function ipv62numeric($ip)
{
    $str = '';
    foreach (unpack('C*', inet_pton($ip)) as $byte) {
        $str .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
    }
    $str = ltrim($str, '0');
    if (function_exists('bcadd')) {
        $numeric = 0;
        for ($i = 0; $i < strlen($str); $i++) {
            $right  = base_convert($str[$i], 2, 10);
            $numeric = bcadd(bcmul($numeric, 2), $right);
        }
        $str = $numeric;
    } else {
        $str = base_convert($str, 2, 10);
    }

    return $str;
}

Example:

echo ipv62numeric('2001:11ff:ffff::f');

Will return "42540853245347499564798372846648688655" as a string, which is the correct answer.

Heath Dutton
  • 4,282
  • 1
  • 13
  • 6