IP без ошибок. Изучаем принципы работы с IP-адресами
Любое приложение, которое хоть как‑то работает с сетью, должно валидировать правильность IP-адресов. Это сложнее, чем может показаться. Здесь легко впасть в крайности: при излишне строгой валидации пользователь не сможет ввести верные данные, при недостаточной — окажется наедине с низкоуровневыми сообщениями об ошибках (если они вообще передаются). В этой статье мы разберем ряд сложностей, возникающих при валидации адресов, а потом посмотрим на готовые библиотеки, которые с этим помогают.
ВАЛИДАЦИЯ АДРЕСОВ
Ошибки в адресах могут появиться тремя способами:
- опечатки;
- недопонимание;
- намеренные попытки сломать приложение.
От попыток сломать приложение одна валидация адресов не поможет. Она может затруднить такие попытки, но не заменит полноценную проверку авторизации и обработку ошибок на всех этапах работы программы, так что улучшение безопасности нужно рассматривать скорее как полезный побочный эффект. Основная цель — упростить жизнь пользователям, которые случайно ввели неверный адрес или неправильно поняли, что от них требуется.
Проверки можно условно разделить на проверки по форме и по существу. Цель формальной проверки — убедиться, что введенная пользователем строка вообще может быть допустимым адресом. Многие программы ограничиваются именно этим. Мы же пойдем дальше и посмотрим, как можно проверять, что адрес не только правильный, но и подходящий для конкретной цели, но об этом позже.
Проверки по форме
Проверка правильности формата только на вид может показаться задачей для несложного регулярного выражения — на деле все не так просто.
В IPv4 сложности начинаются со стандарта на этот формат — такого стандарта не существует. Формат dot-decimal (0.0.0.0–255.255.255.255
) — общепринятый, но не стандартный. Стандарт IPv4 не содержит никаких упоминаний о формате записи адресов вообще. Никакой другой RFC тоже ничего не говорит о формате адресов IPv4, так что общепринятый формат — это не более чем соглашение.
И это даже не единственное соглашение. Функция inet_aton()
позволяет не писать нулевые разряды в конце адреса, например 192.0.2 = 192.0.2.0
. Кроме того, она позволяет вводить адрес одним целым числом, 511 = 0.0.1.255
.
INFO
Может ли адрес хоста заканчиваться на ноль? Конечно, может — в любой сети размером больше /23 найдется хотя бы один такой. Например, 192.168.0.0/23
содержит адреса хостов 192.168.0.1–192.168.1.254
, включая 192.168.1.0
.
Если ограничиться поддержкой только полного dot-decimal из четырех групп, без возможности опускать нулевые разряды, то выражение (\d+)\.(\d+)\.(\d+)\.(\d+)
может поймать значительную часть опечаток. Если задаться целью, можно составить выражение для любого допустимого адреса, хотя оно и будет довольно громоздким. Лучше воспользоваться тем, что его легко разделить на группы, и явно проверить, что каждая из них попадает в диапазон 0–255:
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:db8::1
вместо 2001:db8:0:0:0:0:0:1
. Для пользователя это, безусловно, удобно, но для разработчика все ровно наоборот: разделить адрес на группы по двоеточию невозможно, нужна заметно более сложная логика. К тому же стандарт запрещает использовать ::
больше одного раза в одном адресе, что еще сильнее усложняет задачу.
Так что, если приложение поддерживает IPv6, для валидации адресов нужен полноценный парсер. Писать его самим нет смысла, поскольку существуют готовые библиотеки, которые предоставляют и другие полезные функции.
Проверки по существу
Если уж мы взялись подключать библиотеку и парсить адреса, давай посмотрим, какие дополнительные проверки мы можем провести, чтобы отсеять ошибочные значения и сделать сообщения об ошибках более информативными.
Нужные проверки будут зависеть от того, как будет использоваться адрес. Например, пусть пользователь хотел ввести в поле адреса сервера DNS значение 124.1.2.3
, но опечатка превратила его в 224.1.2.3
. Проверка формата эту опечатку не поймает — формат правильный. Однако этот адрес никак не может быть адресом сервера DNS, поскольку сеть 224.0.0.0/4
зарезервирована для многоадресной маршрутизации, которую DNS не использует никогда.
Если ты хочешь отсеять все адреса, которые не могут быть адресами хостов в публичном интернете, почти полный список зарезервированных сетей можно найти в RFC 5735 (Special use IPv4 addresses). «Почти полный» он потому, что не включает сеть 100.64.0.0/10
, выделенную для CG-NAT (RFC 6598). Совсем полный список всех зарезервированных диапазонов IPv4 и IPv6 можно найти в RFC 6890, однако он не так удобно организован.
При этом нужно обратить внимание на маски подсетей. Некоторые полагают, что сеть для частного использования — 172.16.0.0/16 (172.16.0.0–172.16.255.255)
. Чтение RFC5735 легко развеет этот миф: на самом деле она заметно больше, 172.16.0.0/12 (172.16.0.1–172.31.255.254)
. Реальный пример этой ошибки в GoatCounter — скрипт сбора статистики ошибочно считал посещения изнутри локальной сети.
Нужно также учитывать, что «зарезервированные для использования в будущем» сети могут перестать быть зарезервированными. Сети из RFC 5735 зарезервированы навсегда и в этом смысле безопасны. А вот авторы некогда популярной среди геймеров виртуальной сети Hamachi когда‑то считали, что сеть 5.0.0.0/8
можно использовать для своих нужд, потому что она была зарезервирована для будущего использования, — пока будущее не наступило и IANA не выделила эту сеть RIPE.
БИБЛИОТЕКИ
netaddr
В стандартной библиотеке Python 3 уже есть модуль ipaddress
, но, если есть возможность поставить стороннюю библиотеку, netaddr может сильно упростить жизнь. К примеру, в ней есть встроенные функции для проверки принадлежности адреса к зарезервированным диапазонам.
>>> 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
, так что работать с ними не сложнее, чем со списками или словарями.
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.c
.
#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");
}
}
$ 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
ЗАКЛЮЧЕНИЕ
Валидация адресов и выдача информативных сообщений об ошибочных настройках вроде бы незначительная часть интерфейса, но внимание к деталям — признак профессионализма, тем более что готовые библиотеки существенно упрощают эту задачу.