PHP:匹配子网列表中的IP(CIDR)

时间:2018-01-17 23:30:25

标签: php ip cidr

我有一个像这样的CIDR列表:

192.168.0.1/24
10.0.0.1/32
etc...

名单越来越多 为了检查IP是否适合这些CIDR中的一个,我使用以下函数执行循环

function cidr_match($ip, $range){
    list ($subnet, $bits) = explode('/', $range);
    $ip = ip2long($ip);
    $subnet = ip2long($subnet);
    $mask = -1 << (32 - $bits);
    $subnet &= $mask; // in case the supplied subnet was not correctly aligned
    return ($ip & $mask) == $subnet;
}

由于我的CIDR列表正在增长,我想改进该功能,以避免逐个测试每一行CIDR,直到它返回true。我想摆脱上面函数的 for 循环。
有没有办法对我要检查的IP执行“预检”,以便它不会按顺序运行完整列表(从上到下)?
我想优化,以便我的代码行为方式:将IP提供给函数 - &gt; “排序”列表或“找到”最可能的CIDR的功能种类 - &gt;在IP上检查最可能的CIDR - &gt;尽快返回“true”
我们将不胜感激。

2 个答案:

答案 0 :(得分:1)

老实说,除非你的CIDR范围很大并且你在同一个过程中检查了很多IP,否则你可能不会看到很多性能提升的方式。但是,如果这是您正在查看的场景,那么您可以通过预处理范围和IP来尝试挤压一些性能(执行一次ip2long()调用并存储分离的掩码/子网比较)。

例如,这就是你今天做的方式,我假设:

<?php
// Original style
$ranges = array(
  "192.168.0.1/32",
  "192.168.0.1/26",
  "192.168.0.1/24",
  "192.168.0.1/16",
  "127.0.0.1/24",
  "10.0.0.1/32",
  "10.0.0.1/24"
);


// Run the check
$start = microtime(true);
find_cidr("10.0.0.42", $ranges);
find_cidr("192.168.0.12", $ranges);
find_cidr("10.0.0.1", $ranges);
$end = microtime(true);
echo "Ran 3 find routines in " . ($end - $start) . " seconds!\n";

function find_cidr($ip, $ranges)
{
  foreach($ranges as $range)
  {
    if(cidr_match($ip, $range))
    {
      echo "IP {$ip} found in range {$range}!\n";
      break;
    }
  }  
}

function cidr_match($ip, $range){
    list ($subnet, $bits) = explode('/', $range);
    $ip = ip2long($ip);
    $subnet = ip2long($subnet);
    $mask = -1 << (32 - $bits);
    $subnet &= $mask; // in case the supplied subnet was not correctly aligned
    return ($ip & $mask) == $subnet;
}

在我的机器上,运行时间约为0.0005 - 0.001秒(针对少数几个范围检查3个IP)。

如果我写一些东西来预处理范围:

<?php
// Slightly-optimized style

$ranges = array(
  "192.168.0.1/32",
  "192.168.0.1/26",
  "192.168.0.1/24",
  "192.168.0.1/16",
  "127.0.0.1/24",
  "10.0.0.1/32",
  "10.0.0.1/24"
);

$matcher = new BulkCIDRMatch($ranges);
$start = microtime(true);
$matcher->FindCIDR("10.0.0.42");
$matcher->FindCIDR("192.168.0.12");
$matcher->FindCIDR("10.0.0.1");
$end = microtime(true);
echo "Ran 3 find routines in " . ($end - $start) . " seconds!\n";


class BulkCIDRMatch
{
  private $_preparedRanges = array();

  public function __construct($ranges)
  {
    foreach($ranges as $range)
    {
      list ($subnet, $bits) = explode('/', $range);
      $subnet = ip2long($subnet);
      $mask = -1 << (32 - $bits);
      $subnet &= $mask; // in case the supplied subnet was not correctly aligned

      $this->_preparedRanges[$range] = array($mask,$subnet);
    }
  }

  public function FindCIDR($ip)
  {
    $result = $this->_FindCIDR(ip2long($ip));
    if($result !== null)
    {
      echo "IP {$ip} found in range {$result}!\n";
    }
    return $result;
  }

  private function _FindCIDR($iplong)
  {
    foreach($this->_preparedRanges as $range => $details)
    {
      if(($iplong & $details[0]) == $details[1])
      {
        return $range;
      }
    }

    // No match
    return null;
  }
}

...然后我看到更快的CHECKING,但是当初始化类并且它处理并存储所有范围时,开头的开销略微增加。因此,如果我在少数范围内仅用3个IP计时OVERALL运行,那么&#34;优化&#34;方式实际上有点慢。但是,如果我针对10,000个CIDR运行1,000个IP,那么&#34;优化&#34;方式将比原始方式有更明显的改进(以额外的内存使用为代价来存储预处理的范围数据)。

所以这一切都取决于音量以及你想要做的事情。

也就是说,如果您担心0.001秒的性能太慢,那么PHP可能不是用于检查的正确语言。或者至少你可能想考虑编写自定义扩展,以便在C中完成更多的处理。

编辑:回答关于找到&#34;可能&#34;的原始问题。要检查的范围(在从字符串形式进行任何类型的转换之前),它可能不是一个非常可靠的尝试。范围可以跨越其初始八位字节,因此如果您开始比较这些值(例如&#34;我看着192.168.1.0,那么我只会查看从192和#34开始的范围; ),您不仅会在每个条目上产生字符串比较的性能开销(这会降低整体查找速度),但您可能会错过有效范围。

答案 1 :(得分:0)

如果您真的关心性能,那么您应该将列表存储在类似于结构的内容中,并以一种并不意味着查找每个条目的方式进行搜索,直到找到匹配为止。

在这种情况下,它是一个排序列表和二进制搜索:

class CidrList {

    protected $ranges = [];

    public function addRanges($ranges) {
        foreach($ranges as $range) {
            $this->addRange($range);
        }
        $this->sortRanges();
    }

    public function findRangeByIP($ip) {
        return $this->_findRangeByIP(ip2long($ip));
    }

    // simple binary search
    protected function _findRangeByIP($ip, $start=NULL, $end=NULL) {
        if( $end < $start || $start > $end ) { return false; }

        if( is_null($start) ) { $start = 0; }
        if( is_null($end)   ) { $end = count($this->ranges) -1; }

        $mid = (int)floor(($end + $start) / 2);
        switch( $this->inRange($ip, $this->ranges[$mid]) ) {
            case 0:
                return $this->ranges[$mid][2];
            case -1:
                return $this->_findRangeByIP($ip, $start, $mid-1);
            case 1:
                return $this->_findRangeByIP($ip, $mid+1, $end);
        }
    }

    // add a single range, protected as the list must be sorted afterwards.
    protected function addRange($range) {
        list ($subnet, $bits) = explode('/', $range);
        $subnet = ip2long($subnet);
        $mask = -1 << (32 - $bits);
        $min = $subnet & $mask;
        $max = $subnet | ~$mask;
        $this->ranges[] = [$min, $max, $range];
    }

    // sort by start, then by end. aka from narrowest overlapping range to widest
    protected function sortRanges() {
        usort($this->ranges, function($a, $b) {
            $res = $a[0] - $b[0];
            switch($res) {
                case 0:
                    return $a[1] - $b[1];
                default:
                    return $res;
            }
        });
    }

    protected function inRange($ip, $range) {
        list($start, $end, $cidr) = $range;
        if( $ip < $start ) { return -1; }
        if( $ip > $end ) { return 1; }
        return 0;
    }
}

用法:

$l = new CidrList();
$l->addRanges(["192.168.0.1/16", "192.168.0.1/24", "127.0.0.1/24", "10.0.0.1/24"]);

var_dump(
    $l->findRangeByIP('192.168.0.10'),
    $l->findRangeByIP('192.168.1.10'),
    $l->findRangeByIP('1.2.3.4')
);

输出:

string(14) "192.168.0.1/24"
string(14) "192.168.0.1/16"
bool(false)

此外,您应该通过缓存整个CidrList对象或其内部范围集来避免不断重新处理字符串。