How to handle IPv6 Addresses in PHP?

50,903

Solution 1

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

Solution 2

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.

Solution 3

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

Solution 4

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.

Solution 5

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

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

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

Share:
50,903

Related videos on Youtube

matpie
Author by

matpie

I'm a modest JavaScript developer. I love working with Vue.js, Webpack, and Node.js. I like refactoring applications for performance and maintainability. Code re-usability and readability are my top-priorities.

Updated on July 09, 2022

Comments

  • matpie
    matpie almost 2 years

    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.

    • Pekka
      Pekka over 12 years
      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.
    • oxygen
      oxygen almost 12 years
      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).
  • Bharanikumar
    Bharanikumar about 13 years
    why should i use varbinary instead of varchar, what is this inet_ntop ,why should should not use the $_SERVER['REMOTE_ADDR'];
  • Pacerier
    Pacerier over 12 years
    Shouldn't it be binary(16) ?
  • Rok Kralj
    Rok Kralj about 12 years
    No, since in case address is just IPv4, then it'll consume just 4 bytes instead of 16.
  • Marcin
    Marcin over 11 years
    I think you meant inet_pton();
  • Admin
    Admin over 11 years
    @Marcin: For storing values yes, but it eventually requires both.
  • halfdan
    halfdan over 10 years
    Just don't use ip2long.
  • John McMahon
    John McMahon almost 10 years
    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
  • Patanjali
    Patanjali over 7 years
    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.