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. Я хотел бы избавиться от за цикл вокруг моей функции выше.
Есть ли способ выполнить предварительную проверку IP-адреса, который я собираюсь проверить, чтобы он не запускал полный список последовательно (сверху вниз)?
Я хочу оптимизировать так, чтобы мой код вел себя следующим образом: назначьте IP функции -> виды функций «сортирует» список или «находит» наиболее вероятный CIDR -> запустите проверку IP для наиболее вероятный CIDR (s) -> вернуть «true» так скоро, как возможно
Руководство будет оценено.

0

Решение

Честно говоря, если ваш диапазон 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»), вы не только несете Снижение производительности при сравнении строк в каждой записи (что замедляет общий поиск), но вы можете упустить допустимый диапазон.

1

Другие решения

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

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

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

0

По вопросам рекламы [email protected]