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 функции -> виды функций "сортирует" список или "находит" наиболее вероятный CIDR -> запустите проверку IP для наиболее вероятный CIDR(s) -> вернуть "true" как можно скорее
Руководство будет оценено.

2 ответа

Честно говоря, если ваш диапазон 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;
  }
}

... тогда я вижу более быструю ПРОВЕРКУ, но в начале немного больше издержек, когда класс инициализируется, и он обрабатывает и сохраняет все диапазоны. Так что, если бы я рассчитал OVERALL запустить всего 3 IP-адреса против нескольких диапазонов, "оптимизированный" путь на самом деле немного медленнее. Но если я запускаю 1000 IP-адресов против 10 000 CIDR, "оптимизированный" способ будет иметь более заметные улучшения по сравнению с оригинальным способом (за счет дополнительного использования памяти для хранения предварительно обработанных данных диапазона).

Так что все действительно зависит от объема и того, что вы пытаетесь сделать.

Тем не менее, если вы обеспокоены тем, что скорость 0.001 секунды будет слишком низкой, то PHP может оказаться неподходящим языком для ваших проверок. Или, по крайней мере, вы можете захотеть написать собственное расширение, чтобы больше обработки делалось на C.

РЕДАКТИРОВАТЬ: Чтобы ответить на первоначальный вопрос о поиске "вероятных" диапазонов для проверки (до любого преобразования из его строковой формы), это, вероятно, не очень надежная вещь, чтобы попробовать. Диапазоны могут выходить за пределы их начальных октетов, поэтому, если вы начнете сравнивать эти значения (например, "Я смотрю на 192.168.1.0, поэтому я буду смотреть только на диапазоны, начинающиеся с 192"), вы не только несете Снижение производительности при сравнении строк для каждой записи (что замедляет общий поиск), но вы можете упустить допустимый диапазон.

Если вы на самом деле беспокоитесь о производительности, то вы должны хранить список в чем-то, напоминающем структуру, и искать его таким образом, который не означает, что вы просматриваете каждую запись, пока не найдете совпадение.

В данном случае это отсортированный список и бинарный поиск:

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 объект или его внутренний набор диапазонов.

Другие вопросы по тегам