Книга: Сетевое программирование. От основ до приложений
Назад: Глава 11. Сокеты Netlink
Дальше: Глава 13. Специальные файловые системы

Глава 12. Библиотеки Netlink

Надежное и прозрачное ПО обычно не отвечает интересам проектировщика.

Никлаус Вирт, «A Digital Contrarian Retires», 1999

Введение

Работать с сокетами Netlink напрямую достаточно сложно, а ошибку при этом допустить очень легко, поэтому важную роль для удобства работы с Netlink играют библиотеки. Среди них есть как сложные пакеты, так и легкие обертки, предоставляющие меньшую степень абстракции. Реализованы они на разных языках программирования.

В этой главе мы подробно рассмотрим функциональные возможности и API основных библиотек для работы c Netlink, а также их применение в разработке сетевых приложений.

Начнем с Libnl — достаточно большого пакета, далее рассмотрим более простые альтернативы.. Наконец, в завершение главы перейдем к пакетам Python.

Пакет Libnl

— набор библиотек, покрывающий большую часть 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() и др.

Прочие возможности libnl-core

Основная библиотека содержит еще несколько важных и полезных возможностей:

API для работы с группами многоадресной рассылки. Каждый сокет может подписаться на любое количество групп многоадресной рассылки Netlink-протокола, чтобы получать сообщения, отправленные в группы. Обычно многоадресная рассылка используется для уведомления приложений о событиях.

• API для работы с функциями обратного вызова. Позволяет более точно управлять обработчиками, например, устанавливая их только для корректных сообщений.

• Подсистему кэширования. Менеджер кэша и API для работы с ним. Управляет кэшами значений и поддерживает их актуальность при изменении состояния ядра.

Работу с абстрактными типами данных. Поддерживает абстрактные типы данных высокого уровня, которые используются большинством сетевых протоколов. Это, например, структуры адресов. Библиотека содержит API для выделения под них памяти, работы с ними как с атрибутами, их сравнения и т.п.

Мы перечислили хотя и не основные возможности, но достаточно важные. Всю библиотеку libnl в книге описать не представляется возможным, за подробностями обращайтесь к ее документации.

libnl-route

Библиотека содержит 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, результирующий кэш будет содержать только ссылки, поддерживающие указанное семейство адресов.

libnl-idiag

Это библиотека для работы с сокетами 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.

libnl-genl

Библиотека Generic Netlink. Ее функции:

• Преобразует имена семейств Generic Netlink в числовые идентификаторы. Для этого используется компонент ядра — контроллер, а библиотека предоставляет API для работы с ним.

• Позволяет регистрировать семейства и команды Netlink.

Для работы с Generic сокеты Netlink используют функции genl_connect(), genl_send_simple() и подобные. В книге мы не рассматриваем подсистему Generic Netlink, не будем подробно описывать ее API.

libnl-nf

Функции этой библиотеки предназначены для управления сетевым фильтром 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. Функции выполняют разбор и добавление записей.

Пример работы с libnl

Пример ниже активирует или деактивирует указанный ему интерфейс.

Для работы с библиотекой включим необходимые заголовочные файлы:

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.

Библиотека Libmnl

Другой вариант для работы с 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.

Библиотека Libnetlink   

Внимание! Эта библиотека является внутренней для пакета 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.

Пакет Pyroute2

предназначен для работы с 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.

Пакет Python-netlink

— это маленький низкоуровневый пакет для работы с сокетами 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.


Назад: Глава 11. Сокеты Netlink
Дальше: Глава 13. Специальные файловые системы