Любое приложение, которое хоть как‑то работает с сетью, должно валидировать правильность IP-адресов. Это сложнее, чем может показаться. Здесь легко впасть в крайности: при излишне строгой валидации пользователь не сможет ввести верные данные, при недостаточной — окажется наедине с низкоуровневыми сообщениями об ошибках (если они вообще передаются).
В этой статье мы разберем ряд сложностей, возникающих при валидации адресов, а потом посмотрим на готовые библиотеки, которые с этим помогают.
РЕКОМЕНДУЕМ:
Сетевые утилиты командной строки
Валидация адресов
Ошибки в адресах могут появиться тремя способами:
- опечатки;
- недопонимание;
- намеренные попытки сломать приложение.
От попыток сломать приложение одна валидация адресов не поможет. Она может затруднить такие попытки, но не заменит полноценную проверку авторизации и обработку ошибок на всех этапах работы программы, так что улучшение безопасности нужно рассматривать скорее как полезный побочный эффект. Основная цель — упростить жизнь пользователям, которые случайно ввели неверный адрес или неправильно поняли, что от них требуется.
Проверки можно условно разделить на проверки по форме и по существу. Цель формальной проверки — убедиться, что введенная пользователем строка вообще может быть допустимым адресом. Многие программы ограничиваются именно этим. Мы же пойдем дальше и посмотрим, как можно проверять, что адрес не только правильный, но и подходящий для конкретной цели, но об этом позже.
Проверки по форме
Проверка правильности формата только на вид может показаться задачей для несложного регулярного выражения — на деле все не так просто.
В IPv4 сложности начинаются со стандарта на этот формат — такого стандарта не существует. Формат dot-decimal (0.
) — общепринятый, но не стандартный. Стандарт IPv4 не содержит никаких упоминаний о формате записи адресов вообще. Никакой другой RFC тоже ничего не говорит о формате адресов IPv4, так что общепринятый формат — это не более чем соглашение.
И это даже не единственное соглашение. Функция inet_aton(
позволяет не писать нулевые разряды в конце адреса, например 192.
. Кроме того, она позволяет вводить адрес одним целым числом, 511
.
Может ли адрес хоста заканчиваться на ноль? Конечно, может — в любой сети размером больше /23 найдется хотя бы один такой. Например,
192.
содержит адреса хостов168. 0. 0/ 23 192.
, включая168. 0. 1–192. 168. 1. 254 192.
.168. 1. 0
Если ограничиться поддержкой только полного dot-decimal из четырех групп, без возможности опускать нулевые разряды, то выражение (\
может поймать значительную часть опечаток. Если задаться целью, можно составить выражение для любого допустимого адреса, хотя оно и будет довольно громоздким. Лучше воспользоваться тем, что его легко разделить на группы, и явно проверить, что каждая из них попадает в диапазон 0–255:
1 2 3 4 5 6 7 |
def check_ipv4(s): groups = s.split('.') if len(groups) != 4: for g in groups: num = int(g) if (num > 255) or (num < 0): raise ValueError("Invalid octet value") |
С IPv6 все одновременно проще и сложнее. Проще потому, что авторы IPv6 учли опыт IPv4 и добавили формат записи адресов в RFC 4291. О любых альтернативных форматах можно смело говорить, что они против стандарта, и игнорировать. С другой стороны, сами форматы сложнее. Основную сложность представляет сокращенная запись: группы нулевых разрядов можно заменять на символ :
, например 2001:
вместо 2001:
. Для пользователя это, безусловно, удобно, но для разработчика все ровно наоборот: разделить адрес на группы по двоеточию невозможно, нужна заметно более сложная логика. К тому же стандарт запрещает использовать :
больше одного раза в одном адресе, что еще сильнее усложняет задачу.
РЕКОМЕНДУЕМ:
Выбор оборудования для создания скоростной домашней сети
Так что, если приложение поддерживает IPv6, для валидации адресов нужен полноценный парсер. Писать его самим нет смысла, поскольку существуют готовые библиотеки, которые предоставляют и другие полезные функции.
Проверки по существу
Если уж мы взялись подключать библиотеку и парсить адреса, давай посмотрим, какие дополнительные проверки мы можем провести, чтобы отсеять ошибочные значения и сделать сообщения об ошибках более информативными.
Нужные проверки будут зависеть от того, как будет использоваться адрес. Например, пусть пользователь хотел ввести в поле адреса сервера DNS значение 124.
, но опечатка превратила его в 224.
. Проверка формата эту опечатку не поймает — формат правильный. Однако этот адрес никак не может быть адресом сервера DNS, поскольку сеть 224.
зарезервирована для многоадресной маршрутизации, которую DNS не использует никогда.
Если ты хочешь отсеять все адреса, которые не могут быть адресами хостов в публичном интернете, почти полный список зарезервированных сетей можно найти в RFC 5735 (Special use IPv4 addresses). «Почти полный» он потому, что не включает сеть 100.
, выделенную для CG-NAT (RFC 6598). Совсем полный список всех зарезервированных диапазонов IPv4 и IPv6 можно найти в RFC 6890, однако он не так удобно организован.
При этом нужно обратить внимание на маски подсетей. Некоторые полагают, что сеть для частного использования — 172.
. Чтение RFC5735 легко развеет этот миф: на самом деле она заметно больше, 172.
. Реальный пример этой ошибки в GoatCounter — скрипт сбора статистики ошибочно считал посещения изнутри локальной сети.
Нужно также учитывать, что «зарезервированные для использования в будущем» сети могут перестать быть зарезервированными. Сети из RFC 5735 зарезервированы навсегда и в этом смысле безопасны. А вот авторы некогда популярной среди геймеров виртуальной сети Hamachi когда‑то считали, что сеть 5.
можно использовать для своих нужд, потому что она была зарезервирована для будущего использования, — пока будущее не наступило и IANA не выделила эту сеть RIPE.
Библиотеки
netaddr
В стандартной библиотеке Python 3 уже есть модуль ipaddress
, но, если есть возможность поставить стороннюю библиотеку, netaddr может сильно упростить жизнь. К примеру, в ней есть встроенные функции для проверки принадлежности адреса к зарезервированным диапазонам.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> import netaddr >>> def is_public_ip(s): ... ip = netaddr.IPAddress(s) ... return (ip.is_unicast() and not ip.is_private() and not ip.is_reserved()) ... >>> is_public_ip('192.0.2.1') # Reserved for documentation False >>> is_public_ip('172.16.1.2') # Reserved for private networks False >>> is_public_ip('224.0.0.5') # Multicast False >>> is_public_ip('8.8.8.8') True |
Даже если бы этих функций не было, мы могли бы легко реализовать их сами. Библиотека очень грамотно использует магические методы, чтобы сделать интерфейс таким же удобным, как у встроенных объектов Python. Например, проверку принадлежности адреса к сети или диапазону можно выполнить оператором in
, так что работать с ними не сложнее, чем со списками или словарями.
1 2 3 4 5 6 7 8 9 10 |
def is_public_ip(s): loopback_net = netaddr.IPNetwork('127.0.0.0/8') multicast_net = netaddr.IPNetwork('224.0.0.0/4') ... ip = netaddr.IPAddress(s) if ip in multicast_net: raise ValueError("Multicast address found") elif ip in loopback_net: raise ValueError("Loopback address found") ... |
libcidr
Даже для чистого С можно найти библиотеку с удобным интерфейсом, такую как libcidr Мэттью Фуллера. В Debian ее можно поставить из репозиториев. Для примера напишем проверку принадлежности адреса к сети multicast и положим ее в файл is_multicast.
.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> #include <libcidr.h> void main(int argc, char** argv) { const char* ipv4_multicast_net = "224.0.0.0/4"; CIDR* ip = cidr_from_str(argv[1]); CIDR* multicast_net = cidr_from_str(ipv4_multicast_net); if( cidr_contains(multicast_net, ip) == 0 ) { printf("The argument is an IPv4 multicast address\n"); } else { printf("The argument is not an IPv4 multicast address\n"); } } |
1 2 3 4 5 6 |
$ sudo aptitude install libcidr-dev $ gcc -o is_multicast -lcidr ./is_multicast.c $ ./is_multicast 8.8.8.8 The argument is not an IPv4 multicast address $ ./is_multicast 239.1.2.3 The argument is an IPv4 multicast address |
Заключение
Валидация адресов и выдача информативных сообщений об ошибочных настройках вроде бы незначительная часть интерфейса, но внимание к деталям — признак профессионализма, тем более что готовые библиотеки существенно упрощают эту задачу.