Работа с IPv6 в PHP

Наскоро получихме статус LIR и /29 блок IPv6. След това стана необходимо да следим назначените подсети. И тъй като нашият биллинг е написан на 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. То може да се среща в анотацията само веднъж. С други думи, 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 в бинарна маска. За това ще съберем нейното hex представяне и ще я опаковаме в бинарно.

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) опакова hex представянето по същия начин, както и 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 E5-2650 v4 на стойност 9000 евро за малко пари?

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

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