А-а-а! Этот интерфейс ужасен.
Линус Торвальдс, новостная группа fa.linux.kernel, 2007
Сокеты служат не только для обмена данными между приложениями, но и для управления поведением операционной системы. Для этого в ядре Linux и некоторых Unix-подобных ОС, таких как FreeBSD, имеются сокеты домена AF_NETLINK — IPC-механизм (Inter-Process Communication, механизм межпроцессного взаимодействия), используемый в основном для получения доступа к сетевому API ядра из процессов пользовательского пространства. Этот интерфейс показан на рис. 11.1.
Он был разработан как гибкая замена ioctl и предоставляет интерфейсы конфигурации ядра и мониторинга, связанные с сетью. Например, команда ip в Linux активно использует подсистему Netlink. И это важное различие с командой ifconfig, использующей ioctl.
Рис. 11.1. Подсистема Netlink
Контроллер nlctrl на рис. 11.1 — это «метасемейство» Genetlink, предоставляющее информацию обо всех семействах Netlink-сокетов, зарегистрированных в ядре. Данные сокеты описаны в man 7 netlink, man 7 rtnetlink, man 3 netlink, man 3 rtnetlink и в RFC 3549 «Linux Netlink as an IP Services Protocol».
В том виде, как она известна сейчас, подсистема Netlink была спроектирована и реализована Алексеем Кузнецовым, одним из ключевых разработчиков ядра Linux, внесшим значительный вклад в разработку сетевой подсистемы.
Ранее это было символьное устройство, реализованное Аланом Коксом.
В данной главе мы рассмотрим основные концепции и принципы работы сокетов Netlink. Узнаем, как обработать подключение или отключение интерфейсов, изменение состояния ссылок и другие события. А также научимся выполнять через них запросы к ядру и получать ответы. В конце главы узнаем про альтернативы в других ОС.
Предоставляемый сокетами Netlink API включает следующие основные части:
• Интерфейс на основе сокетов для взаимодействия из пользовательского пространства с ядром и модулями ядра.
• Набор макросов для Netlink-сокетов и для сокетов управления маршрутизацией — NETLINK_ROUTE.
• Пользовательские библиотеки libnl, libmnl, libnetlink и прочие, а также пакеты на Python, например pyroute2.
Сокет Netlink создается следующим образом:
#include <asm/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
int netlink_socket = socket(AF_NETLINK, socket_type, netlink_family);
Надежная передача данных от ядра к пользователю в общем случае невозможна. Например, ядро не может отправить сообщение, если буфер сокета полон: сообщение будет удалено.
Сокеты Netlink оперируют дейтаграммами, поэтому для типа сокета допустимы значения SOCK_RAW или SOCK_DGRAM — они ничем не различаются.
Протокол определяет модуль ядра, с которым будет производиться взаимодействие.
К сети имеют отношение следующие типы сокетов:
• NETLINK_ROUTE — с помощью этого сокета можно получить обновления таблиц маршрутизации и каналов. Также он позволяет изменять таблицы маршрутизации ядра, настройки кэша соседей и многие другие параметры сети.
• NETLINK_NFLOG — журнал Netfilter уровня пользователя.
• NETLINK_W1 — сообщения подсистемы 1-wire.
• NETLINK_USERSOCK — зарезервирован для протоколов сокетов пользовательского режима.
• NETLINK_SOCK_DIAG — запрашивает у ядра ОС информацию о сокетах различных семейств.
• NETLINK_XFRM — фреймворк для преобразования пакетов, например, для шифрования их полезной нагрузки. Используется для реализации набора протоколов IPsec, протокола сжатия полезной нагрузки IP и функций мобильного IPv6.
• NETLINK_ISCSI — поддержка iSCSI, используемого для получения доступа к СХД.
• NETLINK_FIB_LOOKUP — позволяет из пользовательского пространства искать адреса в FIB.
• NETLINK_NETFILTER — подсистема сетевого фильтра.
• NETLINK_RDMA — управление RDMA, то есть удаленным доступом к памяти для шины InfiniBand.
• NETLINK_IP6_FW — получение IPv6-пакетов из сетевого фильтра в пространство пользователя. Используется модулем ядра ip6_queue.
• NETLINK_DNRTMSG — получение сообщений маршрутизации DECnet.
• NETLINK_KOBJECT_UEVENT — получение сообщений ядра в пространстве пользователя.
• NETLINK_INET_DIAG — запрос информации о сокетах различных семейств протоколов из ядра.
• NETLINK_GENERIC — фреймворк, упрощающий использование Netlink для взаимодействия компонентов ядра.
• NETLINK_FIREWALL — устаревший тип, который использовался для передачи IPv4-пакетов из сетевого фильтра на пользовательский уровень.
Пример создания сокета RTNetlink:
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>
int rtnetlink_socket = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
Константа NETLINK_ROUTE задает тип Netlink API.
Сокеты Netlink имеют тип протокола SOCK_DGRAM. Это ненадежный транспорт, не дающий гарантий: сообщения могут быть отброшены при ошибке передачи, а повторная отправка данных не выполняется.
Если пользовательское приложение допустит переполнение буфера, вовремя не получив данные из сокета, то любая следующая операция с сокетом вернет ошибку ENOBUFS «No buffer space available».
При получении ошибки недостаточного места в буфере приложение должно закрыть сокет и создать его заново.
Переполнение буфера возможно не только со стороны приложения, но и со стороны ядра. Поэтому для повышения надежности и подтверждения того, что ядро получило и обработало отправленные ему данные, можно затребовать подтверждение при отправке пакета, выставив в нем флаг NLM_F_ACK. Тогда ядро отправит в ответ сообщение типа NLMSG_ERROR с полем ошибки, установленным в 0, если иначе запрос не возвращает никаких данных.
Для пакетов с ошибкой ядро будет пытаться отправлять сообщение NLMSG_ERROR с ненулевым кодом.
Адрес клиента Netlink как в пространстве пользователя, так и в ядре описывается структурой sockaddr_nl. Структура поддерживает как адреса «точка-точка», так и группы многоадресной рассылки:
struct sockaddr_nl
{
// AF_NETLINK.
sa_family_t nl_family;
// Заполнение нулями.
unsigned short nl_pad;
// Индивидуальный адрес в процессе, который владеет сокетом.
pid_t nl_pid;
// Маска multicast группы.
__u32 nl_groups;
};
Поле nl_family всегда AF_NETLINK, а nl_pad всегда нулевое. Остальные поля содержат компоненты адреса:
• nl_pid — идентификатор процесса, который владеет сокетом Netlink.
• nl_groups — битовая маска, представляющая номера групп Netlink. Это поле поддерживает многоадресные рассылки.
Каждое семейство Netlink имеет набор из 32 групп многоадресной рассылки. При вызове bind() поле nl_groups должно содержать маску групп, которые процесс собирается прослушивать.
Значение поля, равное 0, означает, что многоадресные рассылки приниматься не будут. Типы групп могут быть объединены через условие «ИЛИ».
Например, следующие типы групп получают уведомления об изменении:
• RTMGRP_LINK — сетевых интерфейсов: удалении, добавлении, включении или выключении;
• RTMGRP_IPV4_IFADDR — IPv4-адресов интерфейсов;
• RTMGRP_IPV6_IFADDR — IPv6-адресов интерфейсов;
• RTMGRP_IPV4_ROUTE — таблиц маршрутизации для IPv4;
• RTMGRP_IPV6_ROUTE — таблиц маршрутизации IPv6.
Когда сообщение отправляется с nl_pid, равным 0, оно адресует ядро или получателя, находящегося в ядре. Для процесса пользовательского пространства nl_pid обычно представляет собой уникальный идентификатор процесса, чаще всего PID процесса, создавшего данный сокет.
Внимание! Идентификатор Netlink-сокета nl_pid всегда должен быть уникален, поэтому если процесс владеет несколькими сокетами Netlink, идентификатор будет отличаться от PID.
Установить nl_pid можно через вызов bind(). В этом случае приложение должно обеспечивать его уникальность.
Несмотря на то что pid_t, определенный в ядре, занимает в памяти 4 байта, как и поле nl_pid в пакете Netlink, максимально возможный pid процесса меньше, чем максимальное значение uint32. Это позволяет использовать в процессе старшие байты этого поля для задания уникальных значений nl_pid нескольким сокетам, сохраняя идентификатор процесса. Подробно реализацию можно увидеть в функции generate_local_port(), входящей в состав библиотеки libnl, которая будет рассмотрена в следующей главе.
Сама функция достаточно объемная, и нас интересует только ее суть:
static uint32_t generate_local_port()
{
uint32_t n;
static uint16_t idx_state = 0;
uint32_t pid = getpid() & 0x3FFFFF;
// Сначала формируется случайный ненулевой idx_state.
...
int i = idx_state >> 5;
n = idx_state;
for (int j = 0; j < 32; j++)
{
// Далее выполняется рандомизированный проход по индексу,
// при этом блок 0 остается последним.
...
for (int m = 0; m < 32; m++)
{
...
// PID_MAX_LIMIT — максимальный номер PID примем равным 2^22.
// Остается 10 бит, то есть 1024 уникальных порта на приложение.
// Убедиться, что не будет возвращен 0.
pid = pid + (n << 22);
return pid ? pid : 1024;
}
}
return 0;
}
В функции выше также для проверки того, что порт не использован, существует карта использованных портов, но ее формирование мы не приводим. Тот же прием используется во многих других библиотеках.
Если же идентификатор не привязывается явно либо задается нулевой, ядро назначит уникальный идентификатор автоматически.
В современных ядрах Linux функцию bind() можно вызывать повторно, и это можно использовать, чтобы найти следующее свободное значение поля nl_pid для данного процесса.
Сообщения, которыми производится обмен, состоят из потока байтов: заголовков nlmsghdr, за которыми следует полезная нагрузка.
struct nlmsghdr
{
// Длина сообщения, включая заголовок.
__u32 nlmsg_len;
// Тип содержимого.
__u16 nlmsg_type;
// Дополнительные флаги.
__u16 nlmsg_flags;
// Номер последовательности.
__u32 nlmsg_seq;
// Идентификатор порта отправителя.
__u32 nlmsg_pid;
};
Этот общий заголовок Netlink показан на рис. 11.2. Обычно за ним следует поле семейства «адресов», но частью заголовка оно не является, и его может не быть.
Далее следуют поля, зависящие от протокола.
Рис. 11.2. Общий заголовок Netlink
Если сообщений несколько, они могут быть объединены в пакеты, изображенные на рис. 11.3.
Рис. 11.3. Структура Netlink-сообщений
Поле nlmsg_len равно sizeof(nlmsghdr) + длина тела сообщения.
Поле nlmsg_type определяет тип сообщения:
• NLMSG_NOOP — игнорировать сообщение.
• NLMSG_ERROR — сигнал об ошибке. Тело сообщения — структура nlmsgerr.
• NLMSG_DONE — завершить составное сообщение.
Поле nlmsg_flags содержит управляющие флаги:
• NLM_F_REQUEST — это сообщение запроса.
• NLM_F_MULTI — сообщение является частью составного сообщения. В составных сообщениях первый и все последующие заголовки имеют установленный флаг NLM_F_MULTI, за исключением последнего заголовка, который имеет тип NLMSG_DONE.
• NLM_F_ACK — запрос подтверждения успеха.
• NLM_F_ECHO — повторить этот запрос в ответе.
Запросы бывают разных типов, например GET для получения объектов или NEW для создания объектов. Для разных типов запросов определены дополнительные биты флагов.
Для GET:
• NLM_F_ROOT — вернуть полную таблицу вместо одного элемента.
• NLM_F_MATCH — вернуть соответствия критерию, заданному в сообщении.
• NLM_F_ATOMIC — вернуть атомарный слепок указанной таблицы.
• NLM_F_DUMP — NLM_F_ROOT | NLM_F_MATCH.
Для NEW:
• NLM_F_REPLACE — заменить существующий объект.
• NLM_F_EXCL — не заменять существующий объект.
• NLM_F_CREATE — создать объект, если он не существует.
• NLM_F_APPEND — добавить объект в конец списка объектов.
Поле nlmsg_pid показывает источник сообщения, но как мы поняли, nlmsg_pid и PID процесса не всегда равны.
Пример тела сообщения об ошибке:
struct nlmsgerr
{
// Отрицательная errno или 0 для подтверждения.
int error;
// Заголовок сообщения, вызвавший ошибку.
struct nlmsghdr msg;
// За этим заголовком следует содержимое сообщения,
// если не был установлен NETLINK_CAP_ACK
// или ACK не указывает на успех, то есть поле error == 0.
};
По возможности любая полезная нагрузка должна быть представлена как атрибуты, содержащие длину, тип и данные. Их структура показана на рис. 11.4.
Рис. 11.4. Атрибут Netlink
Структурирование данных как атрибутов позволяет в будущем расширять конкретные протоколы Netlink без нарушения совместимости.
В Linux заголовки атрибутов представляются структурой:
#include <linux/netlink.h>
struct nlattr
{
// Длина.
__u16 nla_len;
// Тип атрибута и флаги.
__u16 nla_type;
};
При этом два старших бита типа — флаги, которые задают особенности хранения атрибутов:
• N — этот атрибут содержит вложенные атрибуты.
• O — данные атрибута хранятся в сетевом порядке байтов.
Сразу за структурой заголовка может следовать заполнение нулями до размера выравнивания, а затем данные полезной нагрузки.
Сообщения, получаемые через сокеты типа NETLINK_ROUTE, называются сообщениями RTNetlink. Они используются для работы с подсистемами ядра, отвечающими за настройки сети. С помощью NETLINK_ROUTE можно добавлять, удалять и изменять IP-адреса, маршруты, записи в таблицах ARP и FIB.
Параметры соединения, дисциплины очередей, классы трафика и классификаторы пакетов тоже управляются через сокеты NETLINK_ROUTE. Также этот протокол используется для создания, удаления и настройки сетевых интерфейсов и процедур управления трафиком. Для каждой из этих целей в рамках протокола NETLINK_ROUTE используются разные сообщения:
• RTM_NEWLINK, RTM_DELLINK, RTM_GETLINK — создать либо изменить, удалить или получить информацию о конкретном сетевом интерфейсе. Эти сообщения содержат структуру ifinfomsg, за которой следует ряд структур rtattr.
• RTM_NEWADDR, RTM_DELADDR, RTM_GETADDR — добавить либо изменить, удалить или получить информацию об IP-адресе, связанном с интерфейсом. Интерфейс может иметь несколько IP-адресов. Параметр сообщений — структура ifaddrmsg, за которой могут следовать атрибуты маршрутизации rtattr.
• RTM_NEWROUTE, RTM_DELROUTE, RTM_GETROUTE — создать, изменить, удалить или получить информацию о сетевом маршруте. Эти сообщения содержат структуру rtmsg, за которой могут следовать атрибуты.
• RTM_NEWNEIGH, RTM_DELNEIGH, RTM_GETNEIGH — создать либо изменить, удалить или получить информацию о записи таблицы соседей, находящихся в одном коллизионном домене, например запись ARP. Сообщение содержит структуру ndmsg.
• RTM_NEWRULE, RTM_DELRULE, RTM_GETRULE — создать либо изменить, удалить или получить правило маршрутизации. Параметр сообщений — структура rtmsg.
• Сообщения управления трафиком. Параметр сообщений — структура tcmsg:
• RTM_NEWQDISC, RTM_DELQDISC, RTM_GETQDISC — создать либо изменить, удалить или получить дисциплину очередей. За параметром может следовать ряд атрибутов.
• RTM_NEWTCLASS, RTM_DELTCLASS, RTM_GETTCLASS — создать либо изменить, удалить или получить класс трафика.
• RTM_NEWTFILTER, RTM_DELTFILTER, RTM_GETTFILTER — создать либо изменить, удалить или получить информацию о фильтре трафика.
Из них мы рассмотрим только некоторые. За подробностями обращайтесь к man 7 rtnetlink.
Структура сообщений RTM_*LINK показана на рис. 11.5.
Рис. 11.5. Сообщения RTM_*LINK
Структура rtattr — опциональные атрибуты, следующие после начального заголовка:
struct rtattr
{
// Длина опции.
unsigned short rta_len;
// Тип опции.
unsigned short rta_type;
// За этим заголовком следуют данные.
};
Сообщения для работы с адресами показаны на рис. 11.6.
Рис. 11.6. Сообщения типа RTM_*ADDR
На рис. 11.7 показан заголовок сообщений для управления маршрутами в сетевом стеке Linux через сокеты Netlink и получения информации о них. Эти сообщения позволяют приложениям взаимодействовать с таблицами маршрутизации.
Рис. 11.7. Сообщения типа RTM_*ROUTE
Поле типа дистанции, или rtm_scope:
• RT_SCOPE_UNIVERSE — глобальный маршрут.
• RT_SCOPE_SITE — внутренний маршрут в местной автономной системе.
• RT_SCOPE_LINK — маршрут на данном соединении.
• RT_SCOPE_HOST — маршрут на локальном хосте.
• RT_SCOPE_NOWHERE — пункт назначения не существует.
Для понимания того, что такое поле типа маршрута, или rtm_type, укажем его значения:
• RTN_UNSPEC — неизвестный тип маршрута.
• RTN_UNICAST — шлюз или прямой маршрут.
• RTN_LOCAL — маршрут через локальный интерфейс.
• RTN_BROADCAST — широковещательный маршрут, широковещательные сообщения.
• RTN_ANYCAST — широковещательный маршрут, одноадресная рассылка.
• RTN_MULTICAST — многоадресный маршрут.
• RTN_BLACKHOLE — «маршрут», отбрасывающий пакеты.
• RTN_UNREACHABLE — недостижимый пункт назначения.
• RTN_PROHIBIT — маршрут, отклоняющий пакеты.
• RTN_THROW — продолжить поиск маршрута в другой таблице.
• RTN_NAT — правило трансляции сетевых адресов.
• RTN_XRESOLVE — относится к внешнему преобразователю и не реализовано.
• rtm_protocol — происхождение маршрута.
Подробнее о взаимодействии полей типа маршрута и дистанции см. в man.
Доступ к потоку байтов должен осуществляться только с помощью стандартных макросов NLMSG_* или API-библиотек, таких как libnetlink. Эти макросы описаны в man 3 rtnetlink и, в некоторых системах, в man 4 rtnetlink.
Рассмотрим основные макросы, определенные в linux/netlink.h:
#include <asm/types.h>
#include <linux/netlink.h>
int NLMSG_ALIGN(size_t len);
int NLMSG_LENGTH(size_t len);
int NLMSG_SPACE(size_t len);
void *NLMSG_DATA(nlmsghdr *nlh);
nlmsghdr *NLMSG_NEXT(nlmsghdr *nlh, int len);
int NLMSG_OK(nlmsghdr *nlh, int len);
int NLMSG_PAYLOAD(nlmsghdr *nlh, int len);
Они похожи на макросы, которые используются для работы со вспомогательными данными:
• NLMSG_ALIGN() — округлить длину сообщения Netlink в большую сторону, до выровненного. Параметр макроса — длина сообщения.
• NLMSG_LENGTH() — получить выровненную длину для сохранения в поле nlmsg_len структуры nlmsghdr. Использует внутри себя NLMSG_ALIGN(), чтобы получить выровненный размер заголовка nlmsghdr. Принимает размер поля данных, то есть размер сообщения минус размер заголовка.
• NLMSG_SPACE() — получить размер буфера, который заняло бы сообщение Netlink с данными размером len.
• NLMSG_PAYLOAD() — получить длину полезных данных, связанных с nlmsghdr.
• NLMSG_DATA() — получить указатель на начало данных, связанных с заголовком nlmsghdr.
• NLMSG_NEXT() — получить указатель на следующий заголовок nlmsghdr в составном сообщении. Перед вызовом необходимо проверить, не установлен ли флаг NLMSG_DONE в текущем nlmsghdr, потому что макрос не возвращает nullptr в конце. Аргумент len представляет собой lvalue, содержащее оставшуюся длину буфера сообщений. Вызов этого макроса уменьшает его на длину заголовка сообщения.
• NLMSG_OK() — вернуть true, если сообщение Netlink не усечено и корректно.
Для манипулирования атрибутами RTNetlink существует набор макросов:
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>
int RTA_OK(rtattr *rta, int rtabuflen);
void *RTA_DATA(rtattr *rta);
unsigned int RTA_PAYLOAD(rtattr *rta);
rtattr *RTA_NEXT(rtattr *rta, unsigned int rtabuflen);
unsigned int RTA_LENGTH(unsigned int length);
unsigned int RTA_SPACE(unsigned int length);
Не только сообщения RTNetlink могут содержать атрибуты. В частности, подсистема SOCK_DIAG, рассмотренная ниже, использует атрибуты в том же формате.
Рассмотрим макросы подробнее:
• RTA_OK() — вернуть истину, если rta указывает на допустимый атрибут маршрутизации. Параметр rtabuflen — текущая длина буфера атрибутов. Если макрос возвращает false, считаем, что в сообщении больше нет атрибутов, даже если attrlen отличен от нуля.
• RTA_DATA() — получить указатель на начало данных этого атрибута.
• RTA_PAYLOAD() — получить длину данных этого атрибута.
• RTA_NEXT() — получить следующий атрибут после rta. Вызов этого макроса обновит rtabuflen. Для проверки корректности возвращенного указателя следует использовать макрос RTA_OK.
• RTA_LENGTH() — получить длину, необходимую для len байт данных вместе с заголовком.
• RTA_SPACE() — получить размер буфера, который заняло бы сообщение с данными размером len.
Рассмотрим, как происходит установка MTU устройства через сокет RTNetlink.
Сначала требуется создать новое сообщение:
extern "C"
{
#include <linux/rtnetlink.h>
}
...
const auto iface_index = if_nametoindex(argv[1]);
const unsigned int mtu = std::stoi(argv[2]);
struct
{
// Заголовок.
struct nlmsghdr nh;
// Сообщение.
struct ifinfomsg if_msg;
// Буфер под атрибуты.
char attrbuf[512];
} req = {0};
// Сокет RTNetlink.
const int rtnetlink_sk = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
if (rtnetlink_sk < 0)
{
// Ошибка.
...
}
// Длина сообщения вместе с заголовком.
req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(ifinfomsg));
// Это сообщение — запрос.
req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
// Изменить сетевой интерфейс.
req.nh.nlmsg_type = RTM_NEWLINK;
// Семейство адресов интерфейса.
req.if_msg.ifi_family = AF_UNSPEC;
// Его индекс.
req.if_msg.ifi_index = iface_index;
// Флаги интерфейса в ifi_flags, которые должны быть изменены.
// Например, IF_UP.
// Так как флаги не меняются, значение 0.
req.if_msg.ifi_change = 0;
Теперь добавим к нему требуемый атрибут, в который будет записано возвращаемое значение, и выполним запрос:
// Отсюда начинаются атрибуты. Учитывается сдвиг на выравнивание.
rtattr *rta = reinterpret_cast<rtattr *>(reinterpret_cast<char *>(&req) +
NLMSG_ALIGN(req.nh.nlmsg_len));
// Атрибут — MTU.
rta->rta_type = IFLA_MTU;
// Установить длину атрибута: ненулевая длина означает, что атрибут
// существует.
rta->rta_len = RTA_LENGTH(sizeof(unsigned int));
// Добавить эту длину к длине сообщения.
req.nh.nlmsg_len = NLMSG_ALIGN(req.nh.nlmsg_len) + RTA_LENGTH(sizeof(mtu));
std::copy_n(reinterpret_cast<const char*>(&mtu), sizeof(mtu),
static_cast<char*>(RTA_DATA(rta)));
// Отправить запрос.
if (send(rtnetlink_sk, &req, req.nh.nlmsg_len, 0) < 0)
{
...
}
Запустив код, увидим, что он изменяет MTU:
➭ ip a show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65535 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
➭ sudo build/bin/b01-ch11-netlink-mtu lo 1230
➭ ip a show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 1230 qdisc noqueue state UNKNOWN group default qlen 1000
Работать с RTNetlink напрямую достаточно сложно: надо правильно выставлять длины, учитывать выравнивание, затем отдельно читать ответ для проверки корректности запроса, что не показано в примере.
Подсистема SOCK_DIAG предоставляет механизм получения от ядра информации о сокетах различных семейств. Через нее возможно запросить список сокетов или получить информацию о конкретном сокете.
Подсистема SOCK_DIAG была введена с целью предоставления информации для ss без использования ProcFS.
NETLINK_INET_DIAG был введен в Linux 2.6 и поддерживал только сокеты AF_INET и AF_INET6.
В Linux 3.3 он был переименован в NETLINK_SOCK_DIAG и была добавлена поддержка сокетов AF_UNIX.
При запросе списка можно указать фильтры, применяемые ядром для выбора подмножества сокетов. Имеется возможность фильтровать сокеты по состоянию: подключен, прослушивается и т.д.
Для получения информации о конкретном сокете можно указать, какие данные требуется получить.
Внимание! SOCK_DIAG сообщает только о тех сокетах, у которых есть адрес, то есть для которых был вызван bind(), connect() либо адрес был назначен автоматически.
Доступ к этой подсистеме возможен через сокеты Netlink:
#include <sys/socket.h>
#include <linux/sock_diag.h>
// Для сокетов домена Unix.
#include <linux/unix_diag.h>
// Для сокетов IPv4 и IPv6.
#include <linux/inet_diag.h>
int diag_socket = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG);
Чтобы получить информацию от системы, требуется сделать запрос, начинающийся с Netlink-заголовка nlmsghdr со следующими параметрами:
• Поле nlmsg_type установлено в SOCK_DIAG_BY_FAMILY.
• В nlmsg_flags установлен флаг NLM_F_DUMP, если запрашивается список сокетов. Если флаг не установлен, ответ на запрос будет включать информацию об отдельном сокете.
За основным заголовком следует заголовок запроса sock_diag:
struct sock_diag_req
{
// Семейство адресов, например AF_INET.
__u8 sdiag_family;
// Конкретный протокол, зависящий от семейства адресов.
// Например, IPPROTO_TCP.
__u8 sdiag_protocol;
};
После общих заголовков следуют заголовки, специфичные для семейства адресов.
Структура либо определяет запрос списка сокетов, состояние которых указано флагами в поле udiag_states, либо идентифицирует конкретный сокет:
struct unix_diag_req
{
// Семейство: AF_UNIX.
__u8 sdiag_family;
// Всегда 0.
__u8 sdiag_protocol;
// Заполнение. Всегда 0.
__u16 pad;
// Используется для запроса групп сокетов.
// 1 << TCP_ESTABLISHED — для получения сокетов, через которые установлено
// соединение.
// 1 << TCP_LISTEN — для прослушивающих сокетов.
__u32 udiag_states;
// Номер inode в файловой системе, при запросе индивидуального сокета.
// Игнорируется при запросе списка.
__u32 udiag_ino;
// Флаги, определяющие, какую информацию получить.
__u32 udiag_show;
// Идентификаторы, которые вместе с udiag_ino могут использоваться для
// определения индивидуального сокета.
// Игнорируются при запросе списка.
// Если установлены в -1, также игнорируются.
__u32 udiag_cookie[2];
};
Информацию, которую необходимо получить, определяют флаги данной структуры, установленные в поле udiag_show:
• UDIAG_SHOW_NAME — путь к файлу сокета.
• UDIAG_SHOW_VFS — устройство и inode, что полезно для групп сокетов.
• UDIAG_SHOW_PEER — inode «удаленного» сокета, подключенного к указанному.
• UDIAG_SHOW_ICONS — inode сокетов, на которых вызвана connect(), но еще не вызвана accept().
• UDIAG_SHOW_RQLEN — ответ содержит два поля:
• Для прослушивающих сокетов — количество ожидающих соединений. Для подключенных сокетов — количество данных в очереди приема.
• Длина очереди, которая была задана при вызове listen(), или объем доступной памяти в буфере передачи.
• UDIAG_SHOW_MEMINFO — массив 32-битных значений, содержащих информацию о сокете. Подробно описан в man 7 sock_diag.
• UNIX_DIAG_SHUTDOWN — биты состояния SHUT_RD или SHUT_WR для сокета.
Общий заголовок для типа SOCK_DIAG, а также поля запроса для INET_DIAG показаны на рис. 11.8.
Рис. 11.8. Заголовок для запроса типа SOCK_DIAG и часть запроса INET_DIAG
Структура аналогична той, которая используется для запроса Unix-сокетов, но фильтры и адресация Internet-сокетов отличаются:
struct inet_diag_req_v2
{
// Семейство: AF_INET, AF_INET6.
__u8 sdiag_family;
// Протокол: IPPROTO_TCP, IPPROTO_UDP, IPPROTO_UDPLITE.
__u8 sdiag_protocol;
// Флаги, которые задают дополнительную информацию для получения.
__u8 idiag_ext;
// Заполнение. Всегда 0.
__u8 pad;
// Фильтр состояния сокета. Применяется в случае запроса списка.
// Будут получены только сокеты, которые подходят под фильтр.
// Когда запрашивается информация о конкретном сокете, поле игнорируется.
__u32 idiag_states;
// Структура, идентифицирующая конкретный интернет-сокет.
struct inet_diag_sockid id;
};
Структура, идентифицирующая сокет, содержит адреса, порты, интерфейс, связанные с сокетом:
struct inet_diag_sockid
{
// Порт источника.
__be16 idiag_sport;
// Порт назначения.
__be16 idiag_dport;
// Адрес источника.
__be32 idiag_src[4];
// Адрес назначения.
__be32 idiag_dst[4];
// Номер связанного интерфейса.
// Если сокет не привязан к интерфейсу, имеет значение 0.
__u32 idiag_if;
// Игнорируются при запросе списка.
// Если установлены в -1, также игнорируются.
__u32 idiag_cookie[2];
};
Внимание! Поля структуры inet_diag_sockid должны быть записаны в сетевом порядке байтов.
Флаги для получения расширенной информации содержатся в поле idiag_ext:
• INET_DIAG_TOS — вернется байт, содержащий значение TOS.
• INET_DIAG_TCLASS — для IPv6-сокета вернется байт, содержащий TClass. Для сокетов в состоянии LISTEN и CLOSE за этим байтом следует атрибут INET_DIAG_SKV6ONLY с байтом, указывающим, является сокет только IPv6 или нет.
• INET_DIAG_MEMINFO — возвращает структуру inet_diag_meminfo, содержащую:
• idiag_rmem — размер данных в очереди приема.
• idiag_wmem — размер неотправленных данных в очереди передачи TCP.
• idiag_fmem — размер памяти, запланированный TCP для будущего использования.
• idiag_tmem — размер данных в очереди передачи.
• INET_DIAG_SKMEMINFO — массив четырехбайтных значений, содержащих информацию о памяти сокета. Подробнее см. в man.
• INET_DIAG_INFO — для сокетов TCP возвращает структуру tcp_info. Для сокетов другого типа будет возвращена другая структура.
• INET_DIAG_CONG — возвращает строку, которая содержит название используемого алгоритма управления перегрузкой TCP.
• INET_DIAG_SHUTDOWN — для сокетов TCP возвращает байт, содержащий биты состояния SHUT_RD, SHUT_WR, SHUT_RDWR или 0.
• INET_DIAG_VEGASINFO — для сокетов TCP возвращает структуру, которая содержит информацию по алгоритму Vegas, используемому для контроля перегрузки.
Некоторые расширения пока нельзя запросить через Sock_diag, потому что поле флагов имеет размер 1 байт и флагов может быть всего восемь.
Внимание! Описание значения флагов отличается от аналогичного для Unix-сокетов. Значения начинаются с единицы. Нулевое значение — INET_DIAG_NONE. Это необходимо учитывать при установке флагов, вычитая из бита флага единицу. Проверка же флагов производится с использованием значений без вычета единицы.
Поле idiag_states устанавливается аналогично udiag_states для Unix-сокетов. Для TCP флаги могут принимать следующие значения, которые соответствуют различным состояниям протокола:
• TCP_ESTABLISHED.
• TCP_SYN_SENT.
• TCP_FIN_WAIT1.
• TCP_FIN_WAIT2.
• TCP_CLOSE_WAIT.
• TCP_LAST_ACK.
• TCP_CLOSING.
Поля *_cookie содержат уникальный для системы 64-битный идентификатор сокета.
Например, утилита ss выводит его после "sk:" и его значение — 4046:
➭ ss -e -4 --tos|head -2
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp ESTAB 0 0 192.168.2.13:54654 91.103.66.241:https
ino:6525419 sk:4046 cgroup:/user.slice/user-1000...scope <-> tos:0
class_id:0
В ответ подсистема вернет массив сообщений Netlink, предваряемых заголовками. За сообщениями могут следовать атрибуты, работа с которыми осуществляется через макросы RTNetlink. Типы этих атрибутов для Unix-сокетов обозначены макросами, например UNIX_DIAG_NAME. Следовательно, достаточно убрать «_SHOW» из макроса запроса, а UDIAG заменить на UNIX_DIAG.
Заголовок для Unix-сокетов:
struct unix_diag_msg
{
__u8 udiag_family;
// SOCK_PACKET, SOCK_STREAM или SOCK_SEQPACKET.
__u8 udiag_type;
// TCP_LISTEN или TCP_ESTABLISHED.
__u8 udiag_state;
__u8 pad;
// Номер inode.
__u32 udiag_ino;
__u32 udiag_cookie[2];
};
Для Internet-сокетов макросы ответов называются так же, как и макросы запросов, то есть на запрос INET_DIAG_TOS придет атрибут с типом INET_DIAG_TOS. А заголовок сообщения описывается следующей структурой:
struct inet_diag_msg
{
// Семейство.
__u8 idiag_family;
// Флаг состояния как в запросе.
__u8 idiag_state;
// Тип активного таймера для TCP-сокетов.
__u8 idiag_timer;
// Количество попыток повторной передачи.
__u8 idiag_retrans;
struct inet_diag_sockid id;
// Количество миллисекунд до истечения таймера для TCP-сокетов.
__u32 idiag_expires;
// Количество ожидающих соединений для прослушивающих сокетов.
// Или размер данных в очереди чтения.
__u32 idiag_rqueue;
// Для прослушивающих сокетов — размер очереди,
// указанный при вызове listen().
// В ином случае — размер очереди для отправки.
__u32 idiag_wqueue;
// Идентификатор пользователя, который владеет сокетом.
__u32 idiag_uid;
// Номер inode сокета в файловой системе.
__u32 idiag_inode;
};
Структуры, возвращаемые при указании флага INET_DIAG_INFO, такие как tcp_info, определены в заголовочных файлах ядра. Например, последняя определена в linux/tcp.h и содержит множество внутренней информации: метрики, размеры окна, количество отправленных пакетов, которые еще не были подтверждены, и т.п.
Внимание! Структура tcp_info определена и в файле netinet/tcp.h. Это разные структуры. Они не взаимозаменяемы. Именно в данном случае нужно использовать структуру из заголовочного файла в каталоге linux, потому что подсистема специфична для Linux и пользуется структурами ядра.
Рассмотрим пример, в котором реализован просмотр TCP-сокетов в системе, то есть часть функциональности утилиты ss. Выполним реализацию, используя подсистему sock_diag.
Как уже говорилось, важно использовать правильные заголовочные файлы, хотя в коде мы увидим, что программа будет проверять размер ответа, поэтому использование неверной структуры приведет к явной ошибке:
extern "C"
{
#include <unistd.h>
#include <sys/socket.h>
#include <asm/types.h>
#include <net/if.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/sock_diag.h>
#include <linux/inet_diag.h>
// Использовать специфичный для Linux заголовочный файл:
#include <linux/tcp.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// Этот заголовочный файл нельзя использовать!
// #include <netinet/tcp.h>
}
...
// Удобная структура, описывающая заголовок для запроса sock_diag
// об интернет-сокетах.
struct netlink_request
{
struct nlmsghdr nlh;
struct inet_diag_req_v2 irh;
};
В функции отправки запроса еще раз обратите внимание на то, как устанавливаются флаги структуры.
Для получения списка IPv6-сокетов или сокетов разных транспортных протоколов требуются отдельные запросы с другими параметрами.
В коде ниже будут получены только TCP-сокеты IPv4. Сначала инициализируем структуры запроса:
void send_query(int fd)
{
// "Адрес" Netlink.
sockaddr_nl nladdr =
{
.nl_family = AF_NETLINK
};
// Структура запроса.
netlink_request req =
{
.nlh =
{
.nlmsg_len = sizeof(req),
// Это запрос sock_diag по семейству адресов.
.nlmsg_type = SOCK_DIAG_BY_FAMILY,
// NLM_F_REQUEST говорит о том, что это запрос.
// NLM_F_DUMP говорит о том, что запрашивается список.
.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP
},
.irh =
{
// Сокеты типа AF_INET.
.sdiag_family = AF_INET,
// Интересуют TCP-сокеты.
.sdiag_protocol = IPPROTO_TCP,
// Дополнительно запросить TOS и структуру tcp_info.
.idiag_ext = 1 << (INET_DIAG_TOS — 1) | 1 << (INET_DIAG_INFO — 1),
.pad = 0,
// Сейчас запрашиваются сокеты в любом состоянии,
// что эквивалентно комбинации флагов:
// 1 << TCP_ESTABLISHED | 1 << TCP_SYN_SENT | \
// 1 << TCP_FIN_WAIT1| 1 << TCP_FIN_WAIT2 | 1 << TCP_CLOSE_WAIT \
// 1 << TCP_LAST_ACK | 1 << TCP_CLOSING
.idiag_states = static_cast<__u32>(-1),
.id = {0}
}
};
Создадим новое сообщение и выполним запрос через send_msg():
// Данные сообщения.
struct iovec iov = { .iov_base = &req, .iov_len = sizeof(req) };
// Сообщение.
struct msghdr msg =
{
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_controllen = 0,
.msg_flags = 0
};
// Отправка сообщения, в случае EINTR ее следует повторить.
while (true)
{
if (sendmsg(fd, &msg, 0) < 0)
{
if (EINTR == errno) continue;
throw std::system_error(errno, std::generic_category(),
"sendmsg");
}
break;
}
}
Функция print_response() получает ответы и вызывает функцию, выводящую их на печать.
Сначала прочитаем ответ через recvmsg():
void print_responses(int fd)
{
long buf[4096];
sockaddr_nl nladdr;
// Указывает на буфер с данными принимаемого сообщения.
iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) };
int flags = 0;
while (true)
{
// Сообщение.
msghdr msg =
{
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1
};
ssize_t ret = recvmsg(fd, &msg, flags);
if (ret < 0)
{
// Как всегда, проверяем EINTR.
if (EINTR == errno) continue;
throw std::system_error(errno, std::generic_category(),
"recvmsg");
}
if (0 == ret) return;
Выполним несколько проверок:
if (nladdr.nl_family != AF_NETLINK)
{
throw std::logic_error("nl_family != AF_NETLINK");
}
const nlmsghdr *h = reinterpret_cast<const nlmsghdr *>(buf);
// Проверка на корректность обязательна,
// ведь транспорт в Netlink ненадежный.
if (!NLMSG_OK(h, ret))
{
throw std::system_error(errno, std::generic_category(),
"not NLMSG_OK");
}
Далее в цикле будем выполнять разбор ответов:
for (; NLMSG_OK(h, ret); h = NLMSG_NEXT(h, ret))
{
// Последнее сообщение в серии.
if (NLMSG_DONE == h->nlmsg_type) return;
// Сообщение будет содержать ошибку, которую вернула подсистема.
if (NLMSG_ERROR == h->nlmsg_type)
{
const nlmsgerr *err = static_cast<const nlmsgerr*>(
NLMSG_DATA(h));
// Проверить длину.
if (h->nlmsg_len < NLMSG_LENGTH(sizeof(*err)))
{
throw std::system_error(errno, std::generic_category(),
"NLMSG_ERROR");
}
else
{
// Можно установить errno, но код ошибки,
// возвращаемый подсистемой, меньше нуля.
errno = -err->error;
throw std::system_error(errno, std::generic_category(),
"NLMSG_ERROR");
}
}
// Теперь проверим, что это нужный ответ.
if (h->nlmsg_type != SOCK_DIAG_BY_FAMILY)
{
throw std::system_error(errno, std::generic_category(),
"unexpected nlmsg_type");
}
// Наконец, можем выполнить преобразование и вызвать функцию
// печати ответа.
print_diag(static_cast<const inet_diag_msg*>(NLMSG_DATA(h)),
h->nlmsg_len);
}
}
}
Функция печати ответа сначала выводит поля структуры inet_diag_msg:
void print_diag(const inet_diag_msg *diag, unsigned int len)
{
// Базовые проверки.
if (len < NLMSG_LENGTH(sizeof(*diag)))
{
throw std::system_error(errno, std::generic_category(),
"short response");
}
if (diag->idiag_family != AF_INET && diag->idiag_family != AF_INET6)
{
throw std::logic_error("unexpected protocol family");
}
std::string src_addr(INET_ADDRSTRLEN, 0);
std::string dst_addr(INET_ADDRSTRLEN, 0);
inet_ntop(diag->idiag_family, diag->id.idiag_src, &src_addr[0],
src_addr.size());
inet_ntop(diag->idiag_family, diag->id.idiag_dst, &dst_addr[0],
src_addr.size());
// Вывод адресов. Помним, что все поля — в сетевом порядке байтов.
std::cout
<< src_addr << ":" << ntohs(diag->id.idiag_sport)
<< " -> "
<< dst_addr << ":" << ntohs(diag->id.idiag_dport);
Если сокет привязан к интерфейсу, также выводится имя этого интерфейса:
// Если номер интерфейса не является нулевым, сокет привязан к интерфейсу.
if (diag->id.idiag_if)
{
// Получим название интерфейса, пользуясь уже изученной функцией.
std::unique_ptr<struct if_nameindex, decltype(&if_freenameindex)>
if_ni(if_nameindex(), &if_freenameindex);
if (nullptr == if_ni)
{
throw std::system_error(errno, std::generic_category(),
"if_nameindex");
}
std::string if_name;
for (struct if_nameindex *i = if_ni.get();
!(0 == i->if_index && nullptr == i->if_name); ++i)
{
if (i->if_index == diag->id.idiag_if)
{
if_name = i->if_name;
break;
}
}
std::cout << " [" << if_name << "]";
}
После этого при наличии атрибутов функция осуществляет их разбор и печать. Выполним эти операции в цикле. Сначала проверим ToS:
std::cout << ":\n";
unsigned int rta_len = len — NLMSG_LENGTH(sizeof(*diag));
tcp_info ti;
// Получение дополнительных атрибутов. Используются макросы RTNetlink.
for (const rtattr *attr = reinterpret_cast<const rtattr *>(diag + 1);
RTA_OK(attr, rta_len); attr = RTA_NEXT(attr, rta_len))
{
switch (attr->rta_type)
{
case INET_DIAG_TOS:
// Всегда необходимо проверять длину атрибута.
if (RTA_PAYLOAD(attr) != 1)
throw std::logic_error("TOS length error");
std::cout
<< " TOS: "
<< +*static_cast<const uint8_t*>(RTA_DATA(attr))
<< "\n";
break;
Выведем диагностическую информацию, такую как RTT и число недоставленных пакетов:
case INET_DIAG_INFO:
{
auto data_size = RTA_PAYLOAD(attr);
// Эта проверка выявит использование неправильной
// структуры tcp_info.
if (data_size != sizeof(tcp_info))
throw std::logic_error("tcp_info length error, check if "
"you use correct header "
"(linux/tcp.h)");
tcp_info ti;
auto data = static_cast<const uint8_t*>(RTA_DATA(attr));
std::copy(data, data + data_size,
reinterpret_cast<uint8_t*>(&ti));
std::cout
<< " Lost packets: " << ti.tcpi_lost << "\n"
<< " Retransmits: " << +ti.tcpi_retransmits << "\n"
<< " RTT: " << ti.tcpi_rtt << "\n";
break;
}
Часть атрибутов не была запрошена, но они в любом случае придут, например, контрольная группа или флаги вызова shutdown(), если он был вызван:
// Некоторые атрибуты не запрашивались, но они все равно приходят.
case INET_DIAG_SHUTDOWN:
if (RTA_PAYLOAD(attr) != 1)
throw std::logic_error("Shutdown flags length error");
std::cout
<< " Shutdown flags: "
<< + *static_cast<uint8_t*>(RTA_DATA(attr))
<< "\n";
break;
case INET_DIAG_CGROUP_ID:
if (RTA_PAYLOAD(attr) != sizeof(uint64_t))
throw std::logic_error("CGroup length error");
std::cout
<< " Control group: "
<< *static_cast<const uint64_t*>(RTA_DATA(attr))
<< "\n";
break;
case INET_DIAG_SOCKOPT:
if (RTA_PAYLOAD(attr) != sizeof(inet_diag_sockopt))
throw std::logic_error("Sockopt flags length error");
// Можно использовать поля структуры inet_diag_sockopt.
std::cout << " SOCKOPT\n";
break;
default:
std::cerr
<< " Unknown attribute: "
<< "0x" << std::hex << attr->rta_type
<< "\n";
}
}
Функция main() создает сокет Netlink, вызывает отправку запроса и получение ответов:
int main()
{
try
{
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG);
if (fd < 0)
{
throw std::system_error(errno, std::generic_category(), "socket");
}
try
{
send_query(fd);
print_responses(fd);
close(fd);
}
catch(...)
{
close(fd);
throw;
}
}
catch (const std::exception &e)
{
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
catch (...)
{
std::cerr << "Unknown exception!" << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Запустим пример:
➭ build/bin/b01-ch11-sock_diag
127.0.0.53:53 -> 0.0.0.0:0 [lo]:
Shutdown flags: 0
TOS: 0
Control group: 2800
SOCKOPT
Lost packets: 0
Retransmits: 0
RTT: 0
...
0.0.0.0:5355 -> 0.0.0.0:0:
Shutdown flags: 0
TOS: 0
Control group: 2800
SOCKOPT
Lost packets: 0
Retransmits: 0
RTT: 0
192.168.2.13:49968 -> 185.211.245.141:8434:
Shutdown flags: 0
TOS: 0
Control group: 55638
SOCKOPT
Lost packets: 0
Retransmits: 0
RTT: 11196
...
Вывод утилиты ss аналогичен:
➭ ss -4 -at
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 4096 127.0.0.53%lo:domain 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:37265 0.0.0.0:*
LISTEN 0 4096 127.0.0.54:domain 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:llmnr 0.0.0.0:*
ESTAB 0 0 192.168.2.13:49968 185.211.245.141:8434
ESTAB 0 0 192.168.2.13:38438 45.95.201.21:11443
ESTAB 0 0 192.168.2.13:38218 209.85.233.94:https
...
Linux — не единственная система, в которой реализован обмен между ядром и приложением через сокеты. Хотя сокеты Netlink и специфичны для Linux, но в некоторых других операционных системах разработчики пошли по аналогичному пути.
В NetBSD и OpenBSD реализованы сокеты домена PF_ROUTE, которые используются для управления таблицей маршрутизации. Сокеты поддерживают команды RTM_ADD, RTM_DELETE и подобные, что позволяет управлять маршрутами. Адреса, связанные с сообщениями о маршрутизации, передаются как двоичные данные в сообщении.
Также в BSD есть сокеты домена PF_SYSTEM. В частности, , то есть драйверами, в MacOS X реализовано через эти сокеты.
Для FreeBSD реализованы сокеты AF_NETLINK, как в Linux, поэтому данный интерфейс постепенно становится переносимым.
Во FreeBSD данные сокеты перенесены в 2021 году, начиная с версии 13, и сейчас они могут быть еще не доработаны.
Например, получать в MacOS X события интерфейсов можно так:
extern "C"
{
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <net/if.h>
#include <sys/kern_event.h>
}
int main(int argc, const char* const argv[])
{
// Сокет PF_SYSTEM создается для прослушивания событий.
int s = socket(PF_SYSTEM, SOCK_RAW, SYSPROTO_EVENT);
// Установим фильтр на события.
kev_request key;
key.vendor_code = KEV_VENDOR_APPLE;
key.kev_class = KEV_NETWORK_CLASS;
key.kev_subclass = KEV_ANY_SUBCLASS;
int code = ioctl(s, SIOCSKEVFILT, &key);
kern_event_msg msg;
// Цикл получения событий.
while (true)
{
// Обратите внимание, что здесь используется обычный recv().
code = recv(s, &msg, sizeof(msg), 0);
// Реакция на разные типы событий.
switch (msg.event_code)
{
case KEV_DL_IF_DETACHED:
// Интерфейс отсоединен.
break;
case KEV_DL_IF_ATTACHED:
// Интерфейс подсоединен.
break;
case KEV_DL_LINK_OFF:
// Интерфейс отключен.
break;
case KEV_DL_LINK_ON:
// Интерфейс подключен.
break;
}
}
return EXIT_SUCCESS;
}
Мы рассмотрели более современный интерфейс для управления сокетами, интерфейсами, устройствами и параметрами ядра, который пришел на замену вызовам ioctl, — сокеты домена AF_NETLINK.
Команда ip в Linux активно использует подсистему Netlink. И это важное отличие от устаревшей команды ifconfig.
API Netlink включает сокетный интерфейс, набор макросов и библиотеки. В этой главе мы рассмотрели первые два компонента.
Адреса Netlink представлены структурой sockaddr_nl, которая содержит уникальный идентификатор и список групп многоадресной рассылки.
Обмен производится сообщениями, у которых общий заголовок — структура nlmsghdr, включающая длину сообщения, его тип, флаги, номер последовательности для понимания того, на какое сообщение приходит ответ, и порт.
Сообщения разных типов имеют разную структуру. Мы рассмотрели NETLINK_ROUTE- и SOCK_DIAG-подсистемы.
Сокеты типа NETLINK_ROUTE предоставляют интерфейс RTNetlink. Он используется для работы с таблицами маршрутизации ядра, позволяя их получать и менять.
Работать с RTNetlink напрямую достаточно сложно: требуется правильно выставлять длины, учитывать выравнивание и отдельно читать ответ для проверки корректности запроса.
Подсистема SOCK_DIAG требуется для получения от ядра информации о сокетах различных семейств.
Через нее можно запросить список сокетов, указать фильтры для подмножества сокетов или получить информацию о конкретном сокете. Прежде всего эта подсистема дает возможность запрашивать данные интернет- и Unix-сокетов.
Хотя работа с сокетами Netlink требует определенных знаний и опыта, они предоставляют разработчикам широкий спектр возможностей для реализации сложных и высокопроизводительных сетевых приложений.
В дальнейшем, с развитием технологий и сетевых стандартов, важность и использование сокетов Netlink продолжит расти, что делает их изучение актуальным. Интерфейсы, подобные сокетам Netlink, появляются не только в Linux, но и в других ОС.
1. Что представляют собой сокеты Netlink?
2. Когда нужно использовать сокеты Netlink?
3. Почему сокеты Netlink пришли на замену ioctl?
4. Каково основное требование к идентификатору сокета Netlink?
5. Какой порядок байтов используется при записи данных в структуры сокета Netlink?
6. Зачем нужно перечисление групп многоадресной рассылки в адресе типа sockaddr_nl?
7. Какие типы сообщений могут быть отправлены через сокеты Netlink?
8. Что собой представляет сообщение Netlink?
9. Надежный ли транспорт предоставляет Netlink? Как обеспечивается надежность?
10. Что такое сообщения RTNetlink? Для чего они используются?
11. Как отправить сообщение в сокет RTNetlink?
12. Как интерпретировать данные ответа, пришедшие через сокет RTNetlink?
13. Как обрабатывается событие подключения или отключения интерфейса в коде, работающем с Netlink?
14. О каких типах сокетов предоставляет информацию подсистема SOCK_DIAG?
15. О каких сокетах не сообщит подсистема SOCK_DIAG?
16. Какие аналоги существуют у сокетов Netlink в других ОС?
17. Доработайте пример из главы, чтобы он показывал процесс, использующий сокет. Аналог ключа -t утилиты ss.