В статье рассмотрим как реализовать команды утилиты 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 (пример).
Больше примеров использования библиотеки можно найти в моём репозитории на гитхабе. Ещё больше интересного в телеграм канале.