Как работать с IPv6 в PHP

11 комментариев

Работа с IPv6 в PHP

Вольный перевод:
https://www.mikemackintosh.com/5-tips-for-working-with-ipv6-in-php/

Работать с IPv4 в php было очень просто, для этого существовало две функции ip2long и long2ip. Эти функции давали возможность переводить IP адрес в обычное число и обратно. К сожалению, в IPv6 такой возможности нет.

Адрес IPv4 состоит из 32 бит — размер который изначально поддерживает большинство операционных систем и языков программирования. 32-ух и более битные платформы поддерживают работу с числами от 0 до 4 294 967 295 (или 232, которое и является максимальным количеством IP адресов в сети). Это давало возможность преобразовывать адрес в число, что в свою очередь давало возможность экономично использовать память и ресурсы системы.

С IPv6 же другая история. На данный момент большинство компьютеров используют 64-ех битную архитектуру, и работают под управлением 64-ех битной ОС. Самое большое беззнаковое число допустимое на 64bit платформе — 18 446 744 073 709 551 616 (или 264). Но IPv6 допускает несоизмеримо больше количество адресов — 340 282 366 920 938 463 463 374 607 431 770 000 000 (или 2128). Это очень огромное число, и, к сожалению, это приводит к проблемам при работе с ним.

Работа с таким количеством битов в числе является громоздким даже для современных языков программирования, и из за проблем работы с памятью на таких числах алгоритмы поддержки IPv6 обычно неправильны.

Прим.автора: Обратите внимание: IPv6 не имеет широковещательных адресов (broadcast). В IPv4 последний адрес в диапазоне зарезервированы для трансляции. В IPv6, нет концепции широковещания, вместо этого используется «многоадресная рассылка» по локальной ссылке для всех нод, ff02::1.

Валидация IPv4 и IPv6.

Это самая простая задача из всех возможных. Некоторые из программистов используют конструкции strpos( $ip , ":") для определения IPv6, и substr_count( $ip , ".") == 3 для проверки IPv4 адреса.

Но это в корне неправильно. Во первых такие проверки небезопасны, по причине того что легко обходятся. А во вторых они просто неточны. Так например, эти функции не смогут правильно обработать адреса IPv6, записанные в IPv4 совместимом формате, такие как, например, этот ::127.0.0.1 или этот ::ffff:10.10.1.1.

В php есть функция filter_var, которая осуществляет проверку входящих данных. В нашем случае, для валидации IP адреса, следует использовать фильтр FILTER_VALIDATE_IP. А для определения версии — флаги FILTER_FLAG_IPV4 или FILTER_FLAG_IPV6. Например:

Это правильный и безопасный способ проверки. Проверять IP адреса стоит именно так, если вы, конечно, не задались целью наломать дров, но таки сделать свой велосипед. Кстати хорошим тоном будет использование filter_var и для проверки многих других данных, таких как email или URL.

Преобразования IPv6

В PHP 5.1 были добавлены две полезные функции inet_pton и inet_ntop. Данные функции преобразуют человеко-понятное представление IP адреса в упакованное in_addr представление, и обратно. Так как результат выполнения inet_pton не является чисто бинарным, то необходимо воспользоваться функцией unpack чтобы в дальнейшем можно было работать с битовыми операциями.

Обе функции поддерживают IPv4 и IPv6. Единственное различие состоит в том как распаковывать полученный результат. Так например для IPv6, вы должны использовать формат A16, а для IPv4 — A4.

Давайте рассмотрим это на примере:

Как видно inet_pton поддерживает и IPv4 и IPv6. Следующий шаг — нужно преобразовать упакованный результат в распакованное значение:

Функция current возвращает первый элемент массива, эквивалентно $array[0]

Как видно после распаковывания и упаковывания, мы видим тот же IP адрес что и был изначально. Это наглядно демонстрирует то что данные в процессе никуда не пропадают.

Готовые функции

Мы огромные сторонники подхода DRY. Под DRY-подходом в программировании подразумевается «не повторяться» (Don’t repeat yourself). Использование готовых функций, классов и пр. являются хорошим примером идеологии DRY. В результате мы подготовили набор готовых функций для того чтобы упростить ваш код.

Функция dtr_pton реализует всю логику которая была описана выше. Она проверяет входящее значение, возвращает преобразованное значение или false/исключение в случае возникновения ошибки:

Функция же dtr_ntop преобразует результат выполнения dtr_pton обратно. Она также проверяет входящее значение на соответствие форматам A4/A16 и возвращает результат либо false/throw в случае возникновения ошибки.

Вот несколько примеров для работы с описанными функциями. Для начала попробуем преобразовать IPv6 туда и обратно:

Проделаем тоже самое с IPv4:

Вот что будет если на вход dtr_ntop дать невалидное значение (мы добавили еще один байт к распакованному значению):

Теперь попробуем преобразовать невалидный IP адрес:

Битовые операции

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

Если вы изучили и понимаете вышенаписанное, то можно приступить к получению сетевых параметров из адреса IP и CIDR.

Используя функции dtr_pton и dtr_ntop мы можем вычислить адрес подсети и широковещательный адрес например для 10.22.99.199/28:

Это выведет:
string(12) "10.22.99.192"
string(12) "10.22.99.207"

Теперь возьмем IPv6, fe80:1:2:3:a:bad:1dea:dad/82:

Выведет:
string(18) "fe80:1:2:3:a:ba0::"
string(26) "fe80:1:2:3:a:baf:ffff:ffff"

Работа с базой данных

Конечно для хранения IPv6 адреса вы можете использовать VARCHAR(39), т.е. хранить его в человекочитаемом виде. Но я бы рекомендовал хранить IP адрес в бинарном виде используя BINARY(16). Если вы будете хранить адрес в бинарном формате то вы сможете делать выборки по диапазонам, битовым маскам и прочее, а это огромное преимущество для построения приложения поддерживающего IPv6.

С версии MySQL 5.6.3 появились функции INET6_ATON(expr) и INET6_NTOA(expr). Как вы уже догадались они преобразуют IPv6 адрес в бинарный вид и обратно. Для версий MySQL младше (если у вас есть права для установки UDF (User-Defined Functions)), я бы рекомендовал установить вот это mysql-udf-ipv6. Этот пакет добавляет отсутствующие функции INET6_ATON(expr) и INET6_NTOA(expr) в версии младше 5.6.3.

  1. Странник

    1,5 года назад впервые посетил этот сайт и нашёл совершенно заброшенным, но добавил в закладки. И вот теперь, через 1,5 года зайдя сюда снова, приятно удивлён возрождением активности. Желаю автору и в дальнейшем продолжать этот интересный блог.

    1. Дмитрий Амиров Автор

      Благодарю) Да, я действительно опомнился, и начал снова сюда писать) Теперь можете заходить почаще чем раз в полтора года, у меня есть куча идей для последующих записей.

      PS: если честно, вы мне аж прям настроение подняли) еще раз спасибо

  2. Radeon

    C ipv6 все намного сложнее получается, и как то непонятно чтоли… Впрочем почерпнул для себя фишку с filter_var — намного лучше чем городить регулярки.

    PS: у вас подсказки голосовалки «Оцените статью:» на русский не переведены ;)

    1. Дмитрий Амиров Автор

      Да я вот с трудом представлял с какой стороны подходить к ipv6, пришлось вот статейку английскую нагуглить, да и перевел заодно.

      Насчет подсказок знаю, все никак руки не дойдут перевести) спасибо

    2. Sept

      Ну, регулярками я никогда не пользовался, конечно. Ибо связка ip2long -> long2ip работает быстрее и надёжнее.
      Но фишка с filter_var явилась для меня новостью и действительно бесподобна. Работает почти втрое шустрее других вариантов и на порядок нагляднее.
      Век живи — век учись :)

  3. X111

    Здравствуйте! Извините, если мой вопрос покажется слишком примитивным. Я пишу функцию, которая имеет два входных параметра — начальный IP и конечный IP. IP адреса могут быть ipv4, ipv6.
    Функция должна выводить список всех IP адресов, которые помещаются в указанный диапазон. С IPV4 — никаких проблем не было, а вот с IPV6 вывод не получается. Вот код функции, который выполняется правильно, для IPV4:

    Для IPV6 я попробовал, для начала, с помощью inet_pton() перевести входные параметры в двоичные числа. Но вместо двоичных чисел получил какую-то хр*нь.

    Может быть, Вы объясните чайнику, как реализовать оставшуюся часть функции.
    Спасибо за статью, и извините, если время отнял.

    1. Дмитрий Амиров Автор

      Вместо двоичных чисел вы получили байтовое представление IP адреса. Т.е. чтобы получить двоичное представление из этой строки вам нужно каждый байт преобразовать в двоичный вид.

      А вообще по поводу вашей задачи — тут не все так просто. PHP не может оперировать с представлением IPv6 в виде числа даже на 64 битовых системах. Одно из решений ставить на сервер http://php.net/manual/ru/book.gmp.php либо же делать аналог своими средставами (а именно функции сравнения, сложения, вычитания).

      1. X111

        Спасибо, что прояснили ситуацию! Просто человек, который выдал это задание, сказал, что его выполнение займёт полчаса. Для IPV4 заняло 15 минут. А с IPV6 уже несколько дней возился, потом решил Вам написать. Спасибо ещё раз.

        1. Дмитрий Амиров Автор

          Рад что смог помочь. С ipv6 работы не на полчаса, а однозначно поболее) хотя зря вы ждали несколько дней, надо было сразу мне написать

  4. Андрей

    Здравствуйте! Можете привести пример как делать выборки в БД по диапазонам IPv6, используя BINARY(16)?

    Заранее спасибо.

Добавить комментарий

Прочли запись? Понравилась? Не стесняйтесь, оставьте, пожалуйста, свой комментарий. Мне очень интересно, что вы думаете об этом. Кстати в комментарии вы можете задать мне любой вопрос. Я обязательно отвечу.

Вы можете оставить коментарий анонимно, для этого можно не указывать Имя и email. Все комментарии проходят модерацию, поэтому ваш комментарий появится не сразу.