将IPv6地址匹配到CIDR子网

11

有没有一种好的方式使用CIDR表示法将IPv6地址与IPv6子网匹配?我要找的是IPv6版本的以下内容:Matching an IP to a CIDR mask in PHP 5?

上述示例无法使用,因为IPv6地址长度为128位,阻止位左移正常工作。你能想到其他的方法吗?

编辑:已将我的解决方案添加到答案列表中。

7个回答

18

由于你不能将IPv6地址转换为整数,因此应该像这样操作位:

$ip='21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A';
$cidrnet='21DA:00D3:0000:2F3B::/64';

// converts inet_pton output to string with bits
function inet_to_bits($inet) 
{
   $splitted = str_split($inet);
   $binaryip = '';
   foreach ($splitted as $char) {
             $binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
   }
   return $binaryip;
}    

$ip = inet_pton($ip);
$binaryip=inet_to_bits($ip);

list($net,$maskbits)=explode('/',$cidrnet);
$net=inet_pton($net);
$binarynet=inet_to_bits($net);

$ip_net_bits=substr($binaryip,0,$maskbits);
$net_bits   =substr($binarynet,0,$maskbits);

if($ip_net_bits!==$net_bits) echo 'Not in subnet';
else echo 'In subnet';

还有,如果你使用某个数据库来存储IP地址,它可能已经具备了所有比较IP地址的函数。例如,Postgres拥有一个inet类型,可以像这样确定IP地址是否包含在子网中:

SELECT 
   '21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A'::inet << 
   '21DA:00D3:0000:2F3B::/64'::inet;

9.11. PostgreSQL中的网络地址函数和运算符


2
很好 - 我从来没有考虑过二进制字符串,不知为何。 - MW.
无法将IPv6地址转换为整数。我猜这是因为它们有128位,而整数仅限于32位或64位? - Sean the Bean
1
我不理解unpack调用。在我的情况下,没有它似乎效果更好。网络掩码通常以0结尾,它们被移除了。因此,“foreach(str_split($inet)as $char)”似乎是正确的。 - iRaS

8

您还可以使用IpUtils类,它来自symfony/http-foundation包:

IpUtils::checkIp6('2a01:8760:2:3001::1', '2a01:8760:2:3001::1/64')

这将检查IPv6的有效性和范围匹配。如果不符合条件,将返回false


5

PHP可以对字符串执行按位运算!

  • 支持IPv4或IPv6
  • 无需进行进制转换
  • 不需要ASCII比特串
  • 速度相当快
<?php

/**
 * Does the given IP match the CIDR prefix?
 */
function matchIp(string $ip, string $cidr): bool
{
  // Get mask bits
  list($net, $maskBits) = explode('/', $cidr);

  // Size
  $size = (strpos($ip, ':') === false) ? 4 : 16;

  // Convert to binary
  $ip = inet_pton($ip);
  $net = inet_pton($net);
  if (!$ip || !$net) {
    throw new InvalidArgumentException('Invalid IP address');
  }

  // Build mask
  $solid = floor($maskBits / 8);
  $solidBits = $solid * 8;
  $mask = str_repeat(chr(255), $solid);
  for ($i = $solidBits; $i < $maskBits; $i += 8) {
    $bits = max(0, min(8, $maskBits - $i));
    $mask .= chr((pow(2, $bits) - 1) << (8 - $bits));
  }
  $mask = str_pad($mask, $size, chr(0));

  // Compare the mask
  return ($ip & $mask) === ($net & $mask);
}


4
我创建了自己的解决方案,使用以下代码:
function iPv6MaskToByteArray($subnetMask) {
  $addr = str_repeat("f", $subnetMask / 4);
  switch ($subnetMask % 4) {
    case 0:
      break;
    case 1:
      $addr .= "8";
      break;
    case 2:
      $addr .= "c";
      break;
    case 3:
      $addr .= "e";
      break;
  }
  $addr = str_pad($addr, 32, '0');
  $addr = pack("H*" , $addr);
  return $addr;
}

function iPv6CidrMatch($address, $subnetAddress, $subnetMask) {
  $binMask = iPv6MaskToByteArray($subnetMask);
  return ($address & $binMask) == $subnetAddress;
}

请注意,$address和$subnetAddress是通过将字符串地址传递给inet_pton函数获取的。调用该函数的方法如下:
$subnet = inet_pton("2001:06b8::");
$mask = 32;
$addr = inet_pton("2001:06b8:0000:0000:0000:0000:1428:07ab");
$match = iPv6CidrMatch($addr, $subnet, $mask); // TRUE

3
这里有一个例子,通过将IP地址与包含单个IP或CIDR的列表进行比对来实现,支持IPv4和IPv6:

https://gist.github.com/lyquix-owner/2620da22d927c99d57555530aab3279b

<?php
// IP to check
$ip_check = $_SERVER['REMOTE_ADDR'];

// Array of allowed IPs and subnets, both IPv4 and IPv6
$ips_allowed = array(
    '192.30.252.0/22',
    '2620:112:3000::/44',
    '192.168.16.104'
);

// Flag for IP match allowed list
$allow = false;

foreach($ips_allowed as $ip_allow) {
    // If IP has / means CIDR notation
    if(strpos($ip_allow, '/') === false) {
        // Check Single IP
        if(inet_pton($ip_check) == inet_pton($ip_allow)) {
            $allow = true;
            break;
        }
    }
    else {
        // Check IP range
        list($subnet, $bits) = explode('/', $ip_allow);
        
        // Convert subnet to binary string of $bits length
        $subnet = unpack('H*', inet_pton($subnet)); // Subnet in Hex
        foreach($subnet as $i => $h) $subnet[$i] = base_convert($h, 16, 2); // Array of Binary
        $subnet = substr(implode('', $subnet), 0, $bits); // Subnet in Binary, only network bits
        
        // Convert remote IP to binary string of $bits length
        $ip = unpack('H*', inet_pton($ip_check)); // IP in Hex
        foreach($ip as $i => $h) $ip[$i] = base_convert($h, 16, 2); // Array of Binary
        $ip = substr(implode('', $ip), 0, $bits); // IP in Binary, only network bits
        
        // Check network bits match
        if($subnet == $ip) {
            $allow = true;
            break;
        }
    }
}
if(!$allow) {
    die('IP not allowed');
}

这个答案比OP要求的多做了一些,但是功能齐全。 - Mathieu Dubois
1
base_convert 也不能用于IPv6,因为这个这个原因。 - a55

1
如果您的掩码总是可被四整除的(这在ipv6中很常见),您可以使用以下方法:
function checkIPv6WithinRange($ipv6, $range) {
    list ($net, $mask) = preg_split("/\//", $range);

    if ($mask % 4)
        throw new NotImplementedException("Only masks divisible by 4 are supported");
    $stripChars = (128-$mask)/4;

    $hexNet = bin2hex(inet_pton($net));
    $reducedNet = substr($hexNet, 0, 0 - $stripChars);

    $hexIp = bin2hex(inet_pton($ipv6));
    $reducedIp = substr($hexIp, 0, 0 - $stripChars);

    return $reducedIp === $reducedNet;
}

2
我有很多口罩在家,它们不能被4整除! - Michael Hampton
1
简化:substr($hexNet, 0, $mask/4)。这样它也适用于IPv4 :) - Pascal Pixel Rigaux
如果你的掩码总是可以被四整除可能很常见,但这正是那种注定会让你后悔的任意限制。不建议使用这种解决方案。 - Sean the Bean

-1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接