Работа с IPv6 в PHP

Скоро получихме статус LIR и блок IPv6 /29. И след това се появи необходимостта да водим отчет за назначените подсети. И тъй като нашият биллинг е написан на PHP, трябваше да се запознаем с въпроса и да осъзнаем, че този език не е най-дружелюбният за работа с IPv6. Под кат ще представим нашето решение на възникналите задачи свързани с адресите и диапазоните. Възможно е да не е най-елегантното, но задачите, които сме си поставили, се изпълняват.

Работа с IPv6 в PHP

Няколко теории

Отказ от отговорност. Ако сте запознати с това какво е IPv6 и с какво се консумира, тази част може да бъде скучна за вас. Може и да не е.

Хората, които за първи път виждат анотация на IPv6, могат да бъдат доста затруднени. След елегантните 64.233.177.101 внезапно се сблъскваме с 2607:f8b0:4002:c08::8b и можем да се объркаме. И двете са просто разбираеми представяния на 32 и 128 бита съответно. Всеки IP пакет съдържа заглавие с строго стандартно предназначение за всеки бит. Не навлизайки още по-дълбоко в структурата на заглавията, оттук трябва да изведем едно: за операциите с IP адреси и диапазони е удобно да използваме бинарна математика и побитови операции. Най-удобно е да се съхраняват в базата данни като BINARY(4) за IPv4 и BINARY(16) за IPv6.

Още един важен аспект, който си заслужава да споменем, е маските на мрежата и CIDR нотацията. CIDR е акроним от Classless Inter-Domain Routing (безкласова адресация). Тази концепция замени класовата в определянето на коя част от IP адреса е префикс на мрежата, а коя е адресът на мрежовия интерфейс в тази мрежа. В практиката първите n бита, които съответстват на префикса, ще бъдат зададени на 1, а останалите на 0.

На разбираем език това се записва като ip.add.re.ss/cidr. Например, 64.233.177.0/24 означава, че първите 24 бита принадлежат на префикса. Последните 8 бита, те също — последното число в разбираемата нотация, принадлежат на адреса в подмрежата. Още няколко упражнения. 64.233.177.101/32 и 2607:f8b0:4002:c08::8b/128 — един конкретен адрес. 2607:f8b0:4002:c08::/64 — първите 64 бита (първите 4 групи) — префикс, останалите 64 бита — локалната част. Между другото, ако някого обърква "::" в записа, двойното двоеточие заменя произволен брой секции, съдържащи 0. То може да се среща в анотацията само 1 път. С други думи, 2607:f8b0:4002:c08::8b = 2607:f8b0:4002:c08:0:0:0:8b.

Какво можем да извлечем от всичко това? Първо, адресите на първата и последната подмрежа могат да се получат с помощта на бинарни операции AND и OR, знаейки маската в бинарен вид. На второ място, следващата подмрежа с размер (т.е. с CIDR) n може да се изчисли, добавяйки 1 към n-та позиция в бинарното представяне. Под бинарно представяне имам предвид резултата от използването на функции pack() и inet_pton() и по-нататъшното използване на бинарни оператори, под бинарно — представяне в двоична система, което може да бъде получено, например, с помощта на base_convert().

Историческа справкаПреходът от класова адресация към безкласова е бил в миналото. В онези далечни години никой не предполагаше, че ще има толкова много подсетове, и те се разпределяха наляво и надясно в големи блокове: клас A — префиксът е първите 8 бита (т.е. първото число), с водещ бит 0; клас B — първите 16 (първите две числа), водещи битове 10; клас C — първите 24 бита, водещи битове 110. Тези водещи битове задаваха диапазоните, в които се издава адрес на даден клас: 0.0.0.0 — 127.255.255.255 за клас A, 128.0.0.0 — 191.255.255.255 — клас B, 192.0.0.0 — 223.255.255.255 — клас C. Когато интернет започна да се разпространява по планетата, регулаторите осъзнаха, че са сгрешили, и в началото на 90-те разработиха концепцията за безкласова адресация, която позволи да не се обвързват с водещите битове. Малко повече подробности могат да бъдат намерени, например, в великата и всезнаеща.

Да преминем към практиката

На практика ще реализираме три най-вероятни, каквито ми се струват, задачи:

  1. получаване на първия и последния адрес от диапазона;
  2. получаване на следващия диапазон с даден размер (CIDR);
  3. проверка дали адресът принадлежит на диапазона.

Реализацията ще бъде за IPv6, но при нужда логиката може лесно да се адаптира. Някои идеи почерпих оттук, но реализирах малко по-различно. Също така в примерите няма проверка за входни грешки. И така, нека започнем.

Както вече споменах, адресите на първия и последния адрес от диапазона могат да бъдат определени с помощта на битови операции, знаейки началото на диапазона и бинарната маска на подсетата. Съответно, първото нещо, което трябва да направим, е да преобразуваме CIDR в бинарна маска. За това ще съберем нейното хекс представяне и ще я опаковаме в бинарен формат.

function cidrToMask ($cidr) {
    $mask = str_repeat('f', ceil($cidr / 4));
    $mask .= dechex(4 * ($cidr % 4));
    $mask = str_pad($mask, 32, '0');
    return pack('H*', $mask);
}

Извикването pack(‘H*’, $mask) опакова хекс представянето по същия начин, както и inet_pton(). С тази разлика, че при извикването pack() всички 0 трябва да бъдат на местата си, а двоеточия в записа не трябва да има, за разлика от човекоразбираемия запис.

Следващата стъпка е изчисляването на началото и края на диапазона. И тук ще възникнат нюанси. Битовите операции са ограничени от разрядността на процесора. Съответно, на моя 32-битов CubieTruck, който понякога използвам за различни тестови действия, не е възможно да обработя всичките 128 бита адрес за една операция. Въпреки това, нищо не пречи да го разделим на групи по 32 бита (за всеки случай, кой знае на какви процесори ще бъдем активни).

function getRangeBoundary ($ip, $cidr, $which, $ipIsBin = false, $returnBin = false) {
    $mask = cidrToMask($cidr);
    if (!$ipIsBin) {
        $ip = inet_pton($ip);
    }
    $ipParts   = str_split($ip, 4);
    $maskParts = str_split($mask, 4);
    $rangeParts  = [];
    for ($i = 0; $i < count($ipParts); $i++) {
        if ($which == 'start') {
            /* Побитовый & адреса и маски оставит только биты префикса. */
            $rangeParts[$i] = $ipParts[$i] & $maskParts[$i];
        } else {
            /* Побитовый | с обратной маской (~) оставит биты префикса и установит все биты локальной части в 1. */
            $rangeParts[$i] = $ipParts[$i] | ~$maskParts[$i];
        }
    }
    $rangeBoundary = implode($rangeParts);
    if ($returnBin) {
        return $rangeBoundary;
    } else {
        return inet_ntop($rangeBoundary);
    }
}

За бъдеща употреба предвиждаме възможността да предаваме IP и да получаваме резултата както в двоичен, така и в човекоразбираем вид. Параметърът $which тук задава, дали искаме да получим началото или края на диапазона (стойностите 'start' или 'end' съответно).

Следващата задача (освен че е най-практична за нашата компания) е изчисляването на следващия диапазон. За тази задача нищо по-добро не ми дойде наум, освен да разложа адреса в двоична низ и да добавя 1 на нужната позиция, след което да сглобя всичко обратно. За да избегна появата на артефакти, реших при разлагането и сглобяването да разделя адреса по байт.

function getNextBlock ($ipStart, $cidr, $ipIsBin = false, $returnBin = false) {
    if (!$ipIsBin) {
        $ipStart = inet_pton($ipStart);
    }
    $ipParts = str_split($ipStart, 1);
    $ipBin   = '';
    foreach ($ipParts as $ipPart) {
        $ipBin .= str_pad(base_convert(unpack('H*', $ipPart)[1], 16, 2), 8, '0', STR_PAD_LEFT);
    }
    /* Добавляем 1 в нужном разряде двоичного представления строки "влоб" :) */
    $i = $cidr - 1;
    while ($i >= 0) {
        if ($ipBin[$i] == '0') {
            $ipBin[$i] = '1';
            break;
        } else {
            $ipBin[$i] = '0';
        }
        $i--;
    }
    $ipBinParts = str_split($ipBin, 8);
    foreach ($ipBinParts as $key => $ipBinPart) {
        $ipParts[$key] = pack('H*', str_pad(base_convert($ipBinPart, 2, 16), 2, '0', STR_PAD_LEFT));
    }
    $nextIp = implode($ipParts);
    if ($returnBin) {
        return $nextIp;
    } else {
        return inet_ntop($nextIp);
    }
}

На изхода ще получим префикс на следващия диапазон с размер, посочен в $cidr. С тази функция ние разпределяме блокове адреси на нашите клиенти.

Накрая, проверка дали адресът принадлежи на диапазона. Например, разпределихме един блок /48 за раздаване на клиенти на блокове /64, и трябва да се уверим, че при назначаването не излизаме извън пределите на разпределения блок (в практиката това няма да се случи скоро, но все пак е възможно). Тук е просто. Получаваме началото и края на диапазона в двоичен вид и проверяваме дали адресът е в границите.

function ipInRange ($ip, $rangeStart, $cidr) {
    $start = getRangeBoundary($rangeStart, $cidr, 'start', false, true);
    $end = getRangeBoundary($rangeStart, $cidr, 'end', false, true);
    $ipBin = inet_pton($ip);
    return ($ipBin >= $start && $ipBin <= $end);
}

Надявам се да е било полезно. Какви други функции за работа с адреси смятате, че ще бъдат полезни? Всякакви допълнения, коментари и прегледи на кода са горещо приветствани в коментарите.

Ако вече сте наш клиент или само сериозно обмисляте да станете такъв, по случай публикуването на тази статия ви предлагаме напълно безплатно блок /64 за всички услуги VPS или видим сървър в дата център Equinix Tier IV в Нидерландия, по ваше искане към отдела за продажби, предоставяйки връзка към тази статия в тикета. Офертата е валидна до март 2020 година.

Малко реклама 🙂

Благодаря, че оставате с нас. Харесвате ли нашите статии? Искате ли да видите повече интересни материали? Подкрепете ни, като направите поръчка или я препоръчате на познати, облачни VPS за разработчици от $4.99, уникален аналог на entry-level сървъри, създаден от нас за Вас: Всичко за VPS (KVM) E5-2697 v3 (6 ядра) 10GB DDR4 480GB SSD 1Gbps от $19 или как да разделите правилно сървъра? (налични опции с RAID1 и RAID10, до 24 ядра и до 40GB DDR4).

Dell R730xd два пъти по-евтин в дата-центъра Equinix Tier IV в Амстердам? Само при нас 2 х Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100TB от $199 в Нидерландия! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — от $99! Четете за това Как да изградим инфраструктура от корпоративен клас с прилагане на сървъри Dell R730xd Е5-2650 v4 на стойност 9000 евро за копейки?

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster