В статье рассмотрим как реализовать команды утилиты traffic control с помощью библиотеки libnl на с/c++. У libnl есть неплохая документация так же есть небольшое количество тестов. Которые помогают разобраться как работать с библиотекой.
Traffic control — утилита которая нужна для управления входящем и исходящим трафиком. С помощью tc возможно реализовывать политики QoS (Quality of Service), а именно выполнять SHAPING (выравнивание), SCHEDULING (приоритизация), POLICING (ограничение) и DROPPING (отбрасывание) трафика.
Рассмотрим следующие команды:
tc qdisc add dev lo root handle 1: htb default 20 tc class add dev lo parent 1: classid 1:1 htb rate 10240kbit tc class add dev lo parent 1:1 classid 1:10 htb rate 100kbit ceil 10240kbit prio 1 tc class add dev lo parent 1:1 classid 1:20 htb rate 100kbit ceil 10240kbit prio 2 tc filter add dev lo parent 1: protocol ip prio 1 u32 match ip dst 127.0.0.1/32 flowid 1:10
В результате выполнения этих команд создаётся корневая дисциплина htb (шейпер) для выравнивания скорости передачи данных через интерфейс loopback (default 20 означает, что весь не классифицированный трафик будет отнесён к классу с меткой 20). Два класса с разными приоритетами, которые наследуются от корневого шейпера. Фильтр который относит весь трафик у которого ip адресс назначения соответствует 127.0.0.1/32 к первому классу. Пример визуализации команд ниже, создано с помощью tcviz.

Рассмотрим как реализовать эти команды с помощью библиотеки libnl, для этого необходимо:
Создать корневую дисциплину(htb)
Создать родительский класс(htb), который наследуется от корневой дисциплины и устанавливает порог выравнивания скорости.
Создать два дочерних класса, один в который будут попадать пакеты классифицированные с помощью фильтра и второй для не классифицированных пакетов.
Создать фильтр для классификации пакетов.
Для того чтобы добавить дисциплину реализуем функцию add_qdisc_htb
void add_qdisc_htb(struct rtnl_link *link, uint32_t parent_handle, uint32_t handle, struct nl_sock* sock, uint32_t default_class) { int err; struct rtnl_qdisc *qdisc; qdisc = rtnl_qdisc_alloc(); // выделяем ресурсы под дисциплину rtnl_tc_set_link(TC_CAST(qdisc), link); rtnl_tc_set_parent(TC_CAST(qdisc), parent_handle); // устанавливаем родительский класс rtnl_tc_set_handle(TC_CAST(qdisc), handle); // устанавливаем текущий класс err = rtnl_tc_set_kind(TC_CAST(qdisc), "htb"); // устанавливаем тип дисциплины throw_err(err); err = rtnl_htb_set_defcls(qdisc, default_class); // устанавливаем класс по умолчанию throw_err(err); err = rtnl_qdisc_add(sock, qdisc, NLM_F_CREATE); // добавляем дисциплину throw_err(err); rtnl_qdisc_put(qdisc); // освобождаем ресурсы дисциплины }
гдеrtnl_link *link это структура для привязки к интерфейсу, uint32_t parent_handle - родительский класс, uint32_t handle - текущий класс (все метки класса в командах утилиты tc задаются в шестнадцатеричной системе), struct nl_sock* sock - сокет для работы подсистемой маршрутизации ядра linux, uint32_t default_class - класс по умолчанию, куда будут отправлять не классифицированные пакеты.
Для того чтобы добавить классы реализуем функцию add_htb_class
void add_htb_class(struct rtnl_link *link, uint32_t parent_handle, uint32_t handle, struct nl_sock* sock, uint32_t rate, uint32_t ceil = 0, uint32_t prio = 0, uint32_t quantum = 0) { int err; struct rtnl_class *cl; cl = rtnl_class_alloc(); rtnl_tc_set_link(TC_CAST(cl), link); rtnl_tc_set_parent(TC_CAST(cl), parent_handle); rtnl_tc_set_handle(TC_CAST(cl), handle); err = rtnl_tc_set_kind(TC_CAST(cl), "htb"); throw_err(err); err = rtnl_htb_set_rate(cl, rate); throw_err(err); if (ceil) { err = rtnl_htb_set_ceil(cl, ceil); throw_err(err); } if (prio) { err = rtnl_htb_set_prio(cl, prio); throw_err(err); } if (quantum) { err = rtnl_htb_set_quantum(cl, quantum); throw_err(err); } err = rtnl_class_add(sock, cl, NLM_F_CREATE); throw_err(err); rtnl_class_put(cl); }
аргументы функции аналогичны за исключением, что возможно установить минимальную гарантированную скорость для класса, максимальную полосу пропускания, приоритет, quantum который используется в DWRR
Для того чтобы добавить фильтр реализуем функцию add_u32_filter_32key
void add_u32_filter_32key(struct rtnl_link *link, uint32_t handle, uint32_t flowid, struct nl_sock* sock, int prio, u32key key) { struct rtnl_cls *filter; int err; filter = rtnl_cls_alloc(); rtnl_tc_set_link(TC_CAST(filter), link); rtnl_tc_set_parent(TC_CAST(filter), handle); err = rtnl_tc_set_kind(TC_CAST(filter), "u32"); throw_err(err); rtnl_cls_set_prio(filter, prio); rtnl_cls_set_protocol(filter, ETH_P_IP); err = rtnl_u32_add_key_uint32(filter, key.value, key.mask, key.offset, key.keyoffmask); // правило для совпадение пакета throw_err(err); err = rtnl_u32_set_classid(filter, flowid); // установка идификатора класса throw_err(err); err = rtnl_u32_set_cls_terminal(filter); throw_err(err); err = rtnl_cls_add(sock, filter, NLM_F_CREATE); throw_err(err); rtnl_cls_put(filter); }
Структура u32key содержит 4 поля, по которым происходит классификация пакетов. Рассмотрим на примере команды tc: tc filtertc filter add dev lo parent 1: protocol ip prio 1 u32 match ip dst 127.0.0.1/32 flowid 1:10 поле value отвечает за значение ip адреса, поле mask за маску для ip адреса, offset за смещение относительно начала ip пакета (т.к. протокол ip), в контексте этого примера это направление и keyoffmask это маска для offset.
struct u32key { uint32_t value; uint32_t mask; int offset; int keyoffmask; };
Реализация функции main которая использует все вышеописанные функции
int main() { struct nl_cache *cache; struct rtnl_link *link; struct nl_sock *sock; int if_index; sock = nl_socket_alloc(); nl_connect(sock, NETLINK_ROUTE); rtnl_link_alloc_cache(sock, AF_UNSPEC, &cache); link = rtnl_link_get_by_name(cache, "lo"); struct rtnl_qdisc *qdisc; if (!(qdisc = rtnl_qdisc_alloc())) { std::runtime_error("Can not allocate Qdisc"); } rtnl_tc_set_link(TC_CAST(qdisc), link); rtnl_tc_set_parent(TC_CAST(qdisc), TC_H_ROOT); //Delete current qdisc rtnl_qdisc_delete(sock, qdisc); free(qdisc); //tc qdisc add dev eth1 root handle 1: htb default 20 add_qdisc_htb(link, TC_H_ROOT, TC_HANDLE(0x1, 0), sock, TC_HANDLE(0, 0x20)); //tc class add dev eth1 parent 1: classid 1:1 htb rate 10240kbit add_htb_class(link, TC_HANDLE(0x1, 0), TC_HANDLE(0x1, 0x1), sock, 1250000); //tc class add dev eth1 parent 1:1 classid 1:10 htb rate 7168kbit add_htb_class(link, TC_HANDLE(0x1, 0x1), TC_HANDLE(0x1, 0x10), sock, 12500, 1250000, 1); //tc class add dev eth1 parent 1:1 classid 1:20 htb rate 3072kbit ceil 10240kbit add_htb_class(link, TC_HANDLE(0x1, 0x1), TC_HANDLE(0x1, 0x20), sock, 12500, 1250000, 2); u32key key; inet_pton(AF_INET, "127.0.0.1", &(key.value)); inet_pton(AF_INET, "255.255.255.255", &(key.mask)); key.value = ntohl(key.value); key.mask = ntohl(key.mask); key.offset = 16; // OFFSET_DESTINATION int prio = 1; //tc filter add dev lo parent 1: protocol ip prio 1 u32 match ip dst 127.0.0.1/32 flowid 1:10 add_u32_filter_32key(link, TC_HANDLE(0x1, 0), TC_HANDLE(0x1, 0x10), sock, prio, key); return 0; }
В процессе работы с библиотекой отметил для себя следующие факты: Для фильтрации по vlan_id необходима работа с протоколом 802q1, фильтр u32 не поддерживает классификацию трафика по полям кадра 802q1. Если работать с tc напрямую без c++, то фильтрацию по vlan можно сделать через фильтр tc flower. К сожалению фильтр tc flower в библиотеке реализован не полностью. Поэтому можно маркировать пакеты фаерволом (например nftables, работает с vlan) и далее уже управлять маркированными пакетами через tc (пример).
Больше примеров использования библиотеки можно найти в моём репозитории на гитхабе. Ещё больше интересного в телеграм канале.
