Недавно мы получили статус LIR и /29 блок 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().
На практике реализуем три наиболее вероятные, как мне показалось, задачи:
Реализация будет для IPv6, но при необходимости логику можно легко адаптировать. Некоторые идеи я почерпнул отсюда, но реализовал немного иначе. Также в примерах нет проверки на ошибки ввода. Итак, поехали.
Как я уже упоминал, первый и последний адрес диапазона можно определить с помощью побитовых операций, зная начало диапазона и бинарную маску подсети. Соответственно, первым делом нам надо превратить CIDR в бинарную маску. Для этого соберём её hex представление и запакуем в бинарное.
Вызов pack('H*', $mask) пакует hex представление таким же образом, как и inet_pton(). С той лишь разницей, что при вызове pack() все 0 должны быть на своих местах, а двоеточий в записи быть не должно, в отличие от человекопонятной записи.
Следующий шаг — вычисление начала и конца диапазона. И вот тут возникнут нюансы. Побитовые операции ограничены разрядностью процессора. Соответственно, на моём 32-битном CubieTruck, который я порой использую для всякого тестового баловства, все 128 бит адреса обработать за одну операцию не выйдет. Впрочем, ничто не мешает нам разбить его на группы по 32 бита (на всякий случай, кто знает, на каких процессорах будем запускаться).
Для будущего использования предусмотрим возможность передавать IP и получать результат как в бинарном, так и в человекочитаемом виде. Параметр $which тут задаёт, хотим ли мы получить начало или конец диапазона (значения 'start' или 'end' соответственно).
Следующая задача (к тому же наиболее практичная для нашей компании) — вычисление следующего диапазона. Для данной задачи ничего лучше не пришло в голову, кроме как разложить адрес в двоичную строку и добавить 1 в нужной позиции, после чего свернуть всё назад. Чтобы нигде не появилось артефактов, я решил при разложении и сборке дробить адрес по байту.
На выходе получим префикс следующего диапазона размера, указанного в $cidr. Этой функцией мы выделяем блоки адресов нашим клиентам.
Наконец, проверка, принадлежит ли адрес диапазону. К примеру, мы выделили один блок /48 под раздачу клиентам блоков /64, и нам нужно убедиться, что при назначении мы не выходим за пределы выделенного блока (на практике это случится нескоро, но всё же вероятность есть). Тут всё просто. Получаем начало и конец диапазона в бинарном виде и проверяем, находится ли адрес в пределах.
Надеюсь, было полезно. Какие ещё функции по работе с адресами могут по Вашему мнению быть полезны? Любые дополнения, комментарии и код-ревью горячо приветствуются в комментариях.
Если Вы уже являетесь нашим клиентом или только думаете им стать, по случаю выхода этой статьи мы предлагаем Вам получить блок /64 совершенно бесплатно для всех услуг vps или выделенный сервер в дата центре Equinix Tier IV, Нидерланды по запросу в отдел продаж, предоставив ссылку на эту статью в тикете. Предложение действует до марта 2020-го года.
Спасибо, что остаётесь с нами. Вам нравятся наши статьи? Хотите видеть больше интересных материалов? Поддержите нас, оформив заказ или порекомендовав знакомым, облачные VPS для разработчиков от $4.99, уникальный аналог entry-level серверов, который был придуман нами для Вас: Вся правда о VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps от $19 или как правильно делить сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).
Dell R730xd в 2 раза дешевле в дата-центре Equinix Tier IV в Амстердаме? Только у нас 2 х Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 ТВ от $199 в Нидерландах! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — от $99! Читайте о том Как построить инфраструктуру корп. класса c применением серверов Dell R730xd Е5-2650 v4 стоимостью 9000 евро за копейки?
Немного теории
Дисклеймер. Если Вы знакомы с тем, что такое 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().
Историческая справка
Бесклассовой предшествовала классовая сегрегация адресация. В те далёкие годы никто не предполагал, что подсетей будет так много, раздавали их направо и налево большими блоками: класс А — префиксом были первые 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-х разработали бесклассовую концепцию, позволившую не привязываться к лидирующим битам. Чуть больше подробностей можно почерпнуть, скажем, в великой и всезнающей.
Перейдём к практике
На практике реализуем три наиболее вероятные, как мне показалось, задачи:
- получение первого и последнего адреса диапазона;
- получение следующего диапазона заданного размера (CIDR);
- проверка на принадлежность адреса диапазону.
Реализация будет для 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 Cores) 10GB DDR4 480GB SSD 1Gbps от $19 или как правильно делить сервер? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).
Dell R730xd в 2 раза дешевле в дата-центре Equinix Tier IV в Амстердаме? Только у нас 2 х Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 ТВ от $199 в Нидерландах! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — от $99! Читайте о том Как построить инфраструктуру корп. класса c применением серверов Dell R730xd Е5-2650 v4 стоимостью 9000 евро за копейки?
E_STRICT
Vasiliskov Автор
Для того же Python есть хотя бы стандартная либа, которая умеет побольше, чем упаковывать и распаковывать.
Gemorroj
а зачем вы писали с нуля?
с ходу на packagist вижу 2 готовые либы
https://github.com/mlocati/ip-lib
https://github.com/rlanvin/php-ip