Надежное и прозрачное ПО обычно не отвечает интересам проектировщика.
Никлаус Вирт, «A Digital Contrarian Retires», 1999
Работать с сокетами Netlink напрямую достаточно сложно, а ошибку при этом допустить очень легко, поэтому важную роль для удобства работы с Netlink играют библиотеки. Среди них есть как сложные пакеты, так и легкие обертки, предоставляющие меньшую степень абстракции. Реализованы они на разных языках программирования.
В этой главе мы подробно рассмотрим функциональные возможности и API основных библиотек для работы c Netlink, а также их применение в разработке сетевых приложений.
Начнем с Libnl — достаточно большого пакета, далее рассмотрим более простые альтернативы.. Наконец, в завершение главы перейдем к пакетам Python.
— набор библиотек, покрывающий большую часть API Netlink. Он включает несколько библиотек, в которых реализованы подмножества API:
• libnl, или libnl-core, — основная библиотека, в которой реализованы построение и анализ сообщений, работа с сокетами Netlink, отправка и получение данных. От нее зависят все остальные библиотеки пакета.
• libnl-route — API RTNetlink для семейства адресов NETLINK_ROUTE.
• libnl-genl — API общего Netlink-протокола, расширенной версии протокола Netlink.
• libnl-nf — настройка сетевых фильтров и интерфейсов мониторинга.
• libnl-idiag — поддержка сокетов INET_DIAG.
Структура пакета libnl показана на рис. 12.1.
Кратко рассмотрим, что собой представляет API библиотеки. Узнать о нем подробнее можно из .
Рис. 12.1. Структура пакета libnl
В первую очередь для создания и разрушения сокета, используемого остальными функциями пакета, требуется API. Он реализован в библиотеке libnl-core:
#include <netlink/socket.h>
nl_sock *nl_socket_alloc();
void nl_socket_free(nl_sock *sk);
В структуре nl_sock библиотека хранит, например, счетчик порядковых номеров, обработка которых выполняется автоматически.
Счетчиком также можно управлять, используя функции библиотеки:
#include <netlink/socket.h>
unsigned int nl_socket_use_seq(nl_sock *sk);
void nl_socket_disable_seq_check(nl_sock *sk);
Функция nl_socket_use_seq() вернет текущее значение счетчика и увеличит его на единицу. А функция nl_socket_disable_seq_check() отключает контроль последовательности.
Для получения сообщений, приходящих в сокет, можно установить обработчик, используя функцию nl_socket_set_cb():
typedef int (*nl_recvmsg_msg_cb_t)(nl_msg *msg, void *arg);
typedef int (*nl_recvmsg_err_cb_t)(sockaddr_nl *nla, nlmsgerr *nlerr,
void *arg);
enum nl_cb_type
{
// Сообщение корректно.
NL_CB_DUMP_INTR,
__NL_CB_TYPE_MAX,
};
struct nl_cb
{
// Массив обработчиков по типам сообщений
nl_recvmsg_msg_cb_t cb_set[NL_CB_TYPE_MAX + 1];
// Аргументы пользователя, которые будут переданы обработчикам.
void *cb_args[NL_CB_TYPE_MAX + 1];
// Обработчики ошибок.
nl_recvmsg_err_cb_t cb_err;
// Пользовательские аргументы обработчиков.
void *cb_err_arg;
// Может использоваться для замены nl_recvmsgs() функцией пользователя.
int (*cb_recvmsgs_ow)(nl_sock *, nl_cb *);
// Пользовательская замена nl_recv().
// Должна возвращать число прочитанных и размещенных в буфере октетов.
int (*cb_recv_ow)(nl_sock *, sockaddr_nl *, unsigned char **, ucred **);
// Пользовательская замена nl_send().
int (*cb_send_ow)(nl_sock *, nl_msg *);
// Счетчик ссылок на экземпляр структуры.
int cb_refcnt;
// Показывает, что обработчик активен.
enum nl_cb_type cb_active;
};
nl_cb *nl_socket_get_cb(const nl_sock *sk);
void nl_socket_set_cb(nl_sock *sk, struct nl_cb *cb);
Функция nl_socket_get_cb() позволяет получить установленный обработчик.
Видим, что можно также заменять внутренние функции отправки и получения данных на пользовательские, что делает библиотеку абсолютно гибкой.
Простейший вариант отправки данных реализуют следующие функции:
int nl_send_auto(nl_sock *sk, nl_msg *msg);
int nl_send_sync(nl_sock *sk, nl_msg *msg);
Первая завершает и отправляет сообщение Netlink, а вторая после отправки дожидается подтверждения ACK и возвращает управление, то есть вызывает функцию nl_send_auto(), а затем nl_wait_for_ack().
При использовании функции nl_send_auto() порядковый номер заполняется автоматически и сопоставляется, когда получен ответ. Это поведение можно отключить, что полезно, например, когда сокет используется для получения уведомлений.
Функция nl_send_auto() отвечает только за финализацию и отправку, буквально вызывая две функции:
int nl_send_auto(struct nl_sock *sk, struct nl_msg *msg)
{
nl_complete_msg(sk, msg);
return nl_send(sk, msg);
}
Функция nl_send() вызывает функцию nl_send_iovec():
int nl_send_iovec(nl_sock *sk, nl_msg *msg, iovec *iov, unsigned iovlen);
А реальная отправка производится функцией nl_sendmsg(), которая использует системный вызов sendmsg():
int nl_sendmsg(nl_sock *sk, nl_msg *msg, msghdr *hdr);
Основополагающий принцип libnl: более сложный API строится на основе комбинации более простых, но пользователю доступны оба.
Вместо того чтобы конструировать сообщение и затем вызывать функцию отправки, возможно использовать функцию, которая создаст сообщение самостоятельно из буфера и нескольких параметров:
int nl_send_simple(nl_sock *sk, int type, int flags, void *buf, size_t size);
Принять сообщения можно, используя следующую функцию:
int nl_recvmsgs_default(nl_sock *sk);
В процессе работы она будет вызывать обработчики, зарегистрированные для сокета.
Также доступны более низкоуровневые функции. Чтобы принять данные и вызвать только переданный обработчик, можно использовать следующие из них:
int nl_recvmsgs(nl_sock *sk, nl_cb *cb);
int nl_recvmsgs_report(nl_sock *sk, nl_cb *cb);
Функция nl_recvmsgs() в случае успеха возвращает 0, а функция nl_recvmsgs_report() — количество принятых и обработанных сообщений. В остальном эти функции идентичны.
Более низкоуровневая функция nl_recv() требуется, когда разработчик самостоятельно управляет буферами:
int nl_recv(nl_sock *sk, sockaddr_nl *nla, unsigned char **buf,
ucred **creds);
Параметры данной функции:
• sk — сокет Netlink.
• nla — обязательный указатель на адрес пира, который установит ядро.
• buf — указатель на буфер приема.
• creds — указатель для приема учетных данных.
Функция вернет количество прочитанных байтов или отрицательный код ошибки.
Также можно использовать функцию nl_pickup(), чтобы прочитать сообщение и не удалять его:
int nl_pickup(nl_sock *sk,
int (*parser)(nl_cache_ops *, sockaddr_nl *, nlmsghdr *,
nl_parser_param *),
nl_object **result);
Параметры функции nl_pickup():
• sk — сокет Netlink.
• parser — обработчик для принятого ответа.
• result — указатель результата для возврата разобранного ответа.
Функция возвращает 0 в случае успеха и отрицательный код при ошибке.
Сообщение в библиотеке представлено структурой nl_msg, которая хранит их в своем буфере:
struct nl_msg
{
int nm_protocol;
int nm_flags;
struct sockaddr_nl nm_src;
struct sockaddr_nl nm_dst;
struct ucred nm_creds;
// Заголовок сообщения Netlink.
struct nlmsghdr *nm_nlh;
size_t nm_size;
int nm_refcnt;
};
Создание нового сообщения начинается с выделения памяти для структуры с помощью одной из следующих функций:
#include <netlink/msg.h>
// Выделить память для сообщения по умолчанию.
nl_msg *nlmsg_alloc();
// Выделить память заданного размера.
nl_msg *nlmsg_alloc_size(size_t max);
// Выделить память, используя максимальный размер сообщения по умолчанию
// и скопировать в это сообщение заголовок.
nl_msg *nlmsg_inherit(nlmsghdr *hdr);
// То же, что и nlmsg_inherit(), но принимает два поля заголовка.
nl_msg *nlmsg_alloc_simple(int nlmsg_type, int flags);
Функция nlmsg_alloc() и аналогичные по умолчанию выделяют буфер, равный размеру страницы. Изменить этот размер можно, используя функцию nlmsg_set_default_size():
void nlmsg_set_default_size(size_t);
После выделения памяти необходимо сформировать сообщение, добавив в него заголовок, данные, атрибуты.
Максимальный размер сообщения необходим для того, чтобы не перераспределять буфер, например, используя realloc(). Любое такое перераспределение должно выполняться только явно, так как оно приводит к недействительности всех указателей на сообщение и его элементы.
Для перераспределения буфера существует функция nlmsg_expand(). Ее мы рассматривать не будем.
Очевидно, что если для выделения не использовалась функция nlmsg_alloc_simple() или nlmsg_inherit(), то после того, как память выделена, в сообщение необходимо добавить заголовок, используя следующую функцию:
nlmsghdr *nlmsg_put(nl_msg *msg, uint32_t port, uint32_t seqnr,
int nlmsg_type, int payload, int nlmsg_flags);
Параметры функции nlmsg_put():
• msg — буфер сообщения, в которое будет добавлен заголовок.
• port — порт, назначаемый сокету. Может быть установлен в значение NL_AUTO_PORT, тогда локальный порт, назначенный сокету, будет использоваться в качестве исходного порта.
• seqnr — номер последовательности. Может быть установлен в NL_AUTO_SEQ, чтобы указать, что следующий номер должен использоваться автоматически.
• nlmsg_type — тип сообщения.
• payload — данные сообщения.
• nlmsg_flags — флаги.
Большинство функций, описанных ниже, автоматически резервируют место для данных, добавляемых в конец сообщения Netlink.
Функция nlmsg_reserve() резервирует место в конце сообщения:
void *nlmsg_reserve(nl_msg *msg, size_t len, int pad);
Параметры функции nlmsg_reserve():
• len — количество выделяемых байт.
• pad — количество байтов для выравнивания. Обычно это константа NLMSG_ALIGNTO, равная 4 байтам.
Функция возвращает указатель на начало зарезервированной области. Эта функция вызывается функцией nlmsg_append(), которая добавляет байты в конец сообщения и копирует туда данные:
int nlmsg_append(nl_msg *msg, void *data, size_t len, int pad);
Для получения длины различного типа, например полезных данных, можно использовать следующие функции:
// Размер данных без выравнивания.
int nlmsg_size(int payloadlen);
// Общий размер данных, включая заполнение для выравнивания.
int nlmsg_total_size(int payloadlen);
// Размер выравнивания.
int nlmsg_padlen(int payloadlen);
По окончании работы освободить выделенную память можно, используя функцию:
void nlmsg_free(nl_msg *msg);
Разбор пачки сообщений производится функцией:
nlmsghdr *nlmsg_next(nlmsghdr *hdr, int *remaining);
Она автоматически вычитает размер предыдущего сообщения из оставшегося количества байтов.
Чтобы понять, есть ли в потоке еще сообщения, существует функция nlmsg_ok(), которая возвращает true, если длина сообщения не более remaining байт:
int nlmsg_ok(const nlmsghdr *hdr, int remaining);
Функция nlmsg_valid_hdr() проверяет корректность размера заголовка:
int nlmsg_valid_hdr(const nlmsghdr *hdr, int hdrlen);
Если он меньше, чем выровненная длина, переданная в hdr_len, функция вернет 0, иначе 1.
Используются эти функции следующим образом:
void parse_messages(void *stream, int length)
{
nlmsghdr *hdr = stream;
while (nlmsg_ok(hdr, length))
{
// Здесь можно работать с конкретным сообщением.
hdr = nlmsg_next(hdr, &length);
}
}
Это же поведение реализовано в следующем макросе, позволяющем сократить объем типового кода:
nlmsghdr *hdr;
nlmsg_for_each(hdr, stream, length)
{
// Обработка сообщения.
}
Данные сообщения можно получить, используя следующий набор функций:
// Указатель на полезные данные.
void *nlmsg_data(const nlmsghdr *nlh);
// Указатель на конец полезных данных плюс выравнивание.
void *nlmsg_tail(const nlmsghdr *nlh);
// Получить размер данных из заголовка.
int nlmsg_datalen(const nlmsghdr *nlh);
Данные могут содержать атрибуты, значение и размер которых можно получить через следующие функции:
nlattr *nlmsg_attrdata(const nlmsghdr *hdr, int hdrlen);
int nlmsg_attrlen(const nlmsghdr *hdr, int hdrlen);
Внутри эта функция вызывает функцию nla_parse(), которая строит массив атрибутов:
struct nlattr
{
__u16 nla_len;
__u16 nla_type;
};
struct nla_policy
{
// Тип атрибута или NLA_UNSPEC.
uint16_t type;
// Минимальный требуемый размер полезной нагрузки.
uint16_t minlen;
// Максимальный требуемый размер полезной нагрузки.
uint16_t maxlen;
};
int nla_parse(nlattr *tb[], int maxtype, nlattr *head, int len,
nla_policy *policy);
Параметры функции nla_parse():
• tb — массив для заполнения размером maxtype + 1.
• maxtype — максимальный тип атрибута.
• head — заголовок потока атрибутов.
• len — длина потока атрибутов.
• policy — политика проверки атрибутов.
Если для анализа требуется больше полезных данных, функция будет считать, что это атрибуты, и соответствующим образом анализировать полезные данные.
Если размер заголовка больше нуля, функция nlmsg_parse() сначала вызовет функцию nlmsg_valid_hdr(), чтобы проверить, помещается ли заголовок протокола в сообщение, а затем — функцию nla_parse():
int nlmsg_parse(nlmsghdr *hdr, int hdrlen, nlattr **attrs, int maxtype,
nla_policy *policy);
Функции nlmsg_attrdata() и nlmsg_attrlen() также используются в функции проверки атрибутов заголовка:
int nlmsg_validate(nlmsghdr *hdr, int hdrlen, int maxtype,
nla_policy *policy);
Работа с атрибутами — одна из важных функциональных возможностей libnl-core.
Атрибуты возможно не только получать, но и добавлять, для чего используются следующие функции:
// Зарезервировать место под атрибуты.
nlattr *nla_reserve(nl_msg *msg, int attrtype, int len);
// Добавить атрибут.
int nla_put(nl_msg *msg, int attrtype, int attrlen, const void *data);
Для атрибута произвольной структуры в attrtype необходимо передать ATTR_MY_STRUCT. Но есть также функции, добавляющие атрибуты конкретных типов, например nla_put_u32() или nla_put_string().
Чтобы получить атрибуты, используются функции nla_get_string(), nla_gett_u32() и др.
Основная библиотека содержит еще несколько важных и полезных возможностей:
• API для работы с группами многоадресной рассылки. Каждый сокет может подписаться на любое количество групп многоадресной рассылки Netlink-протокола, чтобы получать сообщения, отправленные в группы. Обычно многоадресная рассылка используется для уведомления приложений о событиях.
• API для работы с функциями обратного вызова. Позволяет более точно управлять обработчиками, например, устанавливая их только для корректных сообщений.
• Подсистему кэширования. Менеджер кэша и API для работы с ним. Управляет кэшами значений и поддерживает их актуальность при изменении состояния ядра.
• Работу с абстрактными типами данных. Поддерживает абстрактные типы данных высокого уровня, которые используются большинством сетевых протоколов. Это, например, структуры адресов. Библиотека содержит API для выделения под них памяти, работы с ними как с атрибутами, их сравнения и т.п.
Мы перечислили хотя и не основные возможности, но достаточно важные. Всю библиотеку libnl в книге описать не представляется возможным, за подробностями обращайтесь к ее документации.
Библиотека содержит API, специфичный для управления следующими элементами:
• Адресами. Библиотека позволяет их добавлять, удалять, привязывать к интерфейсам.
• Конфигурацией интерфейсов. Позволяет установить MTU, флаги состояния и другие параметры.
• Маршрутизацией, правилами маршрутизации и кэшем адресов соседей.
• Дисциплинами очередей.
• Шинами, например с CAN.
• Таблицей перенаправления пакетов, или FIB
и другие.
API этой библиотеки начинается с префикса rtnl_. Функции для работы с FIB начинаются с flnl_. Библиотека активно использует кэш, предоставляемый основной библиотекой.
Полностью изучать состав API мы не будем, рассмотрим пример получения интерфейсов:
#include <netlink/route/link.h>
...
nl_cache *cache = nullptr;
// Выделить кэш соединений. Запрос будет сделан автоматически.
if (rtnl_link_alloc_cache(sock, AF_UNSPEC, &cache)) < 0)
{
// Ошибка.
}
// Получить интерфейс по имени.
rtnl_link *link = rtnl_link_get_by_name(cache, "eno2");
// Можно получить интерфейс из кэша по индексу.
// link *rtnl_link_get(cache, if_index);
if (!link)
{
// Интерфейс не существует.
}
// Обработать соединение.
// Уменьшить счетчик ссылок на объект в кэше, удалить неиспользуемый объект.
rtnl_link_put(link);
// Освободить кэш.
nl_cache_put(cache);
Запрос будет сделан при создании кэша и будет содержать объекты интерфейсов, доступ к которым можно будет получить с помощью стандартных функций для работы с кэшем.
Если для параметра семейства задано семейство адресов, отличное от AF_UNSPEC, результирующий кэш будет содержать только ссылки, поддерживающие указанное семейство адресов.
Это библиотека для работы с сокетами SOCK_DIAG. Она содержит отдельную функцию для подключения сокета:
int idiagnl_connect(nl_sock *sk);
И функцию для отправки сообщений:
int idiagnl_send_simple(nl_sock *sk, int flags, uint8_t family,
uint16_t states, uint16_t ext);
Также основной API содержит несколько функций для преобразования атрибутов и состояний в строковую форму.
Библиотека подразделяется на следующие части:
• Получение информации о памяти SOCK_DIAG. Дает возможность получать размеры очередей приема, готовых к отправке данных TCP, переданных данных и очереди для будущего использования TCP.
• Сообщения SOCK_DIAG. API для получения и создания сообщений SOCK_DIAG, например idiagnl_msg_get(), и группа функций idiagnl_msg_set_*(), а также функции для установки и получения атрибутов.
• Диагностические запросы SOCK_DIAG. Получение семейств адресов, индексов интерфейсов, состояний и прочих данных сокетов.
• Информация по алгоритму предотвращения перегрузки TCP Vegas.
Библиотека Generic Netlink. Ее функции:
• Преобразует имена семейств Generic Netlink в числовые идентификаторы. Для этого используется компонент ядра — контроллер, а библиотека предоставляет API для работы с ним.
• Позволяет регистрировать семейства и команды Netlink.
Для работы с Generic сокеты Netlink используют функции genl_connect(), genl_send_simple() и подобные. В книге мы не рассматриваем подсистему Generic Netlink, не будем подробно описывать ее API.
Функции этой библиотеки предназначены для управления сетевым фильтром Netfilter. Функция для подключения к сокету, управляющему Netfilter:
int nfnl_connect(nl_sock *sk);
Функция для отправки данных:
int nfnl_send_simple(nl_sock *sk, uint8_t subsys_id, uint8_t type,
int flags, uint8_t family, uint16_t res_id);
Эта функция просто заполняет буфер идентификатором ресурса и вызывает nl_send_simple().
Для получения разных сообщений, например типа, семейства, подсистемы, данных, существуют отдельные функции.
Библиотека содержит широкий спектр API для работы с отслеживанием соединений, а также со следующими элементами:
• Сообщениями. Для обработки сообщений используются такие функции, как nlmsg_alloc(), выделяющая буфер и nfnlmsg_put(), добавляющая данные в сообщение.
• Кэшем ожиданий.
• Очередями.
• Журналом Netfilter. Функции выполняют разбор и добавление записей.
Пример ниже активирует или деактивирует указанный ему интерфейс.
Для работы с библиотекой включим необходимые заголовочные файлы:
extern "C"
{
#include <net/if.h>
#include <netlink/netlink.h>
#include <netlink/route/link.h>
}
...
Сначала нужно создать новый сокет Libnl и подключить его к требуемой подсистеме:
int main(int argc, const char *const argv[])
{
...
const std::string if_name{argv[1]};
const std::string if_action{argv[2]};
// Создать экземпляр структуры для Libnl сокета.
std::unique_ptr<nl_sock, decltype(&nl_socket_free)>
sock{nl_socket_alloc(), &nl_socket_free};
// Кэш Netlink.
nl_cache *cache = nullptr;
int error_code = 1;
if (!sock)
{
std::cerr << "Can't allocate socket!" << std::endl;
return EXIT_FAILURE;
}
try
{
// Подключить сокет к подсистеме RTNL.
if ((error_code = nl_connect(sock.get(), NETLINK_ROUTE)) < 0)
{
// Код ошибки пригодится далее, при обработке.
throw std::system_error(error_code, std::system_category(),
"Unable to connect socket");
}
std::cout << "Socket connected." << std::endl;
Затем требуется выделить кэш. Изменение состояния интерфейса производится через установку или сброс флага IFF_UP.
Состояние меняется в отдельной копии параметров, которые применяются за один вызов rtnl_link_change():
// Выделить кэш.
if ((error_code = rtnl_link_alloc_cache(sock.get(), AF_UNSPEC,
&cache)) < 0)
... ;
// Получить сетевой интерфейс по имени.
std::unique_ptr<rtnl_link, decltype(&rtnl_link_put)>
link{rtnl_link_get_by_name(cache, if_name.c_str()),
&rtnl_link_put};
if (!link) ... ;
// Получить и вывести флаг состояния интерфейса.
std::cout
<< "\"" << if_name << "\" interface acquired\n"
<< "Current \"" << if_name << "\" status: "
<< ((rtnl_link_get_flags(link.get()) & IFF_UP) ? "up" : "down")
<< std::endl;
// Новая копия параметров интерфейса будет использована для
// изменения параметров за один вызов.
const std::unique_ptr<rtnl_link, decltype(&rtnl_link_put)>
change{rtnl_link_alloc(), &rtnl_link_put};
// Установить или сбросить флаг.
if ("a" == if_action) rtnl_link_set_flags(change.het(), IFF_UP);
else if ("d" == if_action) rtnl_link_unset_flags(change.get(),
IFF_UP);
else std::cerr << "Unknown action type!" << std::endl;
// Применить изменения.
if ((error_code = rtnl_link_change(sock.get(),
link.get(),
change.get(), 0)) < 0) ... ;
}
Для вывода ошибки на экран библиотека содержит отдельную функцию, похожую на perror(), но принимающую код ошибки явно:
catch (const std::system_error &e)
{
// Вывести ошибку libnl.
nl_perror(e.code().value(), e.what());
}
// Освободить выделенный кэш.
if (cache) nl_cache_free(cache);
return error_code;
}
Приведенное описание помогает представить объем и сложность пакета. Этот пакет охватывает практически всю подсистему Netlink.
Хотя пакет и модульный, используют его не все, так как он достаточно сложен.
Чтобы сохранить интерфейс, но взять из кода лишь необходимое, не разбираясь в полной кодовой базе пакета, в проекте OpenWRT реализовали свою библиотеку — .
Она содержит подмножество libnl библиотеки, но состоит всего из полутора десятков C-файлов и собирается с использованием CMake.
Другой вариант для работы с Netlink — , которая предоставляет минималистичный интерфейс. Она берет на себя задачи проверки и построения заголовка Netlink и пакета запроса, а также синтаксический анализ ответов.
Библиотека маленькая и простая, не содержит большого набора абстракций, а скорее представляет собой совокупность слабосвязанных функций.
Работа с сокетом Netlink начинается с его открытия:
#include <libmnl.h>
// Открыть и вернуть сокет Netlink.
mnl_socket *mnl_socket_open(int bus);
// Открыть "сокет", передав флаги сокета.
mnl_socket *mnl_socket_open2(int bus, int flags);
// Создать экземпляр mnl_socket на основе существующего дескриптора.
mnl_socket *mnl_socket_fdopen(int fd);
Идентификатор шины bus — это тип сокета Netlink, например NETLINK_GENERIC, NETLINK_ROUTE, NETLINK_SOCK_DIAG и прочие.
Для получения дескриптора сокета и его порта можно использовать следующие функции:
int mnl_socket_get_fd(const mnl_socket *nl);
unsigned int mnl_socket_get_portid(const mnl_socket *nl);
Также есть несколько функций для проверки корректности полей. Например, следующая функция проверяет, задан ли у сообщения PID, и если передан ненулевой portid, он равен указанному в заголовке:
bool mnl_nlmsg_portid_ok(const nlmsghdr *nlh, unsigned int portid);
Это нужно для того, чтобы определить, что сообщение пришло именно указанному потребителю.
Следующая функция проверяет равенство номера последовательности тому, который передан в seq, если номер был указан в заголовке:
bool mnl_nlmsg_seq_ok(const nlmsghdr *nlh, unsigned int seq);
Функции выполняют простые действия, но могут быть полезны разработчику, которому не придется реализовывать подобное самостоятельно.
К сокету можно привязать группы многоадресной рассылки и PID:
int mnl_socket_bind(mnl_socket *nl, unsigned int groups, pid_t pid);
В конце работы сокет необходимо закрыть, для чего есть отдельная функция:
int mnl_socket_close(mnl_socket *nl);
Отправка и получение данных выполняются двумя функциями:
ssize_t mnl_socket_sendto(const mnl_socket *nl, const void *req, size_t siz);
ssize_t mnl_socket_recvfrom(const mnl_socket *nl, void *buf, size_t siz);
Также есть функции для установки и получения опций:
int mnl_socket_setsockopt(const mnl_socket *nl, int type, void *buf,
socklen_t len);
int mnl_socket_getsockopt(const mnl_socket *nl, int type, void *buf,
socklen_t *len);
Опции были перечислены в главе 8.
Функции для построения сообщения в буфере:
nlmsghdr *mnl_nlmsg_put_header(void *buf);
void *mnl_nlmsg_put_extra_header(nlmsghdr *nlh, size_t size);
Первая функция резервирует данные под заголовок и обнуляет его поля, вторая функция выполняет аналогичные действия для дополнительного заголовка.
Функции для работы с данными позволяют получить как различные указатели на данные, так и размер данных:
// Указатель на начало данных.
void *mnl_nlmsg_get_payload(const nlmsghdr *nlh);
// Указатель на смещение от начала данных.
void *mnl_nlmsg_get_payload_offset(const nlmsghdr *nlh, size_t offset);
// Указатель на конец данных.
void *mnl_nlmsg_get_payload_tail(const nlmsghdr *nlh);
// Размер данных.
size_t mnl_nlmsg_get_payload_len(const nlmsghdr *nlh);
// Получить выровненный размер.
size_t mnl_nlmsg_size(size_t len);
Так же как в libnl, есть возможность итерироваться по сообщениям, состоящим из нескольких частей:
bool mnl_nlmsg_ok(const nlmsghdr *nlh, int len);
nlmsghdr *mnl_nlmsg_next(const nlmsghdr *nlh, int *len);
Функция mnl_nlmsg_next() переходит на следующее за текущим сообщение, а mnl_nlmsg_ok() проверяет, что в буфере еще достаточно места для хранения сообщения.
Здесь len — количество байтов, оставшихся в буфере. Причем это значение может быть и отрицательным в случае неправильно сформированного сообщения.
Следующая функция выводит человеко-читаемое представление сообщения в переданный файловый поток:
void mnl_nlmsg_fprintf(FILE *fd, const void *data, size_t datalen,
size_t extra_header_size);
Сначала будет выведен заголовок сообщения: длина, тип, флаги, номер последовательности и порт. Затем данные.
Для работы с атрибутами предоставляется группа функций:
• mnl_attr_get_u8() — получает значения;
• mnl_attr_put_u8_check() — получает значения с проверкой границ;
• mnl_attr_put_u8() — устанавливает значения атрибутов.
Существуют функции для работы со значениями размером 8–64 бит и для строк, а также функции, общие для всех типов, помимо функций для строго определенных типов атрибутов:
// Получение типа атрибута.
uint16_t mnl_attr_get_type(const nlattr *attr);
// Получение длины атрибута.
uint16_t mnl_attr_get_len(const nlattr *attr);
// Получение длины полезных данных атрибута.
uint16_t mnl_attr_get_payload_len(const nlattr *attr);
// Получение данных атрибута.
void *mnl_attr_get_payload(const nlattr *attr);
// Установка данных атрибута и установка с проверкой.
void mnl_attr_put(nlmsghdr *nlh, uint16_t type, size_t len, const void *data);
bool mnl_attr_put_check(nlmsghdr *nlh, size_t buflen, uint16_t type,
size_t len, const void *data);
Существует отдельный набор функций для разбора последовательности атрибутов:
// Тип обработчика.
typedef int (*mnl_attr_cb_t)(const nlattr *attr, void *data);
// Разобрать атрибуты в сообщении. Разбор начинается с offset.
int mnl_attr_parse(const nlmsghdr *nlh, unsigned int offset, mnl_attr_cb_t cb,
void *data);
// Разобрать атрибуты в данных Netlink-сообщения.
int mnl_attr_parse_payload(const void *payload, size_t payload_len,
mnl_attr_cb_t cb, void *data);
Данные функции принимают указатель на функцию-обработчик, которая вызывается для каждого атрибута.
Обработчику передается заголовок атрибута, включающий длину и тип, а также data — указатель на произвольные данные, переданные при вызове.
Если он завершается, возвращая MNL_CB_ERROR или MNL_CB_STOP, итерация по атрибутам завершается. Если MNL_CB_OK — итерация будет продолжаться, пока остаются атрибуты.
Атрибуты могут содержать вложенные атрибуты, для которых предоставляются функции:
// Добавить заголовок, идентифицирующий вложенный атрибут.
nlattr *mnl_attr_nest_start(nlmsghdr *nlh, uint16_t type);
// Делает то же самое, но проверяет, что размер буфера достаточен.
nlattr *mnl_attr_nest_start_check(nlmsghdr *nlh, size_t buflen,
uint16_t type);
// Завершить вложенный атрибут, установив поле его длины.
void mnl_attr_nest_end(nlmsghdr *nlh, nlattr *start);
// Отменить вложение атрибута, скорректировать длину сообщения.
void mnl_attr_nest_cancel(nlmsghdr *nlh, nlattr *start);
// Разобрать последовательность атрибутов, вызывая обработчик.
int mnl_attr_parse_nested(const nlattr *attr, mnl_attr_cb_t cb, void *data);
Несколько сообщений могут быть объединены в батч, как показано на рис. 12.2.
Внимание! В буфере может оставаться свободное место, которое меньше длины сообщения.
Рис. 12.2. Батч из сообщений
Разбирать такие батчи можно, используя следующие функции:
mnl_nlmsg_batch *mnl_nlmsg_batch_start(void *buf, size_t bufsiz);
bool mnl_nlmsg_batch_next(mnl_nlmsg_batch *b);
void mnl_nlmsg_batch_stop(mnl_nlmsg_batch *b);
Обычно разбор сообщений, как и атрибутов, выполняется не последовательно в цикле, а через запуск обработчиков с помощью следующих функций:
// Тип обработчика. Параметр data задается вызываемой callback-функцией.
typedef int (*mnl_cb_t)(const nlmsghdr *nlh, void *data);
int mnl_cb_run(const void *buf, size_t numbytes, unsigned int seq,
unsigned int portid, mnl_cb_t cb_data, void *data);
int mnl_cb_run2(const void *buf, size_t numbytes, unsigned int seq,
unsigned int portid, mnl_cb_t cb_data, void *data,
const mnl_cb_t *cb_ctl_array, unsigned int cb_ctl_array_len);
Параметры указанных функций:
• buf — буфер, содержащий сообщения Netlink.
• numbytes — количество байтов в буфере.
• seq — ожидаемый номер последовательности.
• portid — ожидаемый идентификатор порта.
• cb_data — указатель на обработчик.
• data — передаваемые в обработчик данные.
• cb_ctl_array — массив обработчиков управляющих сообщений.
• cb_ctl_array_len — длина массива cb_ctl_array.
Функции возвращают значения, которые вернет функция-обработчик.
Также библиотека включает несколько инструментальных функций, но большая часть ее API показана выше.
Рассмотрим пример, в котором получим все адреса указанного семейства: IPv4 или IPv6, на выбор.
Сначала включим необходимые заголовки:
extern "C"
{
#include <arpa/inet.h>
#include <libmnl/libmnl.h>
#include <linux/if.h>
#include <linux/if_link.h>
#include <linux/rtnetlink.h>
}
...
Теперь реализуем обработчик атрибутов:
static int data_attr_cb(const nlattr *attr, void *data)
{
assert(attr);
// В data передается адрес таблицы атрибутов.
const nlattr **tb = static_cast<const nlattr **>(data);
const int type = mnl_attr_get_type(attr);
// Пропустить не поддерживаемые в пользовательском пространстве атрибуты.
if (mnl_attr_type_valid(attr, IFA_MAX) < 0) return MNL_CB_OK;
// Типов атрибутов может быть много, проверку удобно делать в switch.
switch (type)
{
// Атрибут — адреса.
case IFA_ADDRESS:
// Проверить на корректность.
if (mnl_attr_validate(attr, MNL_TYPE_BINARY) < 0)
{
perror("mnl_attr_validate");
// Завершить обработку с ошибкой.
return MNL_CB_ERROR;
}
break;
}
// Добавить атрибут в таблицу.
tb[type] = attr;
// Продолжить обработку.
return MNL_CB_OK;
}
Обработчик добавляет извлеченные атрибуты в массив, адрес которого передан в параметре data.
Обработчик будет вызван из обработчика сообщения. Он, в свою очередь, разбирает заголовок и вызывает функцию разбора атрибутов, рассмотренную нами ранее:
static int data_cb(const nlmsghdr *nlh, void *data)
{
assert(attr);
assert(data);
// Адрес таблицы будет передан через параметр data обработчика атрибутов.
nlattr *tb[IFA_MAX + 1] = {};
// Получить данные: информацию об интерфейсе.
ifaddrmsg *ifa = static_cast<ifaddrmsg *>(
mnl_nlmsg_get_payload(static_cast<const nlmsghdr *>(nlh)));
std::cout
<< "index = " << ifa->ifa_index << ", family = " << +ifa->ifa_family;
// Разобрать атрибуты: передать таблицу в функцию разбора атрибутов.
mnl_attr_parse(nlh, sizeof(*ifa), data_attr_cb, tb);
std::cout << " addr = ";
if (tb[IFA_ADDRESS])
{
// У интерфейса есть первичный адрес, получим его.
void *addr = mnl_attr_get_payload(tb[IFA_ADDRESS]);
char out[INET6_ADDRSTRLEN];
// Преобразуем в печатную форму.
if (inet_ntop(ifa->ifa_family, addr, out, sizeof(out)))
std::cout << out << " " << std::endl;
}
Выведем тип интерфейса:
std::cout << " scope = ";
// Вывести тип интерфейса.
switch (ifa->ifa_scope)
{
...
case 253:
std::cout << "link ";
break;
case 254:
std::cout << "host ";
break;
...
default:
std::cout << ifa->ifa_scope << " ";
break;
}
std::cout << std::endl;
return MNL_CB_OK;
}
После того как атрибуты разобраны, обработчик выводит их на экран, запускаясь в функции main() после приема ответов на запрос к ядру.
Сначала в main() создается запрос, для чего выделяется буфер, в который помещаются основной и дополнительный заголовки:
int main(int argc, const char *argv[])
{
...
std::vector<char> buf(MNL_SOCKET_BUFFER_SIZE);
unsigned int seq = 0;
// Создать заголовок сообщения.
nlmsghdr *const nlh = mnl_nlmsg_put_header(buf.data());
// Параметры заголовка, говорящие, что нужно получить адреса.
nlh->nlmsg_type = RTM_GETADDR;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
nlh->nlmsg_seq = seq = time(nullptr);
// Добавить дополнительный заголовок, в котором будет указано
// семейство адресов.
rtgenmsg *const rt = static_cast<rtgenmsg *>(mnl_nlmsg_put_extra_header(
static_cast<nlmsghdr *>(nlh), sizeof(rtgenmsg)));
assert(argv[1]);
const std::string s_type = argv[1];
// Выбрать семейство адресов.
if ("inet" == s_type) rt->rtgen_family = AF_INET;
else if ("inet6" == s_type) rt->rtgen_family = AF_INET6;
Теперь следует создать новый сокет и отправить сообщение:
// Открыть сокет Netlink.
mnl_socket *nl = mnl_socket_open(NETLINK_ROUTE);
if (!nl)
{
perror("mnl_socket_open");
return EXIT_FAILURE;
}
try
{
// Привязать к сокету PID.
if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0)
{
throw std::system_error(errno, std::system_category(),
"mnl_socket_bind");
}
// Получить идентификатор порта.
const unsigned int portid = mnl_socket_get_portid(nl);
// Отправить сообщение.
if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) ... ;
Идентификатор порта нужен для того, чтобы затем по нему фильтровать ответы в обработчиках.
Вычитаем ответы, пока они будут приходить:
// Получить ответ.
int ret = mnl_socket_recvfrom(nl, buf.data(), buf.size());
while (ret > 0)
{
// Обработать полученные сообщения, фильтруя по portid.
ret = mnl_cb_run(buf.data(), ret, seq, portid, data_cb, nullptr);
if (ret <= MNL_CB_STOP) break;
// Продолжать читать ответы.
ret = mnl_socket_recvfrom(nl, buf.data(), buf.size());
}
if (-1 == ret) ... ;
}
catch (const std::system_error &e)
{
... ;
}
// Закрыть сокет и освободить память.
mnl_socket_close(nl);
return EXIT_SUCCESS;
}
На каждый ответ выполняется обработчик сообщений, запускающий цикл, в котором вызываются обработчики атрибутов.
Запустим пример:
➭ build/bin/b01-ch11-lib-mnl-example inet
index = 1, family = 2 addr = 127.0.0.1 scope = host
index = 2, family = 2 addr = 10.0.0.1 scope = global
index = 3, family = 2 addr = 192.168.2.13 scope = global
index = 5, family = 2 addr = 172.18.0.1 scope = global
Видно, что были получены все IPv4-адреса.
С данной библиотекой работать несколько сложнее, чем с предыдущей, но для этого не требуется изучать большой объем API.
Внимание! Эта библиотека является внутренней для пакета iproute2. Ее нежелательно использовать в проектах. В некоторых системах, например в Manjaro Linux, она установлена, в других, например в Debian, не поставляется. В образе, который используется для сборки примеров книги, пакет iproute2 изменен так, чтобы устанавливать библиотеку.
Библиотека libnetlink предоставляет интерфейс высокого уровня для получения доступа к RTNetlink. Данную библиотеку использует, например, команда ip route, обращаясь к подсистеме Netlink.
Функции библиотеки работают с дескриптором сокета RTNetlink. Дескриптор описывается следующей структурой:
struct rtnl_handle
{
// Дескриптор.
int fd;
// Адреса эндпоинтов Netlink.
struct sockaddr_nl local;
struct sockaddr_nl peer;
// Номер последовательности.
uint32_t seq;
uint32_t dump;
};
Он используется всеми функциями, и его внутренние поля не должны изменяться пользователем.
Для начала работы с библиотекой требуется открыть сокет rtnetlink и сохранить его состояние в дескриптор, используя функцию rtnl_open():
#include <asm/types.h>
#include <libnetlink.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
int rtnl_open(rtnl_handle *rth, unsigned subscriptions);
Параметры функции rtnl_open():
• rth — дескриптор, в котором хранится состояние.
• subscriptions — битовая карта групп многоадресной рассылки rtnetlink, членом которых будет сокет.
Запросы выполняют следующие функции:
// Запросить полный дамп базы для семейства адресов.
int rtnl_wilddump_request(rtnl_handle *rth, int family, int type);
// Запросить полный дамп.
int rtnl_dump_request(rtnl_handle *rth, int type, void *req, int len);
Параметры функций:
• family — семейство адресов Netlink.
• type — тип сообщения Netlink.
• req — запрос.
• len — длина буфера req.
Они возвращают число отправленных байтов в случае успеха и отрицательное значение в случае неудачи.
Функция rtnl_dump_filter() получает данные Netlink после запроса и отфильтровывает их:
int rtnl_dump_filter(rtnl_handle *rth,
int (*filter)(sockaddr_nl *, nlmsghdr *n, void *),
void *arg1,
int (*junk)(sockaddr_nl *, nlmsghdr *n, void *),
void *arg2);
Параметры функции rtnl_dump_filter():
• rth — дескриптор.
• filter — обработчик-фильтр.
• arg1 — третий параметр фильтра, который передается в функцию пользователем.
• junk — обработчик, вызываемый при несоответствии ответа запросу.
• arg2 — аргумент, передаваемый обработчику junk.
Обратный вызов фильтра получает адрес источника сообщения, само сообщение и arg1 в качестве аргументов и проверяет, требуется ли полученное сообщение. Если обработчик возвращает 0, это означает, что фильтр пройден.
Функции, описанные далее, полезны для создания пользовательских сообщений RTNetlink. Для простого дампа базы данных с фильтрацией лучше использовать вышеописанные функции более высокого уровня.
Следующая функция выполняет запрос и читает ответ:
int rtnl_talk(rtnl_handle *rtnl, nlmsghdr *n, pid_t peer,
unsigned groups, nlmsghdr *answer,
int (*junk)(sockaddr_nl *, nlmsghdr *n, void *),
void *jarg);
Параметры рассматриваемой функции:
• rtnl — дескриптор.
• n — заголовок сообщения.
• peer — идентификатор процесса-отправителя, по нему может фильтроваться ответ на запрос. Поле nl_pid в Netlink-заголовке.
• groups — поле nl_groups в Netlink-заголовке.
• answer — если параметр задан и вызов завершился без ошибки, сюда будет записан ответ.
• junk — обработчик, вызываемый при несоответствии ответа запросу.
• jarg — параметр обработчика, задаваемый пользователем.
Обработчики junk вызываются, если PID ответа или номера последовательности не соответствуют идентификаторам в заголовке и peer. Они реализованы и могут быть использованы не везде.
Функция вернет 0 в случае успеха и –1 при ошибке.
Для отправки запроса существует несколько функций, и прежде всего — функция rtnl_send():
int rtnl_send(rtnl_handle *rth, char *buf, int len);
Параметры функции rtnl_send():
• rth — дескриптор, возвращенный функцией rtnl_open().
• buf — буфер для отправки.
• len — длина буфера.
Функция отправляет буфер длиной len и возвращает количество отправленных данных или –1 в случае ошибки.
В новых версиях библиотеки есть похожая функция:
int rtnl_send_check(rtnl_handle *rth, const void *buf, int len);
Кроме отправки сообщения, она читает ответ и разбирает сообщения. Если отправка сообщения или прием вернули –1 либо появляется сообщение, имеющее тип NLMSG_ERROR, функция вернет –1, в противном случае 0.
Функции, добавляющие атрибуты в сообщение:
// Добавить атрибут data типа type и со значением data в сообщение Netlink,
// которое является частью буфера длины maxlen.
int addattr32(nlmsghdr *n, int maxlen, int type, __u32 data);
// Добавить атрибут переменной длины типа type и со значением data
// и длиной alen в сообщение Netlink n, которое является частью буфера
// длины maxlen. Данные копируются.
int addattr_l(nlmsghdr *n, int maxlen, int type, void *data, int alen);
// Инициализировать атрибут rta типа type со значением data.
int rta_addattr32(rtattr *rta, int maxlen, int type, __u32 data);
// Инициализировать атрибут rta типа type со значением data длиной alen.
int rta_addattr_l(rtattr *rta, int maxlen, int type, void *data, int alen);
Функция для разбора атрибутов:
void parse_rtattr(rtattr *tb[], int max, rtattr *rta, int len);
Параметры функции parse_rtattr():
• tb — массив, в который будут помещены атрибуты.
• max — максимальное значение типа атрибута.
• rta — указатель на атрибуты.
• len — длина атрибутов.
Пример работы с данной библиотекой есть в репозитории книги, но здесь мы его приводить не будем по той причине, что данная библиотека была разработана как внутренняя для реализации утилит пакета iproute2.
В целом библиотека по составу и функционированию API похожа на предыдущие, особенно на lib-mnl, и сейчас пакет iproute2 в некоторых местах уже использует lib-mnl.
предназначен для работы с Netlink из Python. Pyroute2 изначально работает с Netlink API в Linux и сокетами домена PF_ROUTE в BSD-системах. Подмножество API этого пакета, такое как управление маршрутизацией, работает на ОС Windows.
Ядро библиотеки переходит с многопоточной архитектуры на использование asyncio.
Изменения не скажутся на основном пользовательском API, структурах данных и архитектуре высокого уровня, однако полностью изменятся получение, отправка, сборка и разбор сообщений.
Пакет можно использовать для работы с существующими протоколами, такими как RTNL, а также для любых других Netlink-протоколов: он позволяет создавать парсеры для PDU, имеющих структуру (type, length, payload).
В пакете реализована ограниченная поддержка протоколов PF_ROUTE и 9P2000. Первый используется для мониторинга сетевых настроек в BSD-системах, а второй — реализует сетевую файловую систему и RPC в ОС Plan9.
Пакет реализован на чистом Python и не зависит от сторонних библиотек.
Основные составляющие пакета:
• кодировщик и декодировщик сообщений Netlink;
• модули, специфичные для конкретного Netlink-протокола.
На низком уровне он представляет собой расширенный API поверх объектов сокетов, предоставляющий функциональность, схожую с функциональностью библиотеки libnl-core:
• помогает открывать сокеты Netlink и привязывать к ним адреса;
• поддерживает общие протоколы Netlink и группы многоадресной рассылки;
• позволяет создавать, кодировать и декодировать сообщения Netlink и PF_ROUTE.
RTNL API представлен в основном классом IPRoute для Netlink в текущем сетевом пространстве имен, а также классом NetNS для работы в другом пространстве. Архитектура этих классов для платформы Linux показана на рис. 12.3.
Рис. 12.3. Архитектура PyRoute2 для Linux
Класс IPRoute для разных платформ реализован по-разному.
В Linux, например, он наследует классу IPRSocket, реализующему специфические особенности протокола RTNL и некоторые дополнительные операции вне протокола, выполнимые только через ioctl-вызовы. Также он наследует mix-in классу RTNL_API, реализующему основные низкоуровневые методы, для любого RTNL-совместимого сокета в Linux.
Например, это фильтрация сообщений, poll(), реальный метод дампа Netlink, метод link() для работы с сетевыми интерфейсами, методы для работы с маршрутами, контролем трафика, записями соседей и т.п.
В других системах, например в BSD, методы реализованы прямо в классе IPRoute, то есть класс IPRoute в данном случае является интерфейсным. Рассмотрим несколько его методов.
Метод dump() возвращает параметры сетевых интерфейсов, адресов и маршрутов, полученные с использованием Netlink:
def dump(self, groups=None)
Основные методы для работы с сетевыми объектами предоставляют единообразный API:
# Операции с ARP-таблицей.
def neigh(self, command, **spec)
# Операции с сетевыми интерфейсами.
def link(self, command, **spec)
# Операции с IP-адресами.
def addr(self, command, **spec)
# Операции с маршрутами
def route(self, command, **spec)
Каждый метод из рассмотренных принимает в качестве первого и единственного позиционного аргумента команду dump, get, add, del, set и т.п., а также специфические именованные аргументы, например index, link, state. Все они описаны в документации к методам.
Внимание! Из-за того что результат обработки команд dump может быть слишком велик для хранения в памяти, команды dump в методах возвращают генераторы, исполняемые только во время итерации.
По сходному принципу реализованы методы:
• brport() — работа с портами мостов.
• vlan_filter() — управление фильтрами VLAN для мостов.
• fdb() — работа с базой пересылки мостов.
• tc() — управление классами и процедурами контроля трафика.
Такой подход позволяет строить на основе IPRoute более высокоуровневые API, где методы neigh(), link(), addr() и route() привязаны к типу сетевого объекта, а конкретная операция выбирается среди dump, get, add, del, set уже независимым от метода уровнем логики.
Таковы get_links(), get_neighbours() и подобные, чаще всего реализованные как более высокоуровневые обертки вокруг соответствующего низкоуровневого метода типа link(), neigh() и т.д.
Помимо основных методов, класс предоставляет множество вспомогательных диагностических методов, которые могут быть полезны в простых скриптах.
Рассмотрим пример использования базового API. Следующий пример выведет атрибуты интерфейса, переведет в отключенное состояние, назначит ему адрес и восстановит состояние обратно.
Реализуем обработчик, печатающий атрибуты, затем получим список устройств и индекс выбранного сетевого интерфейса:
import sys
from pyroute2 import IPRoute
def print_if_attrs(if_attrs):
# Атрибуты — список кортежей "имя"/"значение".
for attr_name, attr_val in if_attrs:
if 'IFLA_OPERSTATE' == attr_name:
print(f'Oper state: {attr_val}')
elif 'IFLA_ADDRESS' == attr_name:
print(f'Address: {attr_val}')
# Объект API.
with IPRoute() as ipr:
# Получить индексы сетевых интерфейсов по имени.
devs = ipr.link_lookup(ifname=sys.argv[1])
if not devs:
print(f'Device "{sys.argv[1]}" was not found!')
sys.exit(1)
# Индекс сетевого интерфейса.
dev_index = devs[0]
Сначала мы создали экземпляр класса IPRoute и получили индекс интерфейса по его имени. Метод link_lookup() и подобные реализованы через методы класса IPRoute низкого уровня.
Выполним печать атрибутов и манипуляции с адресами и состояниями интерфейса:
print(f'Devices count = {len(devs)}, Device index = {dev_index}')
print_if_attrs(ipr.link('get', index=dev_index)[0]['attrs'])
link_state = 'down'
try:
# Получить состояние интерфейса для восстановления.
link_state = ipr.link('get', index=dev_index)[0]['state']
# Отключить интерфейс.
ipr.link('set', index=dev_index, state='down')
# Изменить MAC интерфейса, затем переименовать его.
ipr.link('set', index=dev_index, address='00:11:22:33:44:55',
ifname='eno2')
# Добавить первичный IP-адрес.
ipr.addr('add', index=dev_index, address='10.0.0.1', mask=24,
broadcast='10.0.0.255')
finally:
# Восстановить состояние интерфейса.
ipr.link('set', index=dev_index, state=link_state)
print(f'Interface {sys.argv[1]} state restored...')
print_if_attrs(ipr.link('get', index=dev_index)[0]['attrs'])
Запустим пример и увидим изменения, которые вносит код:
➭ sudo ./run src/book01/ch12/python/pyroute2-low-level-example.py eno2
Devices count = 1, Device index = 2
Oper state: DOWN
Address: 00:11:22:33:44:55
Interface eno2 state restored...
Oper state: DOWN
Address: 00:11:22:33:44:55
Низкоуровневый API достаточно прост, отображается на сокеты Netlink практически напрямую и не имеет особых функций. Интерес представляют более высокоуровневые интерфейсы, например интерфейс NDB.
Кроме методов, объекты NDB представляют собой словари, которые содержат атрибуты сети, интерфейсов, маршрутов, адресов и т.д. Большинство атрибутов одноуровневые, но некоторые, например маршруты, предоставляют вложенные структуры, то есть являются, по сути, базой данных сетевых параметров, которая выполняет следующие функции:
• проверяет целостность данных;
• реализует транзакции с фиксацией и откатом изменений;
• синхронизирует состояния базы и системы;
• позволяет работать с несколькими источниками, включая сети и удаленные системы.
По сути, NDB — это транзакционная база данных, содержащая записи, которые представляют объекты сетевого стека. Любое изменение в базе данных не отражается сразу в ОС, а ждет, когда будет вызван метод commit().
Неудачная операция во время вызова commit() откатывает все сделанные изменения. Причем откатываются не только явно примененные на уровне ОС изменения, но и те, которые NDB считает зависимыми. При удалении виртуального интерфейса, например VLAN, или адреса с интерфейса ОС удалит также все зависимые маршруты через этот интерфейс. При откате такой транзакции NDB автоматически восстановит все удаленные маршруты.
Общая архитектура NDB показана на рис. 12.4.
Описания объектов сетевого стека и их отношений реализованы в NDB в виде SQL-схемы, а в качестве хранилища NDB использует реляционную БД.
Рис. 12.4. Схема NDB
Использование SQL, в частности первичных ключей для идентификации объектов и внешних ключей для описания их связи, позволяет соблюсти целостность данных даже при том, что ядро оповещает не обо всех изменениях сетевых настроек.
В качестве реляционной СУБД по умолчанию используется SQLite3, но может быть использована PostgreSQL.
Транзакции в NDB могут быть запущены на следующих уровнях:
• уровне объекта — интерфейса, адреса или маршрута. Метод commit() будет вызван у конкретного объекта, и он применит все внесенные в него изменения;
• уровне экземпляра класса NDB. Вызов метода begin() экземпляра NDB создает транзакцию, в которую добавляются новые действия с помощью метода push() экземпляра транзакции.
Протокол работы транзакций, созданных на уровне NDB, таков:
1. У всех объектов, добавленных через push(), вызывается метод commit().
2. Он завершается успешно либо порождает исключение.
3. В последнем случае откатываются все изменения, примененные до момента исключения, включая зависимые.
Этот алгоритм изображен на рис. 12.4.
Во время интерактивной сессии попробуем деактивировать интерфейс, через который доступен интересующий нас адрес, и посмотрим, как NDB откатит изменения, когда адрес станет недоступен.
Рис. 12.5. Создание транзакции NDB
Интерфейс test мы создали предварительно.
Внимание! Для изменения параметров интерфейса необходимо запустить интерпретатор Python с правами суперпользователя.
Для начала создадим экземпляр NDB и проверим, что нужный нам интерфейс и адрес на месте:
>>> from pyroute2 import NDB
>>> ndb = NDB()
>>> ndb.interfaces.summary()
('localhost', 0, 1, 'lo', '00:00:00:00:00:00', 65609, None)
('localhost', 0, 61, 'eth0', 'b0:4f:13:21:75:84', 4099, None)
('localhost', 0, 67, 'test', '56:45:60:94:3a:f2', 65731, 'dummy')
>>> ndb.addresses.summary().filter(ifname='test')
('localhost', 0, 'test', '192.168.14.24', 24)
Как видно из вызова ndb.routes.summary(), интерфейс с именем test имеет адрес 192.168.14.1.
Создадим через него маршрут:
>>> ndb.routes.create(dst='192.168.15.0/24', gateway='192.168.14.1').commit()
...
>>> ndb.routes.summary().filter(ifname='test')
('localhost', 0, 255, 'test', '192.168.14.24', 32, None)
('localhost', 0, 254, 'test', '192.168.14.0', 24, None)
('localhost', 0, 255, 'test', '192.168.14.255', 32, None)
('localhost', 0, 254, 'test', '192.168.15.0', 24, '192.168.14.1')
Подготовим составную транзакцию и включим туда удаление виртуального интерфейса и проверку доступности интересующего нас шлюза 192.168.14.1:
>>> from pyroute2.ndb.transaction import PingAddress
>>> transaction = ndb.begin()
>>> transaction.push(ndb.interfaces['test'].remove())
>>> transaction.push(PingAddress('192.168.14.1'))
>>> transaction.commit()
Перед тем как мы добавили адрес, в транзакцию была добавлена операция удаления интерфейса. Поэтому вызов метода commit() сделает недоступной сеть шлюза 192.168.14.0/24, и транзакция должна откатиться:
>>> transaction.commit()
Traceback (most recent call last):
...
pyroute2.ndb.transaction.CheckProcessException: CheckProcess failed
Видим, что экземпляр класса PingAddress сгенерировал исключение, так как интерфейс стал недоступен и транзакция не была завершена.
С помощью команды ip mon из пакета iproute2 посмотрим, что в этот момент происходило на уровне операционной системы.
[LINK]67: test: <BROADCAST,NOARP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 56:45:60:94:3a:f2 brd ff:ff:ff:ff:ff:ff
[ADDR]Deleted 67: test inet 192.168.14.24/24 scope global test
valid_lft forever preferred_lft forever
[ROUTE]Deleted local 192.168.14.24 dev test table local proto kernel scope host src 192.168.14.24
[NETCONF]Deleted inet test
[LINK]Deleted 67: test: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 56:45:60:94:3a:f2 brd ff:ff:ff:ff:ff:ff
Удаление интерфейса привело к удалению зависимых адресов и маршрутов.
Внимание! ОС уведомляет об удалении не всех зависимых объектов, а лишь некоторых. Так, хотя маршрут до сети 192.168.15.0/24 был удален, ОС не сообщит об этом. Однако NDB, используя реляционную БД, самостоятельно вычислит такие зависимости.
Затем транзакция откатилась:
[LINK]67: test: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
link/ether 56:45:60:94:3a:f2 brd ff:ff:ff:ff:ff:ff
[ADDR]67: test inet 192.168.14.24/24 scope global test
valid_lft forever preferred_lft forever
[ROUTE]local 192.168.14.24 dev test table local proto kernel scope host src 192.168.14.24
[ROUTE]192.168.14.0/24 dev test proto kernel scope link src 192.168.14.24
[ROUTE]broadcast 192.168.14.255 dev test table local proto kernel scope link src 192.168.14.24
[ROUTE]192.168.15.0/24 via 192.168.14.1 dev test proto static
Восстановление маршрута до сети 192.168.15.0/24 попало в журнал последней строкой.
Проверим, что все интересующие нас записи восстановились и в NDB:
>>> ndb.interfaces.summary().filter(ifname='test')
('localhost', 0, 67, 'test', '56:45:60:94:3a:f2', 65731, 'dummy')
>>> ndb.addresses.summary().filter(ifname='test')
('localhost', 0, 'test', '192.168.14.24', 24)
>>> ndb.routes.summary().filter(ifname='test')
('localhost', 0, 255, 'test', '192.168.14.24', 32, None)
('localhost', 0, 254, 'test', '192.168.14.0', 24, None)
('localhost', 0, 255, 'test', '192.168.14.255', 32, None)
('localhost', 0, 254, 'test', '192.168.15.0', 24, '192.168.14.1')
Объекты NDB, будь то интерфейсы, маршруты и т.п., являются контекстными менеджерами, которые вызовут commit() при выходе из блока with:
import sys
from pyroute2 import NDB
# Создать экземпляр NDB. Для отладки необходимо установить параметр log='debug'.
with NDB() as ndb:
iface_ip_addr = '10.0.0.2/24'
try:
# Для запуска транзакции используется контекстный менеджер.
# Сначала будет создан интерфейс с заданным именем,
# в активированном состоянии.
with ndb.interfaces.create(
ifname=sys.argv[1], kind='dummy', state='up'
) as interface:
# Добавить IP-адреса.
interface.add_ip(iface_ip_addr)
interface.add_ip('10.0.0.3/24')
interface.set('mtu', 8192)
# При выходе будет автоматически вызван commit().
# Если адрес есть среди адресов интерфейса, удалить его.
if iface_ip_addr in ndb.addresses:
with ndb.addresses[iface_ip_addr] as addr:
addr.remove()
# Перечислить все адреса всех интерфейсов.
for record in ndb.addresses.summary():
print(record)
finally:
ndb.interfaces[sys.argv[1]].remove().commit()
Запустим пример:
➭ sudo ./run src/book01/ch12/python/pyroute2-ndb-example.py test
('localhost', 0, 'lo', '127.0.0.1', 8)
('localhost', 0, 'eno2', '10.0.0.1', 24)
('localhost', 0, 'wlo1', '192.168.2.13', 24)
('localhost', 0, 'wlo1', 'fe80::4f3e:b28d:ace9:2e05', 64)
('localhost', 0, 'test', '10.0.0.3', 24)
('localhost', 0, 'test', 'fe80::54de:60ff:fe20:ccd7', 64)
Адреса созданного интерфейса видны среди прочих.
В пакете реализованы модули, которые поддерживают некоторые семейства и протоколы:
• arp — поддержка работы с ARP.
• dhcp — поддержка сообщений DHCP.
• ethtool — API для низкоуровневой настройки сетевого интерфейса.
• netlink — в модуле реализованы некоторые базовые структуры различных Netlink-протоколов. Там же доступна простейшая фильтрация пакетов через цель iptables QUEUE, управление и мониторинг оборудования с поддержкой devlink и получение сообщений uevent, используемых в udev. API отправки сообщений не реализован.
• netns — работа с сетевыми пространствами имен.
• nftables — API сетевого фильтра.
• remote — модуль для удаленной работы с сокетами Netlink.
• ipset и wiset — работа с наборами адресов.
• ipvs — виртуальный IP-сервер, реализованный в ядре Linux.
• iwutil — экспериментальный модуль для поддержки nl80211.
• plan9 — реализация протокола 9P2000, который можно использовать как для создания виртуальных файловых систем, так и для RPC. В пакете он используется как основа межкомпонентного RPC.
— это маленький низкоуровневый пакет для работы с сокетами Netlink. Если сравнивать его с Pyroute2, они соотносятся примерно так же, как libmnl и libnl.
В отличие от Pyroute2, обеспечивающего синхронную работу с сокетами Netlink, пакет Python-netlink асинхронный. Сейчас он работает поверх Trio, хотя автор предполагает перевести его на AnyIO.
Пакет Trio и асинхронность в общем мы рассмотрим в книге 2.
Пакет обеспечивает поддержку контроллера Netlink, а также ограниченную поддержку nl80211 и RTNetlink. Поддержка других семейств сетевых ссылок может быть добавлена относительно легко.
Пакет содержит очень небольшое количество модулей:
• netlink — в корне пакета содержатся типы сокетов, типы запросов и т.п. Здесь реализованы:
• NetlinkMessage — базовый класс сообщения Netlink. Каждое сообщение имеет три атрибута: type, flags, payload.
• NetlinkSocket — абстракция сокета Netlink.
• connect(family) — функция, которая создает новый сокет Netlink, привязывает адрес, устанавливает опции NLM_F_REQUEST, NLM_F_ACK и возвращает экземпляр класса NetlinkSocket.
• netlink.streams — классы потоков данных:
• Класс StreamOut — выходной поток, например, для атрибутов. Данные, записанные в этот поток, можно затем получить в бинарной форме.
• Класс StreamIn — входной поток, из которого данные могут быть декодированы в объекты с атрибутами.
• netlink.attributes — общие классы и функции для работы с атрибутами:
• Класс AttributeType — общий класс для типов атрибутов, содержащий методы кодирования и декодирования разных типов.
• Класс Policy — политика атрибутов, возвращаемая запросом Netlink: макcимальное значение типа атрибута, ограничение длины и т.п.
• Функции для создания типов атрибутов: u8(), u16(), u32(), u64(), s8() — s64(), binary(), string(), array(), dict(), flag(). Они просто оборачивают типы в класс AttributeType.
• Функции для представления специальных типов nested() для вложенных атрибутов и padding() для заполнения.
• Функции encode(), decode(), encode_raw() и decode_raw() — служат для кодирования и декодирования потоков данных StreamOut и StreamIn.
Класс NetlinkSocket имеет следующие методы:
# Добавить сокет в группу с идентификатором id.
def add_membership(self, id)
# Принять данные, создать экземпляр сообщения NetlinkMessage и отправить его
# в канал Trio.
async def start(self)
# Отправить данные в сокет.
async def send(self, data)
# Принять данные, отправленные в методе start(), из канала Trio.
async def receive(self)
# Создать запрос Netlink, отправить его и получить ответ.
async def request(self, type, payload=b'', flags=0)
# Создать запрос NLMSG_NOOP.
async def noop(self)
Другие модули предоставляют API для работы с различными типами сокетов Netlink:
• netlink.generic — основной протокол сокетов Netlink:
• Класс Family — семейства Netlink, возвращаемые методом GenericNetlinkController.get_families().
• Классы Policy и CommandPolicy — объекты класса Policy возвращаются методами GenericNetlinkController.
• Класс GenericNetlinkMessage — класс сообщения, содержащий атрибуты family, flags, type, version, attributes.
• Класс GenericNetlinkReceiver — используется сокетом для получения сообщений.
• Класс GenericNetlinkSocket — класс сокета.
• Класс GenericNetlinkController(GenericNetlinkSocket) — контроллер, дающий возможность получать семейства и политики.
• netlink.nl80211 — содержит класс NL80211.
• netlink.route — сокеты RTNetlink. Содержит класс RouteController(generic.GenericNetlinkSocket), который реализует функции RTNetlink. Класс предоставляет методы add_address(), add_neighbour() и remove_neighbour().
Каждый из модулей содержит множество атрибутов и соответствующую классу поддерживаемых сокетов функцию connect().
Это, по сути, весь пакет. Вероятно, автор будет его расширять, но важно, что он предоставляет достаточно низкоуровневый и простой асинхронный интерфейс.
В принципе, описанных пакетов должно быть достаточно, чтобы работать с Netlink. Однако эта подсистема обширная и постоянно развивается, что требует обязательного чтения документации для работы с ней.
Для упрощения работы c Netlink существует несколько библиотек, предоставляющих различную степень абстракции: libnl, libmnl, libnetlink.
Пакет libnl предлагает большой набор библиотек, предоставляющий разработчикам возможность взаимодействовать с сетевыми подсистемами Linux через интерфейс API Netlink:
• libnl-core — основная библиотека. Содержит функции для построения и анализа сообщений Netlink, работы с Netlink-сокетами, отправки и получения данных.
• libnl-route — предоставляет API для работы с RTNetlink.
• libnl-genl — API протокола Generic Netlink.
• libnl-nf — настройка сетевых фильтров и интерфейсов мониторинга, отслеживание соединений, кэши, очереди.
• libnl-idiag — поддержка для работы с сокетами типа SOCK_DIAG. Функции для мониторинга и управления состоянием сокетов.
Библиотека libmnl является более маленькой и простой и предоставляет базовые функции для проверки и построения заголовков Netlink, а также для анализа ответов. Библиотека дает возможность упростить работу, почти не внося дополнительных расходов.
Библиотека libnetlink, которая используется в пакете iproute2, предоставляет интерфейс высокого уровня для доступа к RTNetlink.
Для работы с Netlink из Python используются пакеты pyroute2 и python-netlink.
Пакет Pyroute2 предоставляет высокоуровневый интерфейс. Он позволяет взаимодействовать с сетевыми интерфейсами и маршрутизацией без необходимости погружения в низкоуровневые детали и предлагает механизм транзакций.
Python-netlink — это относительно маленькая библиотека, обеспечивающая низкоуровневое взаимодействие с сокетами Netlink. Она является асинхронной и предоставляет более простой интерфейс.
1. Какие функции и возможности предоставляет библиотека libnl для работы с сетевыми интерфейсами?
2. Какие библиотеки входят в состав libnl?
3. Какой компонент библиотеки libnl предоставляет API для работы с функциями обратного вызова?
4. Как создать сокет Netlink в libnl?
5. Как сконструировать и отправить сообщение в libnl?
6. Как в libnl разобрать ответ ядра?
7. В чем отличие библиотеки libmnl от libnl?
8. Как создать Netlink-сокет с помощью libmnl?
9. Всегда ли батч в libmnl занимает полный буфер?
10. Каков процесс разбора атрибутов в libmnl?
11. Что такое и где используется libnetlink?
12. Рекомендуется ли разработчикам использовать libnetlink в своих продуктах?
13. В каких ОС работает Pyroute2?
14. Из чего состоит Pyroute2?
15. Как называется основной класс в Pyroute2? Что этот класс позволяет делать?
16. Как создать объект для работы с сетевыми интерфейсами в Pyroute2?
17. Как добавить новый сетевой интерфейс с использованием Pyroute2?
18. Где можно найти документацию и примеры использования Pyroute2?
19. Что такое NDB и зачем он нужен?
20. Как работает NDB?
21. Какие модули предлагает пакет Pyroute2?
22. Что такое пакет Python-netlink? Из чего он состоит?
23. В примере библиотеки libnl мы активировали или деактивировали интерфейс. Добавьте установку нового значения MTU с помощью libnl-route.
24. Считайте значение MTU, используя библиотеку Python-netlink и Pyroute2.