Любое приложение, которое хоть как‑то работает с сетью, должно валидировать правильность IP-адресов. Это сложнее, чем может показаться. Здесь легко впасть в крайности: при излишне строгой валидации пользователь не сможет ввести верные данные, при недостаточной — окажется наедине с низкоуровневыми сообщениями об ошибках (если они вообще передаются).
В этой статье мы разберем ряд сложностей, возникающих при валидации адресов, а потом посмотрим на готовые библиотеки, которые с этим помогают.
РЕКОМЕНДУЕМ:
Сетевые утилиты командной строки
Валидация адресов
Ошибки в адресах могут появиться тремя способами:
- опечатки;
- недопонимание;
- намеренные попытки сломать приложение.
От попыток сломать приложение одна валидация адресов не поможет. Она может затруднить такие попытки, но не заменит полноценную проверку авторизации и обработку ошибок на всех этапах работы программы, так что улучшение безопасности нужно рассматривать скорее как полезный побочный эффект. Основная цель — упростить жизнь пользователям, которые случайно ввели неверный адрес или неправильно поняли, что от них требуется.
Проверки можно условно разделить на проверки по форме и по существу. Цель формальной проверки — убедиться, что введенная пользователем строка вообще может быть допустимым адресом. Многие программы ограничиваются именно этим. Мы же пойдем дальше и посмотрим, как можно проверять, что адрес не только правильный, но и подходящий для конкретной цели, но об этом позже.
Проверки по форме
Проверка правильности формата только на вид может показаться задачей для несложного регулярного выражения — на деле все не так просто.
В IPv4 сложности начинаются со стандарта на этот формат — такого стандарта не существует. Формат dot-decimal ( 0.<wbr />0.<wbr />0.<wbr />0–255.<wbr />255.<wbr />255.<wbr />255) — общепринятый, но не стандартный. Стандарт IPv4 не содержит никаких упоминаний о формате записи адресов вообще. Никакой другой RFC тоже ничего не говорит о формате адресов IPv4, так что общепринятый формат — это не более чем соглашение.
И это даже не единственное соглашение. Функция inet_aton(<wbr />) позволяет не писать нулевые разряды в конце адреса, например 192.<wbr />0.<wbr />2 <wbr />= <wbr />192.<wbr />0.<wbr />2.<wbr />0. Кроме того, она позволяет вводить адрес одним целым числом, 511 <wbr />= <wbr />0.<wbr />0.<wbr />1.<wbr />255.
Может ли адрес хоста заканчиваться на ноль? Конечно, может — в любой сети размером больше /23 найдется хотя бы один такой. Например, 192.<wbr />168.<wbr />0.<wbr />0/<wbr />23 содержит адреса хостов 192.<wbr />168.<wbr />0.<wbr />1–192.<wbr />168.<wbr />1.<wbr />254, включая 192.<wbr />168.<wbr />1.<wbr />0.
Если ограничиться поддержкой только полного dot-decimal из четырех групп, без возможности опускать нулевые разряды, то выражение (\<wbr />d+)\.(\<wbr />d+)\.(\<wbr />d+)\.(\<wbr />d+) может поймать значительную часть опечаток. Если задаться целью, можно составить выражение для любого допустимого адреса, хотя оно и будет довольно громоздким. Лучше воспользоваться тем, что его легко разделить на группы, и явно проверить, что каждая из них попадает в диапазон 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. О любых альтернативных форматах можно смело говорить, что они против стандарта, и игнорировать. С другой стороны, сами форматы сложнее. Основную сложность представляет сокращенная запись: группы нулевых разрядов можно заменять на символ :<wbr />:, например 2001:<wbr />db8::<wbr />1 вместо 2001:<wbr />db8:<wbr />0:<wbr />0:<wbr />0:<wbr />0:<wbr />0:<wbr />1. Для пользователя это, безусловно, удобно, но для разработчика все ровно наоборот: разделить адрес на группы по двоеточию невозможно, нужна заметно более сложная логика. К тому же стандарт запрещает использовать :<wbr />: больше одного раза в одном адресе, что еще сильнее усложняет задачу.
РЕКОМЕНДУЕМ:
Выбор оборудования для создания скоростной домашней сети
Так что, если приложение поддерживает IPv6, для валидации адресов нужен полноценный парсер. Писать его самим нет смысла, поскольку существуют готовые библиотеки, которые предоставляют и другие полезные функции.
Проверки по существу
Если уж мы взялись подключать библиотеку и парсить адреса, давай посмотрим, какие дополнительные проверки мы можем провести, чтобы отсеять ошибочные значения и сделать сообщения об ошибках более информативными.
Нужные проверки будут зависеть от того, как будет использоваться адрес. Например, пусть пользователь хотел ввести в поле адреса сервера DNS значение 124.<wbr />1.<wbr />2.<wbr />3, но опечатка превратила его в 224.<wbr />1.<wbr />2.<wbr />3. Проверка формата эту опечатку не поймает — формат правильный. Однако этот адрес никак не может быть адресом сервера DNS, поскольку сеть 224.<wbr />0.<wbr />0.<wbr />0/<wbr />4 зарезервирована для многоадресной маршрутизации, которую DNS не использует никогда.
Если ты хочешь отсеять все адреса, которые не могут быть адресами хостов в публичном интернете, почти полный список зарезервированных сетей можно найти в RFC 5735 (Special use IPv4 addresses). «Почти полный» он потому, что не включает сеть 100.<wbr />64.<wbr />0.<wbr />0/<wbr />10, выделенную для CG-NAT (RFC 6598). Совсем полный список всех зарезервированных диапазонов IPv4 и IPv6 можно найти в RFC 6890, однако он не так удобно организован.
При этом нужно обратить внимание на маски подсетей. Некоторые полагают, что сеть для частного использования — 172.<wbr />16.<wbr />0.<wbr />0/<wbr />16 (<wbr />172.<wbr />16.<wbr />0.<wbr />0–172.<wbr />16.<wbr />255.<wbr />255). Чтение RFC5735 легко развеет этот миф: на самом деле она заметно больше, 172.<wbr />16.<wbr />0.<wbr />0/<wbr />12 (<wbr />172.<wbr />16.<wbr />0.<wbr />1–172.<wbr />31.<wbr />255.<wbr />254). Реальный пример этой ошибки в GoatCounter — скрипт сбора статистики ошибочно считал посещения изнутри локальной сети.
Нужно также учитывать, что «зарезервированные для использования в будущем» сети могут перестать быть зарезервированными. Сети из RFC 5735 зарезервированы навсегда и в этом смысле безопасны. А вот авторы некогда популярной среди геймеров виртуальной сети Hamachi когда‑то считали, что сеть 5.<wbr />0.<wbr />0.<wbr />0/<wbr />8 можно использовать для своих нужд, потому что она была зарезервирована для будущего использования, — пока будущее не наступило и 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.<wbr />c.
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 |
Заключение
Валидация адресов и выдача информативных сообщений об ошибочных настройках вроде бы незначительная часть интерфейса, но внимание к деталям — признак профессионализма, тем более что готовые библиотеки существенно упрощают эту задачу.