如何在 PHP 中处理 IPv6 地址?

Posted

技术标签:

【中文标题】如何在 PHP 中处理 IPv6 地址?【英文标题】:How to handle IPv6 Addresses in PHP? 【发布时间】:2010-10-01 11:48:54 【问题描述】:

在彻底搜索之后,我注意到 php 中稍微缺乏处理 IPv6 的函数。为了我个人的满意,我创建了一些函数来帮助过渡。

IPv6ToLong() 函数是对这里提出的问题的临时解决方案:How to store IPv6-compatible address in a relational database。它会将 IP 拆分为两个整数并以数组的形式返回。

/**
 * 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://***.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);

对于这些函数,我通常先调用这个函数来实现它们:

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

如果我在这里重新发明***或者我做错了什么,请告诉我。

此实现将 IPv4 转换为 IPv6。任何不涉及的 IPv6 地址。

【问题讨论】:

您的 getRealRemoteIp 存在危险的缺陷 - 攻击者可以欺骗方法测试的这些标头之一,并使用任意值覆盖 REMOTE_ADDRESS。如果您使用该 IP 地址进行日志记录,则不能依赖记录的地址。 为了互联网的健康,请编辑您的问题并删除 GetRealRemoteIp 功能(这是用词不当,GetSpoofedRemoteIp 更合适)。 【参考方案1】:

inet_ntop() 怎么样?然后,您只需使用varbinary(16) 来存储它,而不是将事物切成整数。

【讨论】:

为什么我应该使用varbinary而不是varchar,这个inet_ntop是什么,为什么不应该使用$_SERVER['REMOTE_ADDR']; 不应该是binary(16)吗? 不,因为如果地址只是 IPv4,那么它将只消耗 4 个字节而不是 16 个。 我想你的意思是 inet_pton(); @Marcin:用于存储值是的,但最终两者都需要。【参考方案2】:

PHP.net 的 Filter extension 包含用于匹配 IPv4 和 IPv6 地址的 some constants,这对于检查地址可能很有用。不过,我还没有看到任何转换实用程序。

【讨论】:

【参考方案3】:

您还可以将地址存储在 mysql 中的二进制 (16) 中,因此您应该可以选择从 IPv6ToLong() 以二进制形式输出它。

这确实是需要在 PHP 中本地添加的东西,尤其是当许多启用 IPv6 的 Web 服务器报告 ::FFFF:1.2.3.4 作为客户端 IP 并且它与 ip2long 不兼容时,会破坏很多东西。

【讨论】:

只是不要使用 ip2long。【参考方案4】:

这是一个使用 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;

【讨论】:

filter_var() 将在成功时返回过滤后的数据,因此您的第一个 if 语句实际上会因有效的 IPv6 地址而失败。修复将 === true 更改为 !== false【参考方案5】:

回过头来,我写了两个函数,dtr_ptondtr_ntop,它们都适用于 IPv4 和 IPv6。它将在可打印和二进制之间来回转换它们。

第一个函数dtr_pton 将检查提供的参数是有效的 IPv4 还是有效的 IPv6。根据结果​​,可能会引发异常,或者可能会返回 IP 的二进制表示。通过使用此函数,您可以对结果执行 AND'ing 或 OR'ing(用于子网划分/whathaveyou)。我建议您将这些以VARBINARY(39)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;

第二个函数,dtr_ntop 会将 IP 的二进制表示转换回可打印的 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;

另外,这里有一个扩展 IPv6 地址的快速方法found on ***

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;

此外,可以在我的博客HighOnPHP: 5 Tips for Working With IPv6 in PHP 上找到有关该主题的好读物。本文在一个面向对象的类中使用了上面描述的一些方法,可以在GitHub: mikemackintosh\dTR-IP找到。

【讨论】:

在这两个函数中,'A' 应该是 'a',和前者一样,任何 '0' 八位位组都会产生 '32' (ipv4) 或 '20' (ipv6)用空格(ASCII 32)填充,而不是 NUL。

以上是关于如何在 PHP 中处理 IPv6 地址?的主要内容,如果未能解决你的问题,请参考以下文章

PHP 中的 REMOTE_ADDR 和 IPv6

如何在 Vista/Windows7 中添加持久 IPv6 地址?

如何查看电脑的IPV6地址?

如何在 mobilefirst 混合应用程序中使用 IPV6 IP 地址构建应用程序?

如何在 python 中使用“struct”库发送 IPv6 地址

家里联通宽带如何使用ipv6