32

After searching around somewhat thoroughly, I noticed a slight lack of functions in PHP for handling IPv6. For my own personal satisfaction I created a few functions to help the transition.

The IPv6ToLong() function is a temporary solution to that brought up here: How to store IPv6-compatible address in a relational database. It will split the IP in to two integers and return them in an array.

/**
 * Convert an IPv4 address to IPv6
 *
 * @param string IP Address in dot notation (192.168.1.100)
 * @return string IPv6 formatted address or false if invalid input
 */
function IPv4To6($Ip) {
    static $Mask = '::ffff:'; // This tells IPv6 it has an IPv4 address
    $IPv6 = (strpos($Ip, '::') === 0);
    $IPv4 = (strpos($Ip, '.') > 0);

    if (!$IPv4 && !$IPv6) return false;
    if ($IPv6 && $IPv4) $Ip = substr($Ip, strrpos($Ip, ':')+1); // Strip IPv4 Compatibility notation
    elseif (!$IPv4) return $Ip; // Seems to be IPv6 already?
    $Ip = array_pad(explode('.', $Ip), 4, 0);
    if (count($Ip) > 4) return false;
    for ($i = 0; $i < 4; $i++) if ($Ip[$i] > 255) return false;

    $Part7 = base_convert(($Ip[0] * 256) + $Ip[1], 10, 16);
    $Part8 = base_convert(($Ip[2] * 256) + $Ip[3], 10, 16);
    return $Mask.$Part7.':'.$Part8;
}

/**
 * Replace '::' with appropriate number of ':0'
 */
function ExpandIPv6Notation($Ip) {
    if (strpos($Ip, '::') !== false)
        $Ip = str_replace('::', str_repeat(':0', 8 - substr_count($Ip, ':')).':', $Ip);
    if (strpos($Ip, ':') === 0) $Ip = '0'.$Ip;
    return $Ip;
}

/**
 * Convert IPv6 address to an integer
 *
 * Optionally split in to two parts.
 *
 * @see https://stackoverflow.com/questions/420680/
 */
function IPv6ToLong($Ip, $DatabaseParts= 2) {
    $Ip = ExpandIPv6Notation($Ip);
    $Parts = explode(':', $Ip);
    $Ip = array('', '');
    for ($i = 0; $i < 4; $i++) $Ip[0] .= str_pad(base_convert($Parts[$i], 16, 2), 16, 0, STR_PAD_LEFT);
    for ($i = 4; $i < 8; $i++) $Ip[1] .= str_pad(base_convert($Parts[$i], 16, 2), 16, 0, STR_PAD_LEFT);

    if ($DatabaseParts == 2)
            return array(base_convert($Ip[0], 2, 10), base_convert($Ip[1], 2, 10));
    else    return base_convert($Ip[0], 2, 10) + base_convert($Ip[1], 2, 10);
}

For these functions I typically implement them by calling this function first:

/**
 * Attempt to find the client's IP Address
 *
 * @param bool Should the IP be converted using ip2long?
 * @return string|long The IP Address
 */
function GetRealRemoteIp($ForDatabase= false, $DatabaseParts= 2) {
    $Ip = '0.0.0.0';
    // [snip: deleted some dangerous code not relevant to question. @webb]
    if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] != '')
        $Ip = $_SERVER['REMOTE_ADDR'];
    if (($CommaPos = strpos($Ip, ',')) > 0)
        $Ip = substr($Ip, 0, ($CommaPos - 1));

    $Ip = IPv4To6($Ip);
    return ($ForDatabase ? IPv6ToLong($Ip, $DatabaseParts) : $Ip);
}

Someone please tell me if I'm reinventing the wheel here or I've done something wrong.

This implementation converts IPv4 to IPv6. Any IPv6 address it doesn't touch.

webb
  • 4,180
  • 1
  • 17
  • 26
matpie
  • 17,033
  • 9
  • 61
  • 82
  • 4
    Your `getRealRemoteIp` is dangerously flawed - an attacker could spoof one of those headers the method tests for, and override `REMOTE_ADDRESS` with an arbitrary value. If you use that IP address for logging, you can't rely on the addresses logged. – Pekka Oct 01 '11 at 22:17
  • 3
    For the sake of the internet's health, please edit your question and remove the GetRealRemoteIp function (which is a misnomen, GetSpoofedRemoteIp is way more appropriate). – oxygen Jun 09 '12 at 00:49

5 Answers5

20

How about inet_ntop()? Then instead of chopping things into integers, you just use a varbinary(16) to store it.

1

Going back, I wrote two functions, dtr_pton and dtr_ntop which work with both IPv4 and IPv6. It will convert them back and forth between printable and binary.

The first function, dtr_pton will check if the supplied argument is valid IPv4 or valid IPv6. Depending on the outcome, an exception could be thrown, or the binary representation of the IP could be returned. By using this function, you can then perform AND'ing or OR'ing against the result (for subnetting/whathaveyou). I would suggest you store these in your database as a VARBINARY(39) or VARCHAR(39).

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

The second function, dtr_ntop will convert the binary representation of the IP back to a printable IP address.

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

Also, here is a quick way of expanding IPv6 addresses found on StackOverflow

function expand($ip){
    $hex = unpack("H*hex", inet_pton($ip));         
    $ip = substr(preg_replace("/([A-f0-9]{4})/", "$1:", $hex['hex']), 0, -1);

    return $ip;
}

Also, a good read on the subject could be found on my blog at HighOnPHP: 5 Tips for Working With IPv6 in PHP. This article uses some of the methods described above in an Object Oriented class which can be found at GitHub: mikemackintosh\dTR-IP

Samuel Liew
  • 76,741
  • 107
  • 159
  • 260
Mike Mackintosh
  • 13,917
  • 6
  • 60
  • 87
  • In those two functions, the 'A' should be 'a', as with the former, any '0' octets produce a '32' (ipv4) or '20' (ipv6) from being padded with a space (ASCII 32), rather than a NUL. – Patanjali Sep 15 '16 at 10:15
1

You could also store the address in a binary(16) in mysql, so you should have an option to output it in binary from IPv6ToLong().

This is really something that need to be added natively in PHP, especially when many IPv6 enabled web-servers report ::FFFF:1.2.3.4 as the client IP and it's incompatible with ip2long, and will break alot of stuff.

Fredrik
  • 11
  • 1
1

PHP.net's Filter extension contains some constants for matching IPv4 and IPv6 addresses, which might be useful for checking the address. I haven't seen any conversion utilities though.

Ross
  • 46,186
  • 39
  • 120
  • 173
1

Here is an alternative function using filter_var (PHP >= 5.2)

function IPv4To6($ip) {
 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === true) {
  if (strpos($ip, '.') > 0) {
   $ip = substr($ip, strrpos($ip, ':')+1);
  } else { //native ipv6
   return $ip;
  }
 }
 $is_v4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
 if (!$is_v4) { return false; }
 $iparr = array_pad(explode('.', $ip), 4, 0);
    $Part7 = base_convert(($iparr[0] * 256) + $iparr[1], 10, 16);
    $Part8 = base_convert(($iparr[2] * 256) + $iparr[3], 10, 16);
    return '::ffff:'.$Part7.':'.$Part8;
}
datahell
  • 11
  • 1
  • 2
    `filter_var()` will return the filtered data on success, so your first `if` statement will actually fail with valid IPv6 addresses. To fix change `=== true` to `!== false` – John McMahon May 23 '14 at 21:28