Мне просто нужно было взять гипертекст и объединить его с идеями TCP и DNS, и — та-дам! — Всемирная паутина готова.
Тим Бернерс-Ли, «Answers for Young People», 1989
Данная глава описывает работу с адресами, доменными именами и системой DNS, которая играет ключевую роль в получении доступа к ресурсам в интернете. В процессе рассмотрения этой темы мы изучим хранение IP-адресов в системе, а также процессы конвертации IP-адресов из вида, понятного человеку, в форму, понятную API.
Также разберемся с некоторыми особенностями работы поверх IPv6. Узнаем о Dual Stack и о том, как он помогает в переходе на IPv6.
Мы рассмотрим суть доменных имен и способы их преобразования в IP-адреса с помощью системы DNS и узнаем о нескольких функциях, которые позволяют тонко настроить работу DNS.
Эти знания будут весьма полезны как для программирования, так и для администрирования сетей. В главе представлен пример реализации преобразования имен в адреса и обратно в обертке над сокетами — примерно так это может быть реализовано и в библиотеках.
Обычно пользователь вводит IP-адрес как строку в командном интерфейсе или внутри программы. Например: "192.168.2.1". В том же формате он ожидает увидеть программный вывод, например, в протоколе ошибок. Но на практике IP-адрес — это число, хранимое в структуре. И функции сокетного API требуют подстановки этой структуры.
Чтобы преобразовывать IP-адрес из строковой формы в форму, понятную остальному API, и обратно, существует набор функций. Кроме того, хотя реальные сетевые интерфейсы в интернете адресуются по IP, хорошо написанная программа наравне с адресами принимает доменные имена, такие как .
В общем случае, чтобы преобразовать доменное имя в адрес, необходимо выполнить следующее:
• Проверить статически заданные преобразования в файле hosts. Процесс обработки настраивается отдельно. В Linux, например, в /etc/host.conf.
• Если в hosts записи нет, выбрать работоспособный DNS-сервер из сконфигурированных в ОС.
• Обратиться к DNS-серверу с запросом.
Это достаточно сложный процесс, но он полностью реализован в API и вызывается функцией getaddrinfo() и ее аналогами.
Файл hosts в Unix-подобных системах расположен по пути /etc/hosts, а в ОС Windows — по пути %SystemRoot%\system32\drivers\etc\hosts.
Переменная %SystemRoot% обычно имеет значение c:\windows.
В этой главе мы рассмотрим большинство функций для работы с IP-адресами и доменными именами.
Структура sockaddr хранит адреса, привязанные к сокету, в общем для всех семейств адресов формате:
// Заголовок только для POSIX, в Windows это ws2def.h.
#include <sys/socket.h>
// Структура адреса, общая для всех.
struct sockaddr
{
// Семейство адресов, AF_xxx.
unsigned short sa_family;
// 14 байт адреса, для TCP/IP – адрес и порт. char sa_data[14];
};
Но обычно удобнее использовать структуры, специфичные для протокола. Например, для IPv4 структуры адреса таковы:
// Заголовок только для POSIX, в Windows это ws2def.h.
#include <netinet/in.h>
// Только для Internet-протокола IPv4 ("_in").
// struct sockaddr_in6 для IPv6 будет рассмотрена позже.
struct sockaddr_in
{
// Семейство адресов, AF_INET.
short int sin_family;
// Номер порта в сетевом порядке байтов.
unsigned short int sin_port;
// Internet address.
struct in_addr sin_addr;
// Заполнение для соответствия размеру struct sockaddr.
unsigned char sin_zero[8];
};
// Internet address. Исторически представляет собой структуру.
struct in_addr
{
// 32-битный int, то есть байты IPv4.
uint32_t s_addr;
};
Адрес для UNIX-сокетов, которые мы рассмотрим в следующей главе, представляет собой путь в файловой системе. Для сокетов типа AF_PACKET, используемых в Linux для работы с канальным уровнем, адрес содержит индекс устройства.
Устройство структур показано на рис. 2.1.
Рис. 2.1. Различные типы адресов
Структуры адресов можно заполнить вручную, но так делать не стоит. Например:
int main(int argc, char const *argv[])
{
const int port { std::stoi(argv[1]) };
// Аналогично int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP).
socket_wrapper::Socket sock = {AF_INET, SOCK_DGRAM, IPPROTO_UDP};
// Ручное заполнение структуры адреса.
sockaddr_in addr =
{
// Тип адреса.
.sin_family = PF_INET,
// Порт в сетевом порядке байтов.
.sin_port = htons(port),
};
// Адрес 0.0.0.0.
addr.sin_addr.s_addr = INADDR_ANY;
std::cout
<< "Starting echo server on the port "
<< port << "...\n";
// Вызов bind(), для которого структуру требуется преобразовать
// в общий тип.
if (bind(sock, reinterpret_cast<const sockaddr*>(&addr),
sizeof(addr)) != 0)
{
return EXIT_FAILURE;
}
...
Внимание! В общем случае так делать не рекомендуется, и ниже в этой главе показан более предпочтительный способ заполнения структуры.
Адрес — это число, поэтому сформировать его и заполнить структуру можно даже так:
// Адрес 192.168.1.2.
uint8_t ip0 = 192;
uint8_t ip1 = 168;
uint8_t ip2 = 1;
uint8_t ip3 = 2;
unsigned short port = 12345;
// Итоговое число.
uint32_t destination_address = (ip0 << 24) | (ip1 << 16) | (ip2 << 8) | ip3;
sockaddr_in address;
address.sin_family = AF_INET;
// Преобразуем в нужный порядок байтов.
address.sin_addr.s_addr = htonl(destination_address);
address.sin_port = htons(destination_port);
Это пример плохого стиля, и так делать неправильно. Для работы с адресами существует набор функций — используйте их, а не битовые операции с внутренней структурой адресов.
В Python все адреса представлены обычными типами Python, такими как int. Классы для адресов отсутствуют.
Разнообразие устройств, подключенных к сети, очень быстро растет, во многом из-за появления IoT-устройств. Потому интернет медленно, но верно переходит на протокол IPv6, у которого огромное пространство адресов.
Адрес IPv6 содержит 16 октетов против всего четырех в IPv4. Если отображать такие адреса в десятичной нотации с точками, он будет сложно читаемым и очень длинным. Поэтому его печатным отображением служат шестнадцатеричные числа, разделенные двоеточием, с возможными пропусками компонентов адреса.
Протокол IPv4 до сих пор широко применяется, в том числе по следующим причинам:
• Большинство оборудования настроено и проверялось на IPv4.
• Адреса IPv6 тяжело читать, несмотря на «совместимость» формата с IPv4.
• IPv4 «и так работает». А зачем менять то, что работает?
Что произошло с IPv5? Такой протокол действительно существовал, но он представлял собой протокол для работы с потоковым аудио поверх IP, более известный как Internet Stream Protocol 2, или ST-2.
О поддержке IPv6 в современных ОС, как правило, можно не волноваться: везде поддержка уже включена. Например, Linux поддерживает IPv6, начиная с версии ядра 2.2. CPython для проверки этого факта имеет константу socket.has_ipv6, сейчас безусловно установленную в True.
При необходимости можно проверить наличие атрибута IPPROTO_IPV6 у модуля socket hasattr(_socket, 'IPPROTO_IPV6').
Так как IPv6 — протокол сетевого уровня, переход с IPv4 не сильно повлияет на особенности работы сокетов. Чтобы понять, что изменилось, стоит обратиться к man 7 ipv6.
Там говорится следующее:
• Добавились новые константы: пространство адресов AF_INET6.
• Протокол имеет несколько своих опций, что логично.
• Изменилась структура адреса, что следует из описания протокола.
Структура адреса для IPv6 выглядит так:
struct sockaddr_in6
{
// AF_INET6.
sa_family_t sin6_family;
// Номер порта.
in_port_t sin6_port;
// Метка потока IPv6.
// То есть пакетов от источника к назначению,
// отправляемых с конкретной целью.
// Обрабатывается на маршрутизаторах специальным образом.
uint32_t sin6_flowinfo;
// Адрес IPv6.
struct in6_addr sin6_addr;
// Scope ID.
uint32_t sin6_scope_id;
// В Windows поле заменяется следующим объединением:
// union
// {
// ULONG sin6_scope_id;
// SCOPE_ID sin6_scope_struct;
// };
};
Атрибут scope_id — это идентификатор топологической области, в которой IPv6-адрес может использоваться в качестве уникального идентификатора интерфейса либо набора интерфейсов. Адрес может иметь локальную или глобальную область действия.
Поскольку несколько сетевых интерфейсов могут иметь одинаковый IPv6-адрес, scope_id может идентифицировать сетевой интерфейс.
Некоторые дополнительные структуры:
struct in6_addr
{
// Адрес IPv6.
unsigned char s6_addr[16];
};
// Только для Windows:
typedef struct
{
union
{
struct
{
// Зона области администрирования.
ULONG Zone : 28;
ULONG Level : 4;
};
ULONG Value;
};
} SCOPE_ID, *PSCOPE_ID;
Зона области администрирования IPv6 используется для работы групп многоадресной рассылки. Пакеты многоадресной рассылки для таких групп ограничены локальной зоной администрирования, то есть не могут пересекать границу зоны.
В Python адрес IPv6 представляет собой кортеж следующего вида:
tuple(host, port, flowinfo, scope_id)
где flowinfo и scope_id аналогичны членам sin6_flowinfo и sin6_scope_id описанной выше структуры sockaddr_in6. При использовании большинства функций и классов модуля socket эти параметры можно опустить в целях обратной совместимости.
Пропуск scope_id может вызвать проблемы при работе с адресами IPv6, которые содержат область действия, так как в результате может быть некорректно выбран интерфейс для исходящих пакетов.
В IPv4 тоже существует проблема выбора интерфейса среди имеющих одинаковые адреса. Но как она должна решаться, не специфицировано.
Размер адреса по сравнению с IPv4 увеличен, и неясно, какой тип адреса использует клиентская сторона, поэтому потребовалась новая структура, которая может принять любой адрес в accept(). Эта структура называется sockaddr_storage и определяется следующим образом:
#include <sys/socket.h>
struct sockaddr_storage
{
// Семейство адресов.
sa_family_t ss_family;
// Выравнивание, зависящее от реализации.
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
В Windows определена в файле ws2def.h, а на Windows Server 2003 и XP — в winsock2.h.
Некоторые приложения, в том числе примеры, не используют ее. Это говорит о том, что они написаны без достаточной поддержки IPv6. Подобное часто встречается в исходном коде приложений.
При реализации нового приложения лучше сразу закладывать поддержку IPv6. Еще лучше — использовать библиотеки, о которых будет рассказано в следующих книгах.
Сокет, созданный функцией socket(), идентифицируется только по домену, семейству протоколов и конкретному протоколу. Но для того, чтобы подключиться к сокету извне либо организовать подключение с использованием сокета, необходимо привязать к сокету адрес.
Привязка адреса в общем случае означает привязку не IP, а адреса, определяемого доменом. Например, для Unix-сокетов это путь к файлу.
Для TCP, SCTP и UDP-сокетов привязка адреса будет означать привязку:
• IP-адреса;
• порта.
Если сокет ориентирован на соединение, у него имеется два адреса: локальный и адрес пира. Пока мы будем говорить о привязке локального адреса. Сокеты, ориентированные на соединения, мы более подробно изучим в главе 5.
Локальный адрес может быть привязан явно и неявно. В случае подключения к «серверу», например, адрес и порт будут назначены сокету автоматически. Но если сокет является прослушивающим или требуется привязать лишь один из возможных адресов, сокетное API предоставляет возможности для явной привязки адреса.
Данная функция явно привязывает к сокету локальный адрес и объявлена в файле sys/socket.h.
Внимание! Функция bind() чаще всего применяется для привязки адреса и порта сервера. Для клиента используйте функцию connect(), описанную в главе 3, либо явное указание адреса в recvfrom() и sendto().
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
Параметры функции:
• socket — дескриптор сокета;
• address — указатель на адрес для привязки;
• address_len — размер структуры адреса: sizeof(sockaddr).
Возвращаемое значение — 0 при успехе либо –1 при неудаче, в ОС Windows — INVALID_SOCKET.
В случае ошибки bind() сокет может оставаться в разном состоянии. Например, в Linux версии ядра 2.4 и ниже сокет требовалось пересоздать. В новых версиях ядра Linux ошибка bind() не ведет к порче сокета, и поэтому на одном и том же сокете его можно вызывать многократно.
В других ОС поведение сокета после ошибки вызова bind() может отличаться.
Рассмотрим пример:
#if not defined(INVALID_SOCKET)
# define INVALID_SOCKET (-1)
#endif
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET == sock)
{
// Ошибка при создании.
std::cerr
<< sock_wrap.get_last_error_string()
<< std::endl;
return EXIT_FAILURE;
}
// Явное заполнение структуры адреса.
// Не делайте так!
// Подробнее см. далее.
sockaddr_in addr =
{
.sin_family = PF_INET,
.sin_port = htons(8080),
};
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) != 0)
{
// Требуется явно закрыть сокет, пусть даже ОС его сама закроет и удалит
// при выходе из программы.
// Это хороший стиль, который способствует созданию повторно
// используемого кода.
close(sock);
throw std::runtime_error("Bind error");
}
В Python данная функция является методом класса socket.socket:
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
socket.IPPROTO_UDP)
# Привязать адрес, в данном случае IP+порт.
s.bind(('127.0.0.1', 8080))
print(s)
except Exception as e:
# Если адрес уже используется, может возникнуть исключение.
print(f'Socket error: {e}')
raise
Видно, что сокет теперь привязан к определенному адресу и порту:
<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=17, laddr=('127.0.0.1', 8080)>
Для интернет-сокетов, если задан нулевой порт, свободный будет выбран автоматически. Зачем это нужно, можно увидеть в одном из примеров главы 5. О портах подробнее будет рассказано в следующей главе.
За конвертацию отвечают функции, объявленные в файле arpa/inet.h в ОС Linux либо в файлах ws2tcpip.h и wsipv6ok.h в ОС Windows.
Функция inet_pton() конвертирует адрес из читаемой формы в sockaddr. Название функции расшифровывается как inet Printable TO Numeric. Обратная ей функция inet_ntop() конвертирует адрес в читаемую форму. Название функции расшифровывается как inet Numeric TO Printable.
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
Параметры функций inet_:
• af — семейство адресов. Поддерживаются AF_INET и AF_INET6.
• src — источник, содержащий адрес. Для первой функции — строка. Для второй — указатель на структуру in_addr для IP либо in6_addr для IPv6.
• dst — указатель на приемник. Тип — либо указатель на структуру, либо строка.
• size — размер приемника в байтах.
Внимание! Функции inet_ к DNS не обращаются и доменные имена разрешать не могут. Они просто выполняют конвертацию между строковым представлением и структурой адреса.
Функция inet_pton() возвращает 1 в случае успеха, 0, если адрес некорректный, и –1, если указано некорректное семейство адресов.
Функция inet_ntop() возвращает указатель на строку в случае успеха или нулевой указатель в случае неудачи.
Внимание! Вам также могут встретиться другие функции, такие как inet_makeaddr(). Они устарели, могут быть непотокобезопасными и не работают с IPv6. Использовать их не рекомендуется.
Устаревшие и нестандартные функции
Некоторые из устаревших функций:
• char* inet_ntoa(in_addr in) — преобразовывает IPv4-адрес в строку.
• int inet_aton(const char *cp, in_addr *inp) — преобразовывает строку в адрес и сохраняет в inp.
• in_addr_t inet_addr(const char *cp) — преобразовывает строку в числовой адрес в сетевом порядке байтов.
• in_addr_t inet_network(const char *cp) — преобразовывает строку адреса сети в числовой адрес.
• in_addr inet_makeaddr(in_addr_t net, in_addr_t host) — формирует из адреса узла и сети структуру.
• in_addr_t inet_lnaof(in_addr in) — получает локальный адрес узла.
• in_addr_t inet_netof(in_addr in) — получает адрес сети узла.
Кроме них существуют нестандартные функции:
• int inet_net_pton(int af, const char *pres, void netp, size_t nsize) — преобразовывает номер сети в число из строки в число.
• char *inet_net_ntop(int af, const void netp, int bits, char pres, size_t psize) — преобразовывает номер сети в строку.
Эти функции работают с IPv6, но, например, в ОС Windows они не встречаются, и лучше воздержаться от их использования.
Пример:
// IPv4.
sockaddr_in sa;
// IPv6.
sockaddr_in6 sa6;
// IPv4.
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr));
// IPv6.
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr));
// Буфер для строкового представления IPv4.
std::array<char, INET_ADDRSTRLEN> ip4;
// Сконвертировать адрес в строку типа char*.
inet_ntop(AF_INET, &(sa.sin_addr), ip4.data(), ip4.size());
// IPv6:
// Буфер для строкового представления IPv6.
std::array<char, INET6_ADDRSTRLEN> ip6;
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6.data(), ip6.size());
std::cout
<< "IP6 Address: " << ip6.data()
<< ", IP4 Address " << ip4.data()
<< std::endl;
Результат:
➭ build/bin/b01-ch02-ip-convert
IP6 Address: 2001:db8:63b3:1::3490, IP4 Address 10.12.110.57
В Python есть аналогичные функции:
ip4 = socket.inet_pton(socket.AF_INET, '10.12.110.57')
ip6 = socket.inet_pton(socket.AF_INET6, '2001:db8:63b3:1::3490')
print(socket.inet_ntop(socket.AF_INET, ip4))
print(socket.inet_ntop(socket.AF_INET6, ip6))
Результат их вызова такой же:
➭ src/book01/ch02/python/ip-convert.py
10.12.110.57
2001:db8:63b3:1::3490
Стандартная библиотека Python содержит модуль ipaddress, предназначенный для работы с IP-адресами и адресами сетей.
Данный модуль не обращается к сети, а манипулирует исключительно представлением IP-адресов.
Его функции и классы позволяют:
• сравнивать IP-адреса;
• производить с адресами арифметические действия, например складывать;
• представлять IPv4- и IPv6-адреса в различных форматах;
• определять, находятся ли два хоста в одной и той же подсети;
• определять, подходит ли данный адрес указанному интерфейсу;
• перебирать все узлы в конкретной подсети;
• проверять, является ли строка допустимым IP-адресом.
Модуль содержит несколько основных классов:
• IPv4Address и IPv6Address — представление IP-адреса.
• IPv4Network и IPv6Network — подсеть.
• IPv4Interface и IPv6Interface — представление адресов сетевого интерфейса.
За их создание отвечают следующие функции:
# Вернуть IP-адрес, как IPv4, так и IPv6.
def ip_address(address) -> IPv4Address | IPv6Address
# Создать подсеть из адресов, обычно задаваемую адресом и маской подсети.
# Если параметр strict равен True, установленные биты хоста вызовут
# исключение ValueError, иначе просто будут замаскированы.
def ip_network(address, strict=True) -> IPv4Network | IPv6Network
# Создать объект интерфейса.
def ip_interface(address) -> IPv4Interface | IPv6Interface
Для всех функций параметр address — строка в формате IPv4/IPv6 или число, представляющее адрес.
Классы адресов имеют такие свойства, как:
• Определение типа адреса:
• is_multicast — адрес является адресом групповой передачи. Например, 10.10.1.255.
• is_private — внутренний адрес для локальных сетей. Адресов из этого диапазона нет в интернете. Например, 192.168.1.1 или 10.10.1.1.
• is_global — адрес из диапазона адресов, распределенных для интернета. Например, 8.8.8.8.
• is_unspecified — неопределенный адрес. Пример: 0.0.0.0.
• is_reserved — IP-адрес, зарезервированный IANA. Например, 192.168.0.0/24, зарезервированный для специальных целей.
• is_loopback — адрес является локальной петлей (loopback). Это все адреса, начинающиеся со 127, типичный пример — 127.0.0.1.
• is_link_local — IPv4-адрес зарезервирован для локального использования.
• is_site_local — IPv6-адрес зарезервирован для локального использования.
• ipv4_mapped — IPv6-адреса, отображенные на пространство IPv4, начинающиеся с :FFFF/96.
• Представления адреса в разном виде:
• compressed — сокращенная форма представления IPv6-адреса.
• exploded — полная форма представления IPv6-адреса.
• packed — бинарная форма представления адреса: 4 байта для IPv4 или 16 байт для IPv6.
• reverse_pointer — имя обратной DNS PTR-записи для IP-адреса.
В современных IP-сетях адреса не делятся на классы, то есть используется бесклассовая адресация, или CIDR. Поэтому сеть или подсеть обозначаются как адрес и маска, например: 192.168.1.0/24 или 192.168.1.0/255.255.255.0.
Маска задает число битов, которые в адресе выделены под номер сети, являющийся префиксом, то есть идущий первым слева направо в адресе. Строго говоря, маски могут иметь вид: 11111111.11111111.11111111.00000001.
Данной маске соответствует каждый второй узел сети из 28 – 1 узел. Но с такими масками обычно не работают, и пользоваться такой записью не стоит, чтобы не получать непонятных ошибок.
Помимо методов определения типа сети и получения адреса в разном представлении, аналогично классам IP адресов, классы IPv4Network и IPv6Network позволяют:
• Выделить различные типы адресов:
• network_address — адрес сети, который соответствует битам маски.
• broadcast_address — широковещательный адрес для заданного адреса сети.
• hostmask — маска адреса узла данной сети, например, как IP-адрес 0.0.0.255.
• netmask — сетевая маска, как IP-адрес. Например, 255.255.255.0.
• Получить различные части сети:
• hosts() — список хостов.
• subnets() — список подсетей.
• supernet() — надсеть.
• prefix_len() — число битов маски сети.
• Выполнить сравнение:
• subnet_of(), supernet_of() — проверка на то, является ли сеть подсетью или надсетью указанной сети.
• compare_networks() — выполняет сравнение двух сетей, возвращая 0, –1 или 1.
Объекты интерфейсов представляют собой объединение адреса и сети: интерфейс находится в сети, в которой он имеет адрес.
Кроме того, модуль содержит несколько отдельных полезных функций:
# Получить адрес в виде 4 упакованных байт в сетевом порядке.
def v4_int_to_packed(address)
# Получить адрес в виде 16 упакованных байт в сетевом порядке.
def v6_int_to_packed(address)
# Получить итератор диапазона адресов от первого и до последнего адреса.
def summarize_address_range(first, last)
# Возвращает итератор свернутых объектов (адрес и маску)
# IPv4Network или IPv6Network.
def collapse_addresses(addresses)
# Возвращает ключ, подходящий для сортировки между сетями и адресами.
def get_mixed_type_key(obj)
Для выполнения различных действий с адресами модуль очень полезен. Немаловажно также, что он является частью стандартной библиотеки Python.
Существуют библиотеки с более широкими возможностями, например библиотека . Но она не входит в стандартную библиотеку Python, и ее придется устанавливать отдельно.
Доменное имя — это имя, которое идентифицирует области иерархии, являющиеся единицами административной автономии в сети интернет (см., например, доменное имя mail.google.com на рис. 2.2).
Рис. 2.2. Доменный адрес
Такие области называются доменами. По определению, приведенному выше, управляет доменом и несет ответственность за содержимое, связанное с ним, отдельное лицо или организация. За имя и содержимое google.com несет ответственность корпорация Google LLC.
Доменные имена удобны для человека. Кроме того, они играют роль дополнительных адресов в некоторых протоколах, например, для реализации виртуальных серверов в HTTP.
Связывание доменов и серверов организации выполняется по IP-адресу. При этом одному доменному имени может соответствовать множество IP-адресов, что используется, например, при распределении нагрузки.
Общее пространство имен интернета функционирует благодаря системе доменных имен, или DNS, представляющей собой иерархию серверов. Чаще всего DNS используется для преобразования имен в адреса и обратно, но также может быть использована для получения различных служб, относящихся к домену, например e-mail сервера.
Каждый сервер, отвечающий за имя, может передать ответственность за дальнейшую часть домена другому серверу. Для взаимодействия серверов в RFC 1035 Domain names — implementation and specification описан протокол работы с DNS.
Сервера хранят метаданные об узлах либо адреса серверов, которые могут предоставить эти метаданные. Они хранятся как типизированные записи, имеющие предопределенные ключи (в том числе как записи, отображающие доменные имена на адреса либо делегирующие такое отображение другим серверам в иерархии).
Например:
• A или address-запись хранит адрес IPv4.
• AAAA-запись обычно хранит IPv6-адрес.
• CNAME содержит привязку поддомена к каноническому имени домена.
• MX-запись содержит IP-адрес сервера электронной почты домена.
• NS-запись указывает иные DNS-сервера домена.
Компания — регистратор доменных имен вносит на DNS-сервера запись о соответствии домена IP-адресам.
Получим данные домена yandex.ru:
➭ whois yandex.ru
% TCI Whois Service. Terms of use:
% https://tcinet.ru/documents/whois_ru_rf.pdf (in Russian)
% https://tcinet.ru/documents/whois_su.pdf (in Russian)
domain: YANDEX.RU
nserver: ns1.yandex.ru. 213.180.193.1, 2a02:6b8::1
nserver: ns2.yandex.ru. 93.158.134.1, 2a02:6b8:0:1::1
state: REGISTERED, DELEGATED, VERIFIED
org: YANDEX, LLC.
taxpayer-id: 7736207543
registrar: RU-CENTER-RU
admin-contact: https://www.nic.ru/whois
created: 1997-09-23T09:45:07Z
paid-till: 2024-09-30T21:00:00Z
free-date: 2024-11-01
source: TCI
Last updated on 2024-06-06T20:06:30Z
Это домен 2-го уровня, который зарегистрирован регистратором Ru-Center. В данном случае указаны адреса DNS-серверов компании Yandex, что позволяет компании самостоятельно управлять отображением имени домена на конкретные серверы. Регистратор, как правило, вносит записи о доменах 2-го уровня иерархии.
В отдельных случаях регистрацией может заниматься любая организация, которая уже имеет домен. Новые домены будут принадлежать данной организации и являться поддоменами основного домена, например: nas.cloudns.cc — домен 3-го уровня, который зарегистрирован сервисом CloudNS, предоставляющим услуги связывания динамических IP с доменом. Часто такие домены называются доменными зонами, как и домены 1-го уровня, например «доменная зона ru».
Доменные имена состоят из меток либо имен, заканчивающихся точками, и читаются справа налево. Корневая доменная зона, или «домен уровня 0», имеет пустую метку, в результате остается лишь точка.
FQDN — Fully Qualified Domain Name, или полное доменное имя. Это имя, которое определено в иерархии, полностью однозначно. В FQDN указаны все части доменной иерархии, ни одна не пропускается. Например, имя одного из серверов имен Yandex: "ns1.yandex.ru".
Обратите внимание на точку в конце — это домен 0-го уровня. Домен 1-го уровня — зона «ru», домен 2-го уровня — «yandex». Доменное имя сервера имен является доменом 3-го уровня. www.yandex.ru и yandex.ru — разные домены разных уровней.
Согласно RFC 1035 существуют ограничения на размер доменного имени:
• Не более 253 символов на имя, включая точки.
• Не более 63 символов на метку.
• Не более 127 уровней иерархии при использовании односимвольных меток.
Во внутреннем двоичном представлении DNS для хранения имени максимальной длины требуется 255 октетов. Но DNS также хранит длину имени. В результате полное доменное имя может иметь длину не более 253 символов в текстовом представлении.
Имя может быть преобразовано в адрес на локальной машине с использованием статических записей. Обычно это первый этап преобразования. Однако чаще всего узел не знает всех IP-адресов, соответствующих другим именам в интернете, а пользователь предпочитает обращаться к узлам по имени.
Кроме того, некоторые соответствия адресов именам могут динамически изменяться. Так что обычно узлу приходится обращаться к DNS, чтобы получить IP-адрес по имени домена, как показано на рис. 2.3.
Рис. 2.3. Работа с доменами в браузере
Простые вызовы, описанные ниже, лежат в основе более сложных функций преобразования имен.
Рассмотрим несколько простых функций для получения IP-адреса по имени узла и наоборот — имени узла по адресу. Эти функции обращаются к DNS. Как и некоторые другие функции, они используют структуру hostent, определенную в netdb.h:
struct hostent
{
// Официальное имя узла.
// При использовании DNS или аналогичной системы разрешения имен
// полное доменное имя – FQDN.
char *h_name;
// Список псевдонимов, которые отдает DNS-сервер, если они были прописаны
// в записях ALIAS и CNAME. Завершается nullptr.
char **h_aliases;
// Тип адреса или семейство адресов: AF_INET, AF_INET6.
int h_addrtype;
// Длина адреса.
int h_length;
// Список адресов в сетевом порядке байтов. Завершается nullptr.
char **h_addr_list;
}
В данном случае в псевдонимы попадают дополнительные имена, которые связаны с данным именем. Записи типа ALIAS и CNAME создают перенаправление на другое имя домена.
Например, если имеется домен www.google.com, на DNS-сервере для него может существовать единственная запись:
«www.google.com IN CNAME google.com».
Эта запись говорит клиенту, что нужно прочитать записи домена google.com для имени домена www.google.com. В этих записях указано все необходимое, в частности адреса и DNS-сервера.
Записи CNAME нельзя добавлять на домены 2-го уровня. Но записи типа ALIAS можно. Преобразование для ALIAS-записей производится на сервере.
И те и другие записи добавляются в список h_aliases.
Кроме этих типов записей, существуют редко используемые типы ANAME, BNAME и DNAME.
Данные о хосте можно получить с помощью функции gethostent(), последовательный вызов которой возвращает указатели на разные экземпляры структуры hostent:
hostent *gethostent(void);
Нулевой указатель означает, что записей больше нет. Предварительно необходимо «открыть базу», используя функцию sethostent().
Эти функции получают сведения из базы данных узлов через Name Service Switch — интерфейс GLibC, предоставляющий доступ к различным базам данных настроек узла.
Существуют функции, которые предоставляют доступ к базе служб, RPC, сетевых адаптеров, протоколов, сетей и т.п. За подробностями обращайтесь к man 1 getent.
Согласно , для работы с DNS можно использовать TCP. Функции могут включать данный режим между вызовами gethostby...:
// Открыть базу хостов и установить в начало указатель.
// Если stayopen true — использовать постоянное TCP-соединение
// для работы с DNS сервером.
// В ином случае запрос DNS будет выполняться, используя UDP-дейтаграммы.
// Если использован gethostent, значение stayopen должно быть true.
void sethostent(int stayopen);
// Закрыть базу хостов.
// При вызове TCP соединение с сервером DNS будет закрыто.
void endhostent(void);
Пример:
#include <netdb.h>
...
// В какой-то функции:
hostent *he;
sethostent(1);
while ((he = gethostent()) != nullptr)
{
std::cout << he->h_name << std::endl;
}
endhostent();
...
При запуске этого кода на узле увидим примерно следующее:
➭ build/bin/b01-ch02-net-db
localhost.localdomain
localhost.localdomain
office-machine.comp.ru
Функции, которые отвечают за открытие и закрытие «базы хостов», также влияют на поведение gethostbyaddr() и gethostbyname(), описанных ниже.
В Python аналога данным функциям нет. В Go, например, функция проброшена из библиотеки C.
Обычно для высокоуровневой работы с DNS используют следующие функции:
• getaddrinfo() — получает список адресов, выделяя под него память.
• getnameinfo() — получает список имен узлов.
• freeaddrinfo() — освобождает память.
• gai_strerror() — переводит коды ошибок в читаемые строки.
Основная функция getaddrinfo() объединяет действия устаревших функций getipnodebyname(), getipnodebyaddr(), getservbyname() и getservbyport() в одном интерфейсе.
Вызов этих функций является потокобезопасным и обычно использует локальное хранилище потока для создания нужных структур.
Она создает одну или несколько структур адресов сокета, которые в дальнейшем можно использовать в вызовах функций для создания сокета клиента или сервера.
Поведение функции getaddrinfo() можно настраивать. В Linux настройки содержатся в файле /etc/gai.conf, во FreeBSD их можно посмотреть командой ip6addrctl show.
По умолчанию порядок возврата адресов зависит от используемой ОС и может быть такой:
1. Избегать неиспользуемых пунктов назначения. В настоящее время не рассматривается, существует ли подходящий маршрут.
2. Предпочитать соответствие области.
3. Избегать устаревших адресов.
4. Предпочитать домашние адреса.
5. Предпочитать соответствие метке.
6. Предпочитать более высокий приоритет.
7. Предпочитать собственный транспорт.
8. Предпочитать меньшую область.
9. Использовать самый длинный префикс соответствия. Сравнивается длина только в одном семействе адресов.
10. Оставить порядок без изменений.
Эта функция позволяет не заполнять вручную структуры sockaddr, sockaddr_in и подобные.
В Linux для использования этих функций необходимо включить файлы sys/types.h, sys/socket.h, netdb.h, а в Windows — файл ws2tcpip.h.
Прототипы:
#include <netdb.h>
int getaddrinfo(const char *node, const char *service,
const addrinfo *hints,
addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
const char *gai_strerror(int errcode);
Параметры функции getaddrinfo():
• node — доменное имя узла.
• service — имя службы или порт, если установлен соответствующий флаг.
• hints — структура, которая содержит «подсказки», например адреса и флаги.
• res — список, который содержит адреса, возвращенные getaddrinfo().
Рассмотрим структуру addrinfo, которая передается как параметр hints и возвращается как результат в res:
struct addrinfo
{
// AI_PASSIVE, AI_CANONNAME и т.д.
int ai_flags;
// AF_INET, AF_INET6, AF_UNSPEC.
int ai_family;
// SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_RDM, SOCK_SEQPACKET.
int ai_socktype;
// 0 для "any" или IPPROTO_TCP, IPPROTO_UDP и пр.
int ai_protocol;
// Размер ai_addr в байтах.
size_t ai_addrlen;
// struct sockaddr_in или _in6.
struct sockaddr *ai_addr;
// Полное каноническое имя.
char *ai_canonname;
// Следующий узел списка.
struct addrinfo *ai_next;
};
Внимание! В ОС Windows два последних атрибута структуры меняются местами: сначала идет ai_canonname, а потом ai_addr.
В C-коде модуля socket для Python данная структура определена явно в файле , чтобы не зависеть от платформы: она будет использоваться, если в ОС по какой-то причине ее определение недоступно либо некорректно.
Порядок получения адресов из данной структуры показан на рис. 2.4.
Рис. 2.4. Структура addrinfo
В Unix-подобных ОС флаги для getaddrinfo(), передаваемые в поле ai_flags параметра hints, определены в netdb.h и могут быть следующими:
• AI_PASSIVE — адрес сокета предназначен для bind(), то есть адрес можно не указывать. Будет подставлен корректный адрес, вероятно 0.0.0.0, позволяющий запустить сервер, принимающий входящие подключения со всех интерфейсов.
• AI_CANONNAME — запросить каноническое имя.
• AI_NUMERICHOST — использовать числовой адрес хоста в качестве имени. Если флаг установлен, node должно содержать адрес в числовой форме, с точками для IPv4, а не домен.
• AI_NUMERICSERV — задать службу портом, а не именем. Если флаг установлен и поле service ненулевое, оно должно содержать строку с номером порта, то есть его строковое представление, в противном случае — название службы.
• AI_V4MAPPED — если IPv6-адреса не найдены, запросить IPv4-адреса и вернуть их вызывающей стороне как IPv4-отображенные IPv6-адреса, например ::FFFF:129.144.52.38.
• AI_ALL — запросить адреса IPv4 и IPv6. Причем IPv4 запрашивается как при установке флага AI_V4MAPPED.
• AI_ADDRCONFIG — запрашивать IPv4-адреса только в том случае, если система имеет хотя бы один IPv4-адрес; запрашивать адреса IPv6 только в том случае, если система имеет хотя бы один IPv6-адрес.
Начиная с GLibC 2.3.4, появились флаги для работы с доменными именами, не использующими стандарт ASCII:
• AI_IDN — если входное имя содержит символы, отличные от ASCII, используется кодирование IDN. См. RFC 3490 Internationalizing Domain Names in Applications. Исходная кодировка соответствует текущей локали.
• AI_CANONIDN — преобразовать каноническое имя в удобочитаемую форму. Если возвращенное каноническое имя закодировано с помощью ACE, оно будет содержать префикс xn-- для одного или нескольких компонентов имени. Данный флаг используется для преобразования компонентов.
• AI_IDN_ALLOW_UNASSIGNED — установка этих флагов активирует IDNA_ALLOW_UNASSIGNED — разрешить неназначенные кодовые точки Unicode.
• AI_IDN_USE_STD3_ASCII_RULES — проверить выходные данные, чтобы убедиться, что это имя хоста соответствует стандарту STD3, для использования в обработке IDNA — IDNA_USE_STD3_ASCII_RULES.
Функция возвращает 0 в случае успеха или ненулевой код ошибки в ином случае, что выгодно отличает ее от большинства функций, ошибка в которых устанавливается в отдельной переменной. Значения кодов указаны в man 3 getaddrinfo.
Передав возвращенный код в функцию gai_strerror(), можно получить текст ошибки.
В качестве примера использования для сервера приведем функцию из обертки. Для этого сначала введем тип, который будет содержать указатель на список адресов:
// Функция freeaddrinfo() будет вызвана при разрушении указателя.
// Она нужна, чтобы освободить связный список.
typedef std::unique_ptr<addrinfo, decltype(&freeaddrinfo)> AddrInfoResult;
Он позволит автоматически и правильно удалять список, когда указатель выйдет из области видимости.
Функция вернет значение типа:
// Порт – строка.
// Тип сокета, например SOCK_STREAM.
AddrInfoResult get_serv_info(const std::string &port, int sock_type)
{
// Структура "подсказок" – настроек, которые будет использовать
// getaddrinfo().
addrinfo hints =
{
// Флаги: вернуть серверу нужный адрес
// и флаг, указывающий, что служба задана не именем, а номером порта.
.ai_flags = AI_PASSIVE | AI_NUMERICSERV,
// IPv4. Если не важно, IPv4 или IPv6, указать AF_UNSPEC.
.ai_family = AF_INET,
.ai_socktype = sock_type,
// Протокол.
.ai_protocol = (sock_type == SOCK_STREAM ? IPPROTO_TCP : IPPROTO_UDP)
};
// Указатель на результаты.
addrinfo *s_i = nullptr;
if (int ai_status = getaddrinfo(nullptr, port.c_str(), &hints, &s_i);
ai_status != 0)
{
// Обратите внимание: для вывода ошибки используется gai_strerror().
throw std::logic_error(gai_strerror(ai_status));
}
// s_i теперь указывает на связный список
// из одной или более структур addrinfo.
return AddrInfoResult(s_i, freeaddrinfo);
}
Параметр service, который в примере задается портом, определяет службу. Если бы мы не использовали флаг AI_NUMERICSERV, он также мог быть задан именем службы, например «https». Соответствие «имя-порт» задано в /etc/services.
Задавать службу или порт крайне желательно, поскольку для разных служб может быть получен разный адрес, как для клиентского запроса, так и для запроса получения адреса сервера.
Если же служба задана некорректно, например недоступна для указанного протокола, функция вернет ошибку EAI_SERVICE.
Пример для клиента:
AddrInfoResult get_client_info(const std::string &host,
const std::string &port, int sock_type,
int sock_family)
{
addrinfo hints =
{
// Разница во флагах: AI_PASSIVE отсутствует, флаг возврата
// канонического имени не обязателен.
.ai_flags = AI_CANONNAME | AI_NUMERICSERV,
// Семейство адресов, например AF_UNSPEC для IPv4 и IPv6.
.ai_family = sock_family,
// Тип сокета.
.ai_socktype = sock_type,
// Протокол.
.ai_protocol = (sock_type == SOCK_STREAM ? IPPROTO_TCP : IPPROTO_UDP)
};
// Указатель на результаты.
addrinfo *c_i = nullptr;
// Для клиента нужно указать имя хоста, к которому подключаемся.
if (int ai_status = getaddrinfo(host.c_str(), port.c_str(), &hints, &c_i);
ai_status != 0)
{
throw std::logic_error(gai_strerror(ai_status));
}
// c_i теперь указывает на связный список из одной или более
// структур addrinfo.
// std::make_unique здесь не используется,
// потому что нужен пользовательский deleter.
return AddrInfoResult(c_i, freeaddrinfo);
}
Часто из списка просто берут первый адрес. Но можно пройти по списку и найти адрес, соответствующий нужным критериям, например:
• Любой работающий — проверяется выполнением подключения.
• С минимальным временем ответа — проверяется отправкой PING-запроса на адреса ICMP с расчетом времени ответа.
• Ближайший географически по базе GeoIP, находимый без запроса.
Внимание! Не все узлы будут отвечать на ping, но это не значит, что они не работают. При проверках это необходимо учитывать, иначе работающий адрес можно пропустить.
В Python данная функция вернет список из кортежей, содержащих:
• Семейство адресов.
• Тип сокета.
• Протокол.
• Каноническое имя.
• (Адрес, Порт) для IPv4 или (Адрес, Порт, flow info, scope id) для IPv6.
В случае неудачи функция выбросит исключение socket.gaierror или другие, например TypeError:
addrs = socket.getaddrinfo('www.google.com', '3490',
family=socket.AF_UNSPEC, type=socket.SOCK_STREAM)
Очевидно, что в Python уже не требуется вызывать freeaddrinfo(). Также обратите внимание, что в данную функцию порт можно передать как строку, а в случае передачи числа не требуется его явное преобразование в сетевой порядок байтов.
Порт 3490 зарезервирован для Colubris Wireless Management System:
➭ grep 3490 /etc/services
colubris 3490/tcp
colubris 3490/udp
Мы выбрали его для примера случайным образом. Очевидно, что мы не используем систему Colubris, поэтому логичнее писать число, чтобы не вводить читателя кода в заблуждение.
Если ничего передавать не требуется, в Python вместо nullptr используется None.
Иногда требуется по IP-адресу получить имя соответствующего ему узла. Для такого преобразования существует функция getnameinfo():
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
Параметры функции getnameinfo():
• addr — исходный адрес, по которому будет получено имя.
• addrlen — размер структуры адреса.
• host — буфер для помещения имени узла. Может иметь значение nullptr, если имя узла получать не требуется.
• hostlen — длина буфера host. Если буфер имеет нулевое значение, длина тоже должна быть равна нулю.
• serv — буфер для помещения имени службы. Может иметь значение nullptr, если имя службы получать не требуется.
• servlen — длина буфера serv. Как и для hostlen, если буфер имеет нулевое значение, длина тоже должна быть равна нулю.
• flags — флаги для более тонкой настройки параметров возвращаемого значения. Например, позволяют вернуть строку чисел IP-адреса из структуры addr.
Внимание! Размер буферов для узла и службы должен быть достаточным, чтобы сохранить имя с завершающим нулевым символом.
Флаги для getnameinfo() определены в netdb.h:
• NI_NOFQDN — для локальных узлов вернуть только имя узла от полного доменного имени.
• NI_NUMERICHOST — вернуть числовую форму адреса узла вместо его имени.
• NI_NAMEREQD — вернуть ошибку, если имя узла не может быть найдено.
• NI_NUMERICSERV — вместо имени службы использовать числовую форму адреса службы.
• NI_NUMERICSCOPE — для IPv6-адресов вместо имени вернуть числовую форму идентификатора области.
• NI_DGRAM — указывает, что служба является службой дейтаграмм — SOCK_DGRAM: по умолчанию считается, что служба потоковая — SOCK_STREAM.
Для getnameinfo() существуют интернациональные флаги, аналогичные флагам для getaddrinfo(): NI_IDN, NI_IDN_ALLOW_UNASSIGNED и NI_IDN_USE_STD3_ASCII_RULES.
Подробнее см. в документации к своей ОС.
Возвращаемое значение функции аналогично тому, что возвращает getaddrinfo().
Пример:
sockaddr *addr;
socklen_t addrlen;
std::array<char, NI_MAXHOST> hbuf;
if (getnameinfo(addr, addrlen, hbuf.data(), hbuf.size(), nullptr, 0,
NI_NAMEREQD))
{
std::cerr << "could not resolve hostname" << std::endl;
}
else
std::cout << "host" << hbuf << std::endl;
Аналог в Python принимает только адрес и порт, а также флаги. В случае неудачи также выбрасывает исключение gaierror либо ряд других:
print(socket.getnameinfo(('8.8.8.8', 80), socket.NI_NAMEREQD))
print(socket.getnameinfo(('8.8.8.8', 8080), socket.NI_NAMEREQD))
Результат:
('dns.google', 'http')
('dns.google', 'http-alt')
В работе вам могут пригодиться следующие функции:
#include <unistd.h>
// Получить имя текущего хоста.
int gethostname(char *name, size_t namelen);
// Установить имя текущего хоста.
int sethostname(const char *name, size_t namelen);
// Получить уникальный 32-битный идентификатор машины.
long gethostid(void);
// Установить идентификатор.
int sethostid(long hostid);
Параметры функций:
• name — имя. Массив символов, 0 в конце не обязателен.
• namelen — длина имени.
• hostid — идентификатор узла.
Очевидно, что set-функции будут работать только с правами суперпользователя или с привилегией администратора.
В Python функций для получения id нет, есть только функции для работы с именем узла:
def sethostname(name: str) -> None
def gethostname() -> str
В Python есть полезная функция socket.getfqdn(), которая принимает единственный аргумент name и возвращает FQDN. Данная функция сначала проверяет имя хоста, возвращаемое функцией gethostbyaddr(), а затем его псевдонимы. Если полное доменное имя недоступно и параметр name задан, он вернется без изменений. Для пустого имени либо адреса 0.0.0.0 будет вызван gethostname():
import socket
# localhost.localdomain
print(socket.getfqdn())
# a904c694c05102f30.awsglobalaccelerator.com
print(socket.getfqdn('unknowndomain.com'))
# yandex.ru
print(socket.getfqdn('yandex.ru'))
# localhost.localdomain
print(socket.getfqdn('0.0.0.0'))
# dns.google
print(socket.getfqdn('8.8.8.8'))
Результат вызова примера будет следующим:
➭ src/book01/ch02/python/fqdn_example.py
localhost.localdomain
a904c694c05102f30.awsglobalaccelerator.com
yandex.ru
localhost.localdomain
dns.google
Функции ниже помечены как устаревшие, но их вызовы могут встретиться, например, в старом коде:
#include <netdb.h>
// Будет содержать номер ошибки, если функции ниже завершатся некорректно.
// Возможные ошибки: HOST_NOT_FOUND, NO_DATA, NO_RECOVERY, TRY_AGAIN.
extern int h_errno;
// Получить список hostent по имени хоста.
struct hostent *gethostbyname(const char *name);
// AF_INET.
#include <sys/socket.h>
// Получить hostent по адресу хоста.
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
В Python существует функция socket.gethostbyname_ex(), которая вернет кортеж, содержащий:
• Официальное имя хоста.
• Список псевдонимов, то есть содержимое DNS-записей CNAME.
• Список IPv4-адресов.
def gethostbyname_ex(host) -> Tuple(name, aliaslist, addresslist)
Пример:
>>> print(socket.gethostbyname_ex('yandex.ru'))
('yandex.ru', [], ['77.88.55.66', '5.255.255.55', '5.255.255.50', '77.88.55.70'])
Внимание! Эти функции в C API устарели. Не используйте их.
Внимание! Этот раздел, скорее всего, вам не пригодится, особенно на базовом уровне. Его можно пропустить или прочесть позже. На понимание материала это не повлияет.
Достаточно редко возникает необходимость конфигурировать резолвер под конкретные задачи, например, при реализации служебных утилит, работающих с DNS или при каких-либо «экзотических» требованиях к составлению запроса и к обработке результатов, выдаваемых сервером DNS.
Функции типа res_*() и функции dn_comp(), dn_expand() отвечают за прямую работу с резолвером. Они выполняют запросы и обрабатывают ответы, полученные от серверов доменных имен. Данные функции не кросс-платформенные. Подробнее о них можно прочесть в man 3 resolver.
Для гибкого выполнения запросов к DNS существует утилита dig — domain information groper. То, как используются приведенные ниже функции и флаги, можно посмотреть в ее коде.
Для работы с перечисленными функциями требуется включить следующие заголовочные файлы, в которых функции объявлены и определена структура __res_state:
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
struct __res_state;
typedef __res_state *res_state;
Функции res_ninit() и устаревшая res_init() нужны для инициализации DNS и должны вызываться первыми. Их вызов, а также последующий вызов функций res_nquery() выделяет память. Поэтому для ее освобождения требуется вызвать res_nclose():
int res_ninit(res_state statep);
void res_nclose(res_state statep);
Обратите внимание, что res_state — указатель на структуру и по нему в структуру будет производиться запись.
Алгоритм работы функций следующий:
1. Прочитать файлы настройки из файла /etc/resolv.conf для получения имени домена по умолчанию и адресов серверов имен.
2. Если серверы не заданы, используется локальный узел.
3. Если не задан домен, используется домен из переменной окружения LOCALDOMAIN или домен локального узла.
Они возвращают 0 в случае успеха и –1 при ошибке.
Внимание! На каждый вызов res_ninit() должен быть сделан вызов res_nclose().
В Linux структура __resstate определена в /usr/include/bits/types/res_state.h и достаточно сложна. Она содержит различные параметры, массив серверов имен, компоненты домена, которые используются для поиска, и т.д.
Поля, которые могут быть интересны пользователю:
• retrans — интервал повторной передачи.
• retry — число попыток повторной передачи.
• options — флаги, описанные ниже.
• pfcode — флаги RES_PRF_. Используются в программе dig.
• ipv6_unavail — флаг неуспешности подключения к IPv6-серверу.
Константы опций и флагов pfcode определены в /usr/include/resolv.h.
Настройки отладки и безопасности:
• RES_DEBUG — печатать отладочные сообщения. Этот параметр доступен, только если GLibC собрана с включенной отладкой, которая по умолчанию выключена.
• RES_INSECURE1 — принимать ответ от ошибочного сервера. Может использоваться для обнаружения потенциальных угроз безопасности, но требует перекомпиляции GLibC с включенной отладкой и установки флага RES_DEBUG.
• RES_INSECURE2 — принимать ответ, содержащий некорректный запрос. Требует перекомпиляции GLibC с включенной отладкой.
• RES_USE_DNSSEC — использовать DNSSEC с битом OK в записи OPT. Это значение подразумевает установленный RES_USE_EDNS0.
• RES_USE_EDNS0 — включить поддержку расширений DNS EDNS0, описанных в RFC 2671 «Extension Mechanisms for DNS (EDNS0)».
Настройки запроса:
• RES_IGNTC — не пытаться повторить запрос с помощью TCP.
• RES_SNGLKUPREOP — открывать для каждого запроса новый сокет, если указано значение RES_SNGLKUP.
• RES_STAYOPEN — используется вместе с RES_USEVC для поддержания одного TCP-соединения для запросов между ответами.
• RES_USEVC — использовать TCP-соединение для запросов вместо дейтаграмм UDP.
• RES_SNGLKUP — выполнять запросы IPv6 и IPv4 последовательно. По умолчанию GLibC, начиная с версии 2.9, выполняет поиск по IPv4 и IPv6 параллельно. Некоторые приложения DNS-серверов не могут обработать такие запросы должным образом и делают паузу между ответами на запрос. Включение флага замедлит процесс определения имени.
Настройки поиска:
• RES_DEFNAMES — res_nsearch() будет добавлять имя домена по умолчанию к именам с одним компонентом в имени, то есть не содержащим точек.
• RES_DNSRCH — если указан, res_search() будет искать имена узлов в текущем и родительском домене.
• RES_INIT — произведена инициализация. True, если уже вызывалась функция res_ninit().
• RES_NOALIASES — отключить использование переменной окружения HOSTALIASES.
• RES_NOTLDQUERY — не искать неполное имя как домен верхнего уровня — TLD, Top Level Domain.
• RES_RECURSE — установить в запросах бит рекурсии. Рекурсия выполняется сервером доменных имен, а не функцией res_nsend(). Параметр включен по умолчанию.
• RES_ROTATE — включить циклический выбор среди имеющихся серверов имен. Это приводит к распределению нагрузки среди серверов и их циклическому использованию, когда каждый раз клиенты используют следующий в очереди сервер.
Комбинации флагов:
• RES_DEFAULT — комбинация флагов: RES_RECURSE, RES_DEFNAMES, RES_DNSRCH и RES_NOIP6DOTINT.
Устаревшие опции res_ninit()
При использовании данных параметров выдается предупреждение компилятора:
• RES_NOCHECKNAME — выключить в современном BIND проверку недопустимых символов в поступающих именах узлов и почтовых именах, таких как символы подчеркивания «_», не-ASCII или управляющие символы.
• RES_USE_INET6 — пытаться выполнить запрос AAAA раньше запроса A внутри функции gethostbyname() и отображать ответы IPv4 в «туннелированной форме» IPv6, если записи AAAA не были обнаружены, но есть запись типа A. Новые приложения должны использовать getaddrinfo(), а не gethostbyname().
• RES_PRIMARY — запрашивать только первичный сервер имен.
• RES_AAONLY — принимать только достоверные ответы. Функция res_send() продолжит работать, пока не найдет достоверный ответ или вернет ошибку.
• RES_KEEPTSIG — не обрезать записи TSIG.
• RES_BLAST — посылать каждый запрос одновременно и рекурсивно всем серверам.
• RES_USEBSTRING — выполнить поиск обратной записи IPv6 с помощью формата значимых битов, описанного в RFC 2673. По умолчанию параметр не задан, поэтому используется полубайтовый формат.
• RES_NOIP6DOTINT — использовать зону ip6.arpa при поиске обратной записи IPv6 вместо ip6.int.
Флаги RES_PRF отвечают за получение и вывод утилитой dig различных секций запроса и ответа к DNS.
Вряд ли они будут представлять практический интерес, но для примера приведем некоторые из них:
• RES_PRF_STATS — вывести статистику в конце ответа, например время на запрос, имя DNS-сервера и размер сообщения.
• RES_PRF_CLASS — вывести класс записи, например IN для Internet.
• RES_PRF_CMD — вывести командную строку запроса.
• RES_PRF_QUES — вывести секцию Question.
• RES_PRF_ANS — вывести секцию Answer.
• RES_PRF_TTLID — вывести значение TTLS в записях.
Включить опции в утилите dig можно следующим образом:
➭ dig ya.ru +qu
; <<>> DiG 9.18.13 <<>> ya.ru +qu
;; global options: +cmd
...
;; QUESTION SECTION:
;ya.ru.
Так как большинство флагов включено, более вероятно, что их придется отключать. Это делается через префикс «no»:
➭ dig ya.ru +nocl +nost +nocmd +noque +nohead
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 393
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; ANSWER SECTION:
ya.ru. 117 A 5.255.255.242
ya.ru. 117 A 77.88.55.242
Рассмотрение данных опций выходит за рамки книги. Подробности см. в man 1 dig.
Запрос данных записей по доменному имени выполняет функция res_nquery():
int res_nquery(res_state statep, const char *dname,
int class, int type,
unsigned char *answer, int anslen);
Параметры функции res_nquery():
• statep — состояние резолвера, инициализированное ранее через res_ninit().
• dname — доменное имя.
• class — класс пространства имен. Согласно RFC 1034 Domain Names — Concepts and facilities, это 16-битное значение, которое идентифицирует семейство протоколов или протокол. Например, IN — класс Internet.
• type — тип DNS-записи: A для адреса узла, CNAME — каноническое имя, NS — имя DNS-сервера домена и т.п.
• answer — буфер, в который будет помещен ответ.
• anslen — размер буфера answer в байтах.
В arpa/nameser.h определены следующие классы пространства имен:
// Значения для поля класса.
typedef enum __ns_class
{
// Cookie.
ns_c_invalid = 0,
// Internet.
ns_c_in = 1,
// Не поддерживается.
ns_c_2 = 2,
// MIT Chaos-net.
ns_c_chaos = 3,
// MIT Hesiod.
ns_c_hs = 4,
//
// Значения классов, не появляющиеся в ресурсных записях.
//
// Для предварительных разделов в запросах на обновление.
ns_c_none = 254,
// Любая запись, шаблон.
ns_c_any = 255,
ns_c_max = 65536
} ns_class;
Если вам когда-либо придется использовать функции из этого раздела, скорее всего, значение параметра class всегда будет ns_c_in.
Количество значений параметра type значительно больше. Среди них интересны прежде всего следующие:
• ns_t_a — запись с адресом IPv4.
• ns_t_aaaa — адрес IPv6.
• ns_t_cname — каноническое имя.
• ns_t_ns — адрес сервера, отвечающего за доменную зону.
• ns_t_mx — адрес почтового шлюза домена.
• ns_t_ptr — соответствие адреса имени. Требуется для обратного DNS-запроса.
• ns_t_soa — информация о доменной зоне. SOA-запись, которая содержит такие сведения, как адрес электронной почты администратора, дата обновления домена и т.д.
• ns_t_srv — указание на местоположение серверов для служб. Подробнее см. в RFC 2782 A DNS RR for specifying the location of services (DNS SRV).
• ns_t_txt — произвольные данные. Часто используется такими сервисами, как Let's Encrypt, для авторизации домена.
Функция res_nsearch() отправляет запрос и ждет ответа подобно res_nquery(), но при этом учитывает правила работы и поиска по умолчанию:
int res_nsearch(res_state statep,
const char *dname, int class, int type,
unsigned char *answer, int anslen);
Правила настраиваются через RES_DEFNAMES и RES_DNSRCH параметра options структуры, на которую указывает statep.
Функция res_nquerydomain() отправляет запрос с помощью res_nquery() с объединенными name и domain.
int res_nquerydomain(res_state statep,
const char *name, const char *domain,
int class, int type, unsigned char *answer,
int anslen);
Функция res_nmkquery() используется внутри res_nquery() и является процедурой низкого уровня. Она создает сообщение-запрос в buf длиной buflen для имени домена dname:
int res_nmkquery(res_state statep,
int op, const char *dname, int class,
int type, const unsigned char *data, int datalen,
const unsigned char *newrr,
unsigned char *buf, int buflen);
Параметры функции res_nmkquery():
• statep — состояние резолвера.
• op — тип запроса:
• QUERY — стандартный запрос.
• IQUERY — обратный запрос. Удален в glibc 2.26, так как давно не поддерживался серверами DNS.
• NS_NOTIFY_OP — уведомить об изменении SOA (Start of Authority) вторичный сервер.
• dname — доменное имя.
• class — класс пространства имен.
• type — тип записи.
• data — данные, которые будут добавлены в запрос.
• datalen — размер данных в байтах.
• newrr — не используется.
• buf — буфер, в котором будет сформирован запрос.
• buflen — размер буфера в байтах.
Функция res_nsend() отправляет заранее созданный запрос, указанный в msg, длиной msglen и возвращает ответ в буфере answer длиной anslen. Вызывает функцию res_ninit(), если это еще не сделано.
int res_nsend(res_state statep,
const unsigned char *msg, int msglen,
unsigned char *answer, int anslen);
Функция dn_comp() сжимает имя домена. Сжатие достигается за счет использования указателей в тексте для повторяющихся строк или символов в доменном имени, то есть функция удаляет компоненты имени.
Функция dn_expand() раскрывает сжатое имя домена до полного доменного имени.
Сжатое имя может содержаться в запросе или ответном сообщении.
#include <resolv.h>
int dn_comp(const char *exp_dn, unsigned char *comp_dn,
int length, unsigned char **dnptrs,
unsigned char **lastdnptr);
int dn_expand(const unsigned char *msg,
const unsigned char *eomorig,
const unsigned char *comp_dn, char *exp_dn,
int length);
Параметры функции dn_comp():
• exp_dn — имя домена.
• comp_dn — буфер для сжатого имени.
• length — длина буфера comp_dn.
• dnptrs — массив указателей на предварительно сжатые компоненты имени в текущем сообщении. Первый указатель содержит адрес начала сообщения. Список оканчивается нулевым указателем.
• lastdnptr — предел массива. Если dnptrs нулевой, имя домена не является сжатым.
Функция вставляет метки в сообщение по мере сжатия имени, но если список указателей на метки нулевой, функция не сжимает имя, а преобразует его из ASCII во внутренний формат. Если lastdnptr нулевой, список меток не обновляется.
Параметры dn_expand():
• msg — начало сообщения, содержащее сжатое имя.
• eomorig — указатель на первый адрес после сообщения; по сути, аналог метода end() в векторе C++.
• comp_dn — сжатое имя домена.
• exp_dn — буфер, в который будет сохранено полное имя.
• length — длина буфера exp_dn.
Функция возвращает размер сжатого имени либо –1 при ошибке.
Рассмотрим пример, в котором сначала вызовем функцию dn_comp() для сжатия имени:
int main()
{
// Буфер для сжатого имени.
std::array<unsigned char, 256> buf;
unsigned char *dnptrs = nullptr;
unsigned char *lastdnptr = nullptr;
// Сжать доменное имя.
if (auto result = dn_comp("www.google.com", buf.data(), buf.size(),
&dnptrs, &lastdnptr); result != -1)
{
std::cout << "Len = " << result << "\n"
<< std::string(buf.begin(), buf.begin() + result) << "\n"
<< "DP = "
<< (dnptrs ? reinterpret_cast<const char *>(dnptrs) : "nullptr")
<< "\n"
<< "LDP = "
<< (lastdnptr ? reinterpret_cast<const char *>(lastdnptr) :
"nullptr")
<< std::endl;
Затем вызовем функцию dn_expand() для его распаковки из сообщения и получения имени в обычном несжатом виде:
// Буфер для распакованного доменного имени.
std::array<char, 256> exp_buf;
// Получить доменное имя в exp_buf.
if ((result = dn_expand(buf.data(), buf.data() + result, buf.data(),
exp_buf.data(), exp_buf.size())) != -1)
{
std::cout
<< "Len = " << result << "\n"
<< std::string(exp_buf.begin(), exp_buf.begin() + result)
<< std::endl;
return EXIT_SUCCESS;
}
perror("dn_expand");
return EXIT_FAILURE;
}
perror("dn_comp");
return EXIT_FAILURE;
}
Запустим пример:
➭ build/bin/b01-ch02-dn-comp-expand
Len = 16
wwwgooglecom
DP = nullptr
LDP = nullptr
Len = 16
www.google.com
Видно, что размер буфера, который использовала функция dn_comp(), составляет 16 байт, тогда как размер выведенной строки — 12 байт. Функция использовала оставшиеся 4 байта для хранения служебной информации, по которой вызов функции dn_expand() восстановил доменное имя.
Устаревшие функции
Существуют устаревшие функции, прототипы которых здесь приведены для справки. Их отличие от используемых только в том, что они не принимают явный параметр состояния.
Что они делают, понятно из описания функций выше:
extern struct __res_state _res;
int res_init(void);
int res_query(const char *dname, int class, int type,
unsigned char *answer, int anslen);
int res_search(const char *dname, int class, int type,
unsigned char *answer, int anslen);
int res_querydomain(const char *name, const char *domain,
int class, int type, unsigned char *answer,
int anslen);
int res_mkquery(int op, const char *dname, int class,
int type, const unsigned char *data, int datalen,
const unsigned char *newrr,
unsigned char *buf, int buflen);
int res_send(const unsigned char *msg, int msglen,
unsigned char *answer, int anslen);
Данные функции используют общую глобальную переменную, и поэтому они не являются реентерабельными и потокобезопасными.
Модуль реализует преобразование имен согласно RFC 3490 «Internationalizing Domain Names» in Applications и RFC 3491 «Nameprep: A Stringprep Profile for Internationalized Domain Names». Эти RFC описывают протокол для поддержки символов, отличных от ASCII, в доменных именах, то есть кодирование в Punycode и stringprep и обратно: преобразование в кодировку, совместимую с ASCII, — ASCII-compatible encoding, или ACE.
В модуле socket производится прозрачное преобразование имен узлов Unicode в ACE и обратно. Поэтому обычно приложениям не нужно беспокоиться о преобразовании имен.
Другие модули Python, работающие с сетью, также выполняют преобразования незаметно для пользователя. Но при получении имен узлов из сети, например, при обратном поиске имен, автоматическое преобразование в Unicode не выполняется, поэтому приложения, выполняющие подобные задачи, должны декодировать имена в Unicode.
Модуль encodings.idna содержит функции для выполнения данной задачи:
# Вернуть подготовленное имя.
def encodings.idna.nameprep(label)
# Вернуть значение, преобразованное в ASCII с помощью метода Punycode.
def encodings.idna.ToASCII(label)
# Вернуть значение, преобразованное в Unicode.
def encodings.idna.ToUnicode(label)
Рассмотрим примеры вызова функций:
>>> import encodings.idna
>>> encodings.idna.ToASCII('www.сайт.рф')
b'xn--www..-7ve7dxcfmy'
>>> encodings.idna.ToUnicode(b'xn--www..-7ve7dxcfmy')
'www.сайт.рф'
>>> encodings.idna.nameprep('www.сайт.рф')
'www.сайт.рф'
>>> encodings.idna.nameprep('www.Сайт.рф')
'www.сайт.рф'
Внимание! Если требуется использовать стандарт IDNA 2008 из «RFC 5891 Internationalized Domain Names in Applications (IDNA): Protocol» и RFC 5895 «Mapping Characters for Internationalized Domain Names in Applications (IDNA) 2008», пользуйтесь .
Реализуем приложение, которое получает IP-адрес ресурса по его доменному имени.
В коде ниже класс SocketWrapper позволяет получить строку ошибки. Показаны два варианта получения адреса:
• через getaddrinfo();
• через gethostbyname(), которая не работает с IPv6 и как в POSIX, так и в не рекомендована к использованию.
Сначала реализуем первый вариант разрешения имен, используя getaddrinfo().
Получим список адресов:
int print_ips_with_getaddrinfo(const std::string &host_name)
{
// Для инициализации в ОС Windows.
socket_wrapper::SocketWrapper sock_wrap;
std::cout
<< "Getting name for \"" << host_name << "\"...\n"
<< "Using getaddrinfo() function." << std::endl;
addrinfo hints =
{
.ai_flags= AI_CANONNAME,
// Неважно, IPv4 или IPv6.
.ai_family = AF_UNSPEC,
// Потоковые сокеты, TCP-службы.
.ai_socktype = SOCK_STREAM,
// Любой протокол.
.ai_protocol = 0
};
// Результат.
addrinfo *s_i = nullptr;
if (int status = getaddrinfo(host_name.c_str(), nullptr, &hints, &s_i);
status != 0)
{
std::cerr
<< "getaddrinfo error: " << gai_strerror(status)
<< std::endl;
return EXIT_FAILURE;
}
// Нужно, чтобы автоматически освободить память.
const socket_wrapper::AddrInfoResult servinfo(s_i, freeaddrinfo);
Затем обойдем его в цикле:
for (auto const *s = servinfo; s != nullptr; s = s->ai_next)
{
std::cout << "Canonical name: ";
if (s->ai_canonname)
std::cout << s->ai_canonname;
std::cout << "\n";
assert(s->ai_family == s->ai_addr->sa_family);
std::cout << "Address type: ";
Выполним обработку IPv4-адресов:
if (AF_INET == s->ai_family)
{
// Это – адрес IPv4 длиной максимум INET_ADDRSTRLEN.
std::array<char, INET_ADDRSTRLEN> ip;
const sockaddr_in* const sin =
reinterpret_cast<const sockaddr_in* const>(s->ai_addr);
std::cout
<< "AF_INET\n"
<< "Address length: "
<< sizeof(sin->sin_addr) << "\n";
std::cout
<< "IP Address: "
<< inet_ntop(AF_INET, &(sin->sin_addr), ip.data(), ip.size())
<< "\n";
}
Аналогично обработаем адреса IPv6:
else if (AF_INET6 == s->ai_family)
{
// Это адрес IPv6.
std::array<char, INET6_ADDRSTRLEN> ip6;
// Для хранения адреса IPv6 используется sockaddr_in6.
sockaddr_in6 const * const sin =
reinterpret_cast<const sockaddr_in6* const>(s->ai_addr);
std::cout
<< "AF_INET6\n"
<< "Address length: " << sizeof(sin->sin6_addr) << "\n"
<< "IP Address: "
<< inet_ntop(AF_INET6, &(sin->sin6_addr), ip6.data(),
ip6.size())
<< "\n";
}
else
{
std::cout << s->ai_family << "\n";
}
std::cout << std::endl;
}
std::cout << std::endl;
freeaddrinfo(servinfo);
return EXIT_SUCCESS;
}
Заголовочные файлы не показаны, к тому же часть из них включена в библиотеку socket_wrapper.
И устаревший вариант, с использованием gethostbyname(). В старом коде он еще может встретиться.
Вызовем функцию и выполним проверку на ошибки:
// Устаревший вариант работает только для IPv4.
int print_ips_with_gethostbyname(const std::string &host_name)
{
socket_wrapper::SocketWrapper sock_wrap;
std::cout
<< "Getting name for \"" << host_name << "\"...\n"
<< "Using gethostbyname() function." << std::endl;
socket_wrapper::SocketWrapper sock_wrap;
const hostent *remote_host { gethostbyname(host_name.c_str()) };
if (nullptr == remote_host)
{
if (sock_wrap.get_last_error_code())
{
std::cerr
<< sock_wrap.get_last_error_string()
<< std::endl;
}
return EXIT_FAILURE;
}
std::cout
<< "Official name: " << remote_host->h_name << "\n";
Затем обработаем возвращенные адреса в цикле:
for (const char* const* p_alias =
const_cast<const char* const*>(remote_host->h_aliases);
*p_alias; ++p_alias)
{
std::cout
<< "# Alternate name: \"" << *p_alias
<< "\"\n";
}
std::cout << "Address type: ";
// Это проверять не обязательно.
if (AF_INET == remote_host->h_addrtype)
{
std::cout
<< "AF_INET\n";
<< "\nAddress length: "
<< remote_host->h_length << "\n";
in_addr addr = {0};
for (int i = 0; remote_host->h_addr_list[i]; ++i)
{
addr.s_addr = *reinterpret_cast<const u_long* const>(
remote_host->h_addr_list[i]);
std::cout << "IP Address: " << inet_ntoa(addr) << "\n";
}
}
Строку «AF_INET6» вы не увидите никогда, так как функция gethostbyname() работает только с IPv4-адресами:
// Событие ниже никогда не случится.
// gethostbyname() является устаревшей функцией.
// Она не работает с IPv6.
else if (AF_INET6 == remote_host->h_addrtype)
{
std::cout << "AF_INET6\n";
}
else
{
std::cout << remote_host->h_addrtype << "\n";
}
std::cout << std::endl;
return EXIT_SUCCESS;
}
Вызовем обе функции в main():
int main(int argc, const char *argv[])
{
if (argc != 2)
{
std::cout << "Usage: " << argv[0] << " <hostname>" << std::endl;
return EXIT_FAILURE;
}
const std::string host_name = { argv[1] };
print_ips_with_getaddrinfo(host_name);
// Задержка в 10 мс, чтобы сервер отдавал примерно одинаковые списки
// адресов, не задействуя механизмы балансировки.
std::this_thread::sleep_for(std::chrono::milliseconds(10));
print_ips_with_gethostbyname(host_name);
return EXIT_SUCCESS;
}
Результат вызова:
➭ build/bin/b01-ch02-dns-resolver google.com
Getting name for "google.com"...
Using getaddrinfo() function.
Canonical name: google.com
Address type: AF_INET
Address length: 4
IP Address: 74.125.205.102
Canonical name:
Address type: AF_INET
Address length: 4
IP Address: 74.125.205.113
...
Canonical name:
Address type: AF_INET
Address length: 4
IP Address: 74.125.205.138
Canonical name:
Address type: AF_INET6
Address length: 16
IP Address: 2a00:1450:4010:c02::64
...
Canonical name:
Address type: AF_INET6
Address length: 16
IP Address: 2a00:1450:4010:c02::8a
Getting name for "google.com"...
Using gethostbyname() function.
Official name: google.com
Address type: AF_INET
Address length: 4
IP Address: 74.125.205.102
IP Address: 74.125.205.113
IP Address: 74.125.205.101
IP Address: 74.125.205.139
IP Address: 74.125.205.100
IP Address: 74.125.205.138
Первый вызов вернул не только IPv4, но и IPv6-адреса. Заметим, что если слишком часто делать запросы к google.com, результат будет отличаться, так как балансировщик нагрузки предлагает разные сервера. Чтобы этого избежать, в примере используется задержка в 10 мс.
Посмотрим, как реализовать резолвер на Python. Первый вариант — через getaddrinfo():
import os
import socket
import sys
import time
def print_ips_with_getaddrinfo(host_name: str):
print(f'Getting name for "{host_name}"...\nUsing getaddrinfo() function.')
# Любой тип адреса и протокола.
addrs = socket.getaddrinfo(host_name, None,
family=socket.AF_UNSPEC,
type=socket.SOCK_STREAM,
flags=socket.AI_CANONNAME, proto=0)
# ai_type, ai_proto не используются, но оставлены для наглядности.
for (ai_family, ai_type, ai_proto, ai_canonname, ai_addr) in addrs:
print(f'Canonical name: {ai_canonname}\n'
f'Address type: '
f'{str(ai_family).replace("AddressFamily.", "")}\n'
f'IP Address: {ai_addr[0]}')
И второй через gethostbyname():
def print_ips_with_gethostbyname(host_name: str):
print(f'Getting name for "{host_name}"...\n'
f'Using gethostbyname() function.')
# Аналог через hostbyname_ex()
official_name, alias_list, address_list = \
socket.gethostbyname_ex(host_name)
print(f'Official name: "{official_name}"')
for alias in alias_list:
print(f'Alias: "{alias}"')
for addr in address_list:
print(f'IP Address: "{addr}"')
Вызовем их:
if '__main__' == __name__:
if len(sys.argv) != 2:
print(f'Usage: {sys.argv[0]} <hostname>')
sys.exit(os.EX_USAGE)
print_ips_with_getaddrinfo(sys.argv[1])
# Задержка.
time.sleep(1)
print()
print_ips_with_gethostbyname(sys.argv[1])
sys.exit(os.EX_OK)
В результате запуска получим следующее:
➭ src/book01/ch02/python/dns-resolver.py google.com
Getting name for "google.com"...
Using getaddrinfo() function.
Canonical name: google.com
Address type: AF_INET
IP Address: 74.125.205.101
Canonical name:
Address type: AF_INET
IP Address: 74.125.205.113
Canonical name:
Address type: AF_INET
...
Address type: AF_INET
IP Address: 74.125.205.138
Canonical name:
Address type: AF_INET6
IP Address: 2a00:1450:4010:c0b::8a
Canonical name:
Address type: AF_INET6
IP Address: 2a00:1450:4010:c0b::64
Canonical name:
...
Getting name for "google.com"...
Using gethostbyname() function.
Official name: "google.com"
IP Address: "74.125.205.113"
...
IP Address: "74.125.205.138"
IP Address: "74.125.205.101"
Если приложение написано с использованием IPv4 и необходимо перевести его на IPv6, потребуется выполнить определенные шаги:
1. Прежде всего желательно использовать getaddrinfo() для получения адресной информации, а не инициализировать структуры вручную. Это сохранит независимость от версии IP и избавит от необходимости выполнять многие последующие шаги.
2. Все участки кода с жесткой кодировкой чего-либо, связанного с версией IP, необходимо обернуть вспомогательной функцией.
3. Изменить AF_INET на AF_INET6.
4. Изменить PF_INET на PF_INET6.
5. Изменить INADDR_ANY в присваиваниях на IN6ADDR_ANY.
6. Вместо sockaddr_in использовать sockaddr_in6. Там, где это необходимо, добавить «6» в имена полей.
7. Аналогичное sin_zero поле sin6_zero отсутствует, и обращения к первому необходимо удалить.
8. Вместо in_addr использовать in6_addr.
9. Вместо inet_aton() или inet_addr() использовать inet_pton().
10. Вместо inet_ntoa() использовать inet_ntop().
11. Вместо gethostbyname() использовать getaddrinfo().
12. Вместо gethostbyaddr() использовать функцию getnameinfo(), хотя gethostbyaddr() может работать с IPv6.
13. INADDR_BROADCAST больше не работает. Вместо него можно использовать многоадресную рассылку IPv6.
14. В bind() и везде, где непонятен тип и размер адреса, необходимо использовать sockaddr_storage.
Большинство из этих действий выполнять не потребуется, если не использовать устаревшие функции или нерекомендованные практики.
Замена адреса выполняется следующим образом:
sockaddr_in sa;
sockaddr_in6 sa6;
// Любой IPv4-адрес (0.0.0.0). Или отсутствие привязки к сетевому интерфейсу.
sa.sin_addr.s_addr = INADDR_ANY;
// Аналог для IPv6-адреса.
sa6.sin6_addr = IN6ADDR_ANY;
При определении переменной типа in6_addr значение IN6ADDR_ANY_INIT можно использовать в качестве инициализатора:
in6_addr ia6 = IN6ADDR_ANY_INIT;
С точки зрения сокетов ничего больше не изменилось. Детали и подробности для более глубокого изучения описаны в и .
Простая замена IPv4 на IPv6 приведет к тому, что приложение будет работать только с IPv6. А это часто не то, что требуется разработчику.
Поэтому многие ОС поддерживают Dual Stack, позволяющий обрабатывать пакеты IPv4 и IPv6 одновременно.
Стек протоколов с двойной поддержкой изображен на рис. 2.5.
Рис. 2.5. Двойной TCP/IP-стек
Модуль протокола сетевого уровня определяется по идентификатору протокола.
Проверить наличие поддержки двойного стека в Python можно через функцию has_dualstack_ipv6() модуля socket:
➭ python3 -c 'import socket; print(socket.has_dualstack_ipv6())'
True
Данная функция реализована через установку опции IPV6_V6ONLY в 0 и проверку результата. По умолчанию опция установлена в 0, что означает включение двойного стека, то есть сокет будет принимать как IPv4-, так и IPv6-подключения. В ином случае IPv6-сокет будет работать только с IPv6.
В Linux значение опции по умолчанию берется из файла /proc/sys/net/ipv6/bindv6only.
Убедимся, что если функция вернула True, следующий код будет работать как с IPv4, так и с IPv6:
import socket
with socket.socket(
socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP
) as sock:
sock.bind(('::', 12345, 0, 0))
print(f'Listening socket = {sock}, '
f'dual stack = {socket.has_dualstack_ipv6()}\n')
while True:
s, addr = sock.accept()
print(f'Client socket = {s}\nClient address = {addr}')
while data := s.recv(15):
print(data)
Теперь отправим данные с IPv6-сокета, а затем с IPv4-сокета:
import socket
# Отправить пакет через IPv6-сокет.
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.sendto(('::', 12345), b'test ipv6')
sock.close()
# Отправить пакет через IPv4-сокет.
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.sendto(('127.0.01', 12345), b'test ipv4')
sock.close()
Мы рассмотрим функции обмена данными в следующих главах. Сейчас нам важно, что функции отправки принимают адреса, а функция приема возвращает адрес удаленного абонента.
Сервер принимает данные, отправленные клиентом выше:
➭ src/book01/ch02/python/dualstack-server.py
Listening socket = <socket.socket fd=3, family=10, type=2, proto=17, laddr=('::', 12345, 0, 0)>, dual stack = True
(b'test ipv6', ('::1', 50457, 0, 0))
(b'test ipv4', ('::ffff:127.0.0.1', 48376, 0, 0))
С первым клиентом все ясно: ::1 — адрес IPv6 локального узла.
Для второго клиента сервер видит адрес IPv6, хотя клиент использует IPv4: '::ffff:127.0.0.1'. Это происходит потому, что на сервере был создан IPv6-сокет.
Формат адреса вида "::FFFF:<IPv4 address>" используется в IPv6 для отображения пространства IPv4-адресов в IPv6.
Для IPv6 также можно использовать рассматриваемые в главе 5 функции create_server() и create_connection() примерно следующим образом:
import socket
addr = ('', 12345)
if socket.has_dualstack_ipv6():
s = socket.create_server(addr, family=socket.AF_INET6,
dualstack_ipv6=True)
else:
s = socket.create_server(addr)
Очевидно, что вышесказанное относится и к C++. Обмен данными через IPv6-сокеты работает так же, как и через сокеты IPv4, так как протоколы и абстракции, лежащие уровнем выше, во многом одинаковы.
Обратиться к сокету извне можно по адресу, привязать который позволяет функция bind().
Пользователи часто оперируют разными форматами IP-адресов. Например, IPv4-адрес обычно выглядит как "192.168.2.1". В этом формате пользователь как вводит адреса, так и ожидает увидеть их в выводе, например в журнале ошибок.
Но IP-адрес — это число, которое может быть представлено структурой. Для преобразования IP-адресов в разные форматы существует набор функций.
Для конвертации адреса из читаемой формы в sockaddr используется функция inet_pton(), а для конвертации из sockaddr в читаемую форму — inet_ntop().
В Python конвертацию адресов, а также другие манипуляции с адресами и сетями удобнее выполнять через модуль стандартной библиотеки ipaddress.
Хорошо написанные приложения, удобные и понятные для пользователей, кроме IP-адресов могут обрабатывать доменные имена, такие как .
Имя можно преобразовать в адрес на локальной машине, используя статические записи. Но обычно узел не знает всех IP-адресов, соответствующих всем именам в интернете, поэтому для разрешения имен он обращается к серверам. Одному доменному имени может соответствовать множество IP-адресов. Система доменных имен, благодаря которой работают доменные имена в интернете, называется DNS.
За DNS-сервер, как правило, отвечает организация.
Для работы с базой узлов с помощью DNS предназначены функции gethostent() и sethostent(). Для получения списка адресов по имени используется функция getaddrinfo(). Для получения списка имен по адресу — getnameinfo().
Чтобы изменить порядок работы с DNS или тонко настроить его работу, существует целый набор функций типа res_*() и функции dn_comp(), dn_expand(). Они выполняют запросы и обрабатывают ответы, полученные от серверов доменных имен.
В Python существует модуль encodings.idna, который реализует преобразование имен.
Из-за быстрого роста количества сетевых устройств интернет переходит на протокол IPv6, у которого огромное пространство адресов. Но IPv4 до сих пор популярен, поэтому многие ОС поддерживают Dual Stack для обработки пакетов IPv4 и IPv6 одновременно. Двойной стек предоставляет возможность плавного перехода от IPv4 к IPv6.
1. Какая структура является общей для различных форматов адреса?
2. Что представляет собой структура, хранящая IPv4-адрес? Какие у нее атрибуты?
3. Чем отличается IPv6-адрес? Какие у него атрибуты?
4. Для чего нужна структура sockaddr_storage?
5. Как правильно заполнить структуру IP-адреса?
6. Для чего нужно привязывать адрес к сокету? Какая функция используется для привязки?
7. Допустимо ли использовать функцию bind() для клиента?
8. Как преобразовать IP-адрес в печатную форму? Как преобразовать строку адреса в структуру?
9. Что будет, если в функцию inet_pton() передать доменное имя?
10. Допустимо ли использовать функцию inet_ntoa() вместо inet_ntop()? В каких условиях и почему?
11. Какой модуль в Python используется для работы с IP-адресами? Как узнать, что адрес принадлежит интернет-диапазону? Покажите на примере, как из адреса по маске извлечь адрес подсети.
12. Что такое доменное имя? Для чего оно нужно?
13. Какая система и как поддерживает работу с доменными именами?
14. Может ли одному доменному имени соответствовать несколько IP-адресов и почему?
15. Возможна ли обратная ситуация, когда одному IP-адресу соответствует множество доменных имен?
16. Для чего предназначены функции gethostent() и sethostent()?
17. Как преобразовать доменное имя в IP-адрес?
18. Какая функция используется для получения списка доменных имен по IP-адресу? В чем особенности ее работы?
19. Как обработать ситуацию, когда функция getaddrinfo() не может разрешить доменное имя?
20. В каких случаях может потребоваться изменение порядка работы с DNS или его настройка?
21. Какие функции используются для выполнения запросов к DNS? А для обработки ответов?
22. В каком модуле Python реализовано преобразование доменных имен?
23. Какие задачи выполняет сторонний пакет Python idna?
24. Что такое Dual Stack?
25. Каким образом проводится миграция сети с IPv4 на IPv6 при использовании Dual Stack?
26. Какой тип адресации используется для узлов, работающих в режиме Dual Stack?
27. Какие проблемы могут возникнуть при работе с Dual Stack IPv4 и IPv6 на устройствах IoT?
28. С помощью написанной ранее функции получения списка адресов по имени проведите анализ доступности популярных веб-сайтов, например google.com, facebook.com. Используйте инструменты командной строки, такие как nslookup и dig, для сравнения результатов, получаемых вашей программой, со стандартными методами определения адресации.
29. Добавьте в клиент возможность адресовать сервер как по адресам IPv4 и IPv6, так и по доменному имени.
)