Работа с IPv6 в PHP

Недавно мы получили статус LIR и /29 блок IPv6. А следом возникла необходимость вести учёт назначенных подсетей. И коль скоро наш биллинг написан на PHP, пришлось немного проникнуться вопросом и осознать, что этот язык — не самый дружелюбный в плане работы с IPv6. Под катом — наше решение возникших задач по работе с адресами и диапазонами. Возможно, не самое элегантное, но поставленные задачи выполняет.

Работа с IPv6 в PHP

Un po' di teoria

Dichiarazione di non responsabilità. Если Вы знакомы с тем, что такое 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. Ad esempio, 64.233.177.0/24 обозначает, что первые 24 бита относятся к префиксу. Последние 8 бит, они же — последнее число в человекопонятной записи, относятся к адресу внутри подсети. Ещё пару упражнений. 64.233.177.101/32 e 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() e inet_pton() и дальнейшее использование побитовых операторов, под двоичным — представление в двоичной системе, которое можно получить, скажем, с помощью base_convert().

Nota storicaБесклассовой предшествовала классовая сегрегация адресация. В те далёкие годы никто не предполагал, что подсетей будет так много, раздавали их направо и налево большими блоками: класс А — префиксом были первые 8 бит (т.е. первое число), с лидирующим битом 0; класс В — первые 16 (первые два числа), лидирующие биты 10; класс С — первые 24 бита, лидирующие биты 110. Эти самые лидирующие биты задавали диапазоны, в которых выдавался адрес того или иного класса: 0.0.0.0 — 127.255.255.255 для класса А, 128.0.0.0 — 191.255.255.255 — класс В, 192.0.0.0 — 223.255.255.255 — класс С. По мере того, как интернет расползался по планете, регуляторы поняли, что промахнулись, и в начале 90-х разработали бесклассовую концепцию, позволившую не привязываться к лидирующим битам. Чуть больше подробностей можно почерпнуть, скажем, в великой и всезнающей.

Перейдём к практике

На практике реализуем три наиболее вероятные, как мне показалось, задачи:

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

Реализация будет для IPv6, но при необходимости логику можно легко адаптировать. Некоторые идеи я почерпнул da qui, но реализовал немного иначе. Также в примерах нет проверки на ошибки ввода. Итак, поехали.

Как я уже упоминал, первый и последний адрес диапазона можно определить с помощью побитовых операций, зная начало диапазона и бинарную маску подсети. Соответственно, первым делом нам надо превратить 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);
}

La chiamata 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’ o ‘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-го года.

Un po' di pubblicità 🙂

Grazie per essere con noi. Ti piacciono i nostri articoli? Vuoi vedere più contenuti interessanti? Supportaci effettuando un ordine o raccomandandoci ai tuoi conoscenti, VPS cloud per sviluppatori a partire da $4,99, un'alternativa unica ai server entry-level, che abbiamo creato per te: Tutta la verità su VPS (KVM) E5-2697 v3 (6 Core) 10GB DDR4 480GB SSD 1Gbps a partire da $19. Come dividere il server correttamente? (disponibili opzioni con RAID1 e RAID10, fino a 24 core e fino a 40GB DDR4).

Dell R730xd a metà prezzo nel data center Equinix Tier IV ad Amsterdam? Solo da noi 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TB a partire da $199 nei Paesi Bassi! Dell R420 — 2x E5-2430 2.2GHz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — a partire da $99! Scopri di più su Come costruire un'infrastruttura di classe enterprise con server Dell R730xd E5-2650 v4 dal costo di 9000 euro a prezzi stracciati?

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster