Вольный перевод:
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
. Например:
if( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ){ // Это валидный IPv4 адрес } if( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ){ // Это валидный 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
.
Давайте рассмотрим это на примере:
// Случайные Ip адреса для примера $ip4= "10.22.99.129"; $ip6= "fe80:1:2:3:a:bad:1dea:dad"; // Пример с ip2long var_dump( ip2long($ip4) ); // int(169239425) var_dump( ip2long($ip6) ); // bool(false) // Примре с inet_pton var_dump( inet_pton( $ip4 ) ); // string(4) " c" var_dump( inet_pton( $ip6 ) ); // string(16) "� �"
Как видно inet_pton
поддерживает и IPv4 и IPv6. Следующий шаг — нужно преобразовать упакованный результат в распакованное значение:
// Распаковываем и упаковываем $_u4 = current( unpack( "A4", inet_pton( $ip4 ) ) ); var_dump( inet_ntop( pack( "A4", $_u4 ) ) ); // string(12) "10.22.99.129" $_u6 = current( unpack( "A16", inet_pton( $ip6 ) ) ); var_dump( inet_ntop( pack( "A16", $_u6 ) ) ); //string(25) "fe80:1:2:3:a:bad:1dea:dad"
Функция
current
возвращает первый элемент массива, эквивалентно$array[0]
Как видно после распаковывания и упаковывания, мы видим тот же IP адрес что и был изначально. Это наглядно демонстрирует то что данные в процессе никуда не пропадают.
Готовые функции
Мы огромные сторонники подхода DRY. Под DRY-подходом в программировании подразумевается «не повторяться» (Don’t repeat yourself). Использование готовых функций, классов и пр. являются хорошим примером идеологии DRY. В результате мы подготовили набор готовых функций для того чтобы упростить ваш код.
Функция dtr_pton
реализует всю логику которая была описана выше. Она проверяет входящее значение, возвращает преобразованное значение или false/исключение в случае возникновения ошибки:
/** * dtr_pton * * Converts a printable IP into an unpacked binary string * * @author Mike Mackintosh - mike@bakeryphp.com * @param string $ip * @return string $bin */ function dtr_pton( $ip ){ if(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ return current( unpack( "A4", inet_pton( $ip ) ) ); } elseif(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){ return current( unpack( "A16", inet_pton( $ip ) ) ); } throw new \Exception("Please supply a valid IPv4 or IPv6 address"); }
Функция же dtr_ntop
преобразует результат выполнения dtr_pton
обратно. Она также проверяет входящее значение на соответствие форматам A4/A16
и возвращает результат либо false/throw в случае возникновения ошибки.
/** * dtr_ntop * * Converts an unpacked binary string into a printable IP * * @author Mike Mackintosh - mike@bakeryphp.com * @param string $str * @return string $ip */ function dtr_ntop( $str ){ if( strlen( $str ) == 16 OR strlen( $str ) == 4 ){ return inet_ntop( pack( "A".strlen( $str ) , $str ) ); } throw new \Exception( "Please provide a 4 or 16 byte string" ); }
Вот несколько примеров для работы с описанными функциями. Для начала попробуем преобразовать IPv6 туда и обратно:
try{ var_dump( dtr_ntop( dtr_pton( "fe80:1:2:3:a:bad:1dea:dad") ) ); // Returns: 'string(25) "fe80:1:2:3:a:bad:1dea:dad"' } catch(\Exception $e){ echo $e->getMessage(). "\n"; }
Проделаем тоже самое с IPv4:
try{ var_dump( dtr_ntop( dtr_pton( "10.22.99.129") ) ); // Returns: 'string(12) "10.22.99.129"' } catch(\Exception $e){ echo $e->getMessage(). "\n"; }
Вот что будет если на вход dtr_ntop
дать невалидное значение (мы добавили еще один байт к распакованному значению):
try{ var_dump( dtr_ntop( dtr_pton( "10.22.99.129").'a' ) ); // String too short: Throws 'Please provide a 4 or 16 byte string' } catch(\Exception $e){ echo $e->getMessage(). "\n"; }
Теперь попробуем преобразовать невалидный IP адрес:
try{ var_dump( dtr_ntop( dtr_pton( "ffff:feee:fg::") ) ); // Invalid IP: Throws 'Please supply a valid IPv4 or IPv6 address' } catch(\Exception $e){ echo $e->getMessage(). "\n"; }
Битовые операции
Вот несколько формул, используемых для расчета основных параметров сети:
v4 Subnet Mask: long2ip( ((1<<32) -1) << (32 - CIDR ) ) v4 Wildcard: long2ip( ~(((1<<32) -1) << (32 - CIDR )) ) Network: IP Address & Mask Broadcast: IP Address | ~Mask Available Hosts: Broadcast - Network -1 v4 Available Networks: 2^24 - Available Hosts +2
Если вы изучили и понимаете вышенаписанное, то можно приступить к получению сетевых параметров из адреса IP и CIDR.
Используя функции dtr_pton
и dtr_ntop
мы можем вычислить адрес подсети и широковещательный адрес например для 10.22.99.199/28
:
$ip = dtr_pton("10.22.99.199"); $mask = dtr_pton(long2ip( ((1<<32) -1) << (32 - 28 ) )); var_dump( dtr_ntop( $ip & $mask ) ); var_dump( dtr_ntop( $ip | ~ $mask ) );
Это выведет:
string(12) "10.22.99.192"
string(12) "10.22.99.207"
Теперь возьмем IPv6, fe80:1:2:3:a:bad:1dea:dad/82
:
$ip = dtr_pton("fe80:1:2:3:a:bad:1dea:dad"); $mask = dtr_pton("ffff:ffff:ffff:ffff:ffff:fff0::"); var_dump( dtr_ntop( $ip & $mask ) ); var_dump( dtr_ntop( $ip | ~ $mask ) );
Выведет:
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,5 года назад впервые посетил этот сайт и нашёл совершенно заброшенным, но добавил в закладки. И вот теперь, через 1,5 года зайдя сюда снова, приятно удивлён возрождением активности. Желаю автору и в дальнейшем продолжать этот интересный блог.
Благодарю) Да, я действительно опомнился, и начал снова сюда писать) Теперь можете заходить почаще чем раз в полтора года, у меня есть куча идей для последующих записей.
PS: если честно, вы мне аж прям настроение подняли) еще раз спасибо
C ipv6 все намного сложнее получается, и как то непонятно чтоли… Впрочем почерпнул для себя фишку с filter_var — намного лучше чем городить регулярки.
PS: у вас подсказки голосовалки «Оцените статью:» на русский не переведены ;)
Да я вот с трудом представлял с какой стороны подходить к ipv6, пришлось вот статейку английскую нагуглить, да и перевел заодно.
Насчет подсказок знаю, все никак руки не дойдут перевести) спасибо
Ну, регулярками я никогда не пользовался, конечно. Ибо связка ip2long -> long2ip работает быстрее и надёжнее.
Но фишка с filter_var явилась для меня новостью и действительно бесподобна. Работает почти втрое шустрее других вариантов и на порядок нагляднее.
Век живи — век учись :)
Здравствуйте! Извините, если мой вопрос покажется слишком примитивным. Я пишу функцию, которая имеет два входных параметра — начальный IP и конечный IP. IP адреса могут быть ipv4, ipv6.
Функция должна выводить список всех IP адресов, которые помещаются в указанный диапазон. С IPV4 — никаких проблем не было, а вот с IPV6 вывод не получается. Вот код функции, который выполняется правильно, для IPV4:
Для IPV6 я попробовал, для начала, с помощью inet_pton() перевести входные параметры в двоичные числа. Но вместо двоичных чисел получил какую-то хр*нь.
Может быть, Вы объясните чайнику, как реализовать оставшуюся часть функции.
Спасибо за статью, и извините, если время отнял.
Вместо двоичных чисел вы получили байтовое представление IP адреса. Т.е. чтобы получить двоичное представление из этой строки вам нужно каждый байт преобразовать в двоичный вид.
А вообще по поводу вашей задачи — тут не все так просто. PHP не может оперировать с представлением IPv6 в виде числа даже на 64 битовых системах. Одно из решений ставить на сервер http://php.net/manual/ru/book.gmp.php либо же делать аналог своими средставами (а именно функции сравнения, сложения, вычитания).
Спасибо, что прояснили ситуацию! Просто человек, который выдал это задание, сказал, что его выполнение займёт полчаса. Для IPV4 заняло 15 минут. А с IPV6 уже несколько дней возился, потом решил Вам написать. Спасибо ещё раз.
Рад что смог помочь. С ipv6 работы не на полчаса, а однозначно поболее) хотя зря вы ждали несколько дней, надо было сразу мне написать
Здравствуйте! Можете привести пример как делать выборки в БД по диапазонам IPv6, используя BINARY(16)?
Заранее спасибо.
Уже понял, можете удалить комментарий.
привет. спасибо за статью. у меня прекрасно работает всё без использования pack.
например, эти значения равны:
здесь побитовые операции тоже выполняются корректно:
Цитирование порезало код. Но прочитать несложно
if (inet_pton( $ip6 ) == current(unpack( «A16», inet_pton( $ip6 ) ))) {
echo ‘==’;
} else {
echo ‘!=’;
}
——————————————————
$ip = inet_pton(«fe80:1:2:3:a:bad:1dea:dad»);
$mask = inet_pton(«ffff:ffff:ffff:ffff:ffff:fff0::»);
echo »;
var_dump( inet_ntop( $ip & $mask ) );
echo »;
var_dump( inet_ntop( $ip | ~ $mask ) );