Любое приложение, которое хоть как‑то работает с сетью, должно валидировать правильность IP-адресов. Это сложнее, чем может показаться. Здесь легко впасть в крайности: при излишне строгой валидации пользователь не сможет ввести верные данные, при недостаточной — окажется наедине с низкоуровневыми сообщениями об ошибках (если они вообще передаются). В этой статье мы разберем ряд сложностей, возникающих при валидации адресов, а потом посмотрим на готовые библиотеки, которые с этим помогают.
ВАЛИДАЦИЯ АДРЕСОВ
Ошибки в адресах могут появиться тремя способами:
- опечатки;
- недопонимание;
- намеренные попытки сломать приложение.
От попыток сломать приложение одна валидация адресов не поможет. Она может затруднить такие попытки, но не заменит полноценную проверку авторизации и обработку ошибок на всех этапах работы программы, так что улучшение безопасности нужно рассматривать скорее как полезный побочный эффект. Основная цель — упростить жизнь пользователям, которые случайно ввели неверный адрес или неправильно поняли, что от них требуется.
Проверки можно условно разделить на проверки по форме и по существу. Цель формальной проверки — убедиться, что введенная пользователем строка вообще может быть допустимым адресом. Многие программы ограничиваются именно этим. Мы же пойдем дальше и посмотрим, как можно проверять, что адрес не только правильный, но и подходящий для конкретной цели, но об этом позже.
Проверки по форме
Проверка правильности формата только на вид может показаться задачей для несложного регулярного выражения — на деле все не так просто.
В IPv4 сложности начинаются со стандарта на этот формат — такого стандарта не существует. Формат dot-decimal (
0.0.0.0–255.255.255.255
) — общепринятый, но не стандартный.
И это даже не единственное соглашение. Функция
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 и добавили формат записи адресов в ::
, например
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
зарезервирована для
Если ты хочешь отсеять все адреса, которые не могут быть адресами хостов в публичном интернете, почти полный список зарезервированных сетей можно найти в 100.64.0.0/10
, выделенную для CG-NAT (
При этом нужно обратить внимание на маски подсетей. Некоторые полагают, что сеть для частного использования —
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)
. Реальный пример этой ошибки в
Нужно также учитывать, что «зарезервированные для использования в будущем» сети могут перестать быть зарезервированными. Сети из RFC 5735 зарезервированы навсегда и в этом смысле безопасны. А вот авторы некогда популярной среди геймеров виртуальной сети Hamachi когда‑то считали, что сеть
5.0.0.0/8
можно использовать для своих нужд, потому что она была зарезервирована для будущего использования, — пока будущее не наступило и IANA не выделила эту сеть RIPE.
БИБЛИОТЕКИ
netaddr
В стандартной библиотеке Python 3 уже есть модуль
ipaddress
, но, если есть возможность поставить стороннюю библиотеку,
>>> 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
Даже если бы этих функций не было, мы могли бы легко реализовать их сами. Библиотека очень грамотно использует 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
Даже для чистого С можно найти библиотеку с удобным интерфейсом, такую как 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
ЗАКЛЮЧЕНИЕ
Валидация адресов и выдача информативных сообщений об ошибочных настройках вроде бы незначительная часть интерфейса, но внимание к деталям — признак профессионализма, тем более что готовые библиотеки существенно упрощают эту задачу.