obserwrt: экспорт потоков на OpenWrt после netobserv, pmacct и softflowd
В августе мне захотелось посмотреть, что на самом деле ходит по mesh-сети: кто с кем общается, через какой туннель и сколько. С коллектором вопросов не было — Akvorado принимает IPFIX, сам опрашивает экспортёры по SNMP, чтобы знать имена интерфейсов, и умеет забирать BGP-маршруты по BMP. Сложности начались с экспортёра на роутерах.
В итоге получился obserwrt: eBPF-программы на TC считают потоки прямо в ядре, а небольшой агент забирает их из карты и отправляет в IPFIX. Возможности описаны в README. Здесь — о том, как я к нему пришёл: сначала три готовых экспортёра, потом свой, и что ломалось по дороге.
netobserv-ebpf-agent: одни шаблоны
15 августа. Первым я взял eBPF-агент из OpenShift Network Observability: eBPF
на TC-хуках — ровно то, что хотелось, а ядра на роутерах собраны с BTF. С
первого раза не запустилось вообще ничего. Init-скрипты пакетов ищут настройки
в секции с именем service, а я назвал свои по-другому. Вдобавок интерфейсы
надо было передавать UCI-списком, а не строкой через запятую.
Агент заработал, но потоков в Akvorado так и не появилось. Он слал пакеты байт по сто — только шаблоны IPFIX, без данных. 18 августа я переключился на pmacct, написав в коммите «потом разберусь». Не разобрался, так что почему — до сих пор не знаю.
pmacct: Akvorado выкидывает потоки
pmacct потоки отправлял, но Akvorado выбрасывал их все с ошибкой input and output interfaces missing. Ушёл день, и проблем оказалось три:
- Нет интерфейсов в потоке. pmacct кладёт входной и выходной интерфейс,
только если в
aggregateуказаныin_iface,out_iface. А штатный init-скрипт опциюaggregateиз UCI в конфиг pmacctd просто не передавал. Пришлось подложить свой. - Не те номера интерфейсов. Скрипт нумеровал интерфейсы в карте pmacct
подряд: 1, 2, 3… Akvorado же берёт интерфейсы из SNMP, где у них ifIndex из
ядра, — и ничего не совпадало. Теперь номер берётся из
/sys/class/net/<dev>/ifindex. - Не тот адрес экспортёра. Akvorado различает экспортёры по адресу, а
pmacct представлялся IPv4-адресом, завёрнутым в IPv6 (
::ffff:192.0.2.1), и Akvorado его обратно не разворачивает. Помогnfprobe_source_ipс обычным IPv4, прописанным для каждого хаба явно.
И всё это с сэмплированием 1:100: pmacct копирует пакеты в user space, на каждый пакет его не хватит.
softflowd: тем же вечером
19 августа, вечер. Третьим вариантом стал softflowd — по экземпляру на интерфейс. Он уже есть в пакетах OpenWrt и устроен просто. Но через два дня я сел писать своё. Почему — я сформулировал позже, в README obserwrt, так что это взгляд задним числом, а не записи того вечера:
- softflowd ловит пакеты через
AF_PACKET: каждый пакет копируется в user space и разбирается там, поэтому нагрузка на CPU растёт вместе с потоком пакетов. Для слабых MIPS-роутеров это плохо. - Мои туннели при каждом
ifdown/ifupудаляются и создаются заново — и каждый раз с новым ifIndex. Сниффер, привязавшийся к интерфейсу при старте, так и продолжает слушать старый.
obserwrt на ucode
21 августа. Идея простая: eBPF-программы на входе и выходе нужных интерфейсов обновляют карту потоков в ядре, а user space работает с потоками, а не с пакетами. Значит, и сэмплирование не нужно. Агента я сначала написал на ucode — это скриптовый язык OpenWrt, и пакет получался совсем маленьким.
Чтобы роутер посчитал хоть какие-то пакеты, пришлось разобраться с этим:
- libbpf 1.6 перестал определять тип программы по имени секции
classifier/, теперь его надо указывать явно. А LRU hash map не принимает флагBPF_F_NO_PREALLOC. - У интерфейсов AmneziaWG нет Ethernet-заголовка, пакет сразу начинается с IP. Программе пришлось понимать оба случая. То, как именно она это делала, вылезло боком в октябре.
- ucode — не JavaScript, хоть и очень похож.
push()— глобальная функция, а не метод массива; строку нельзя индексировать;for…inпо массиву отдаёт элементы, а не индексы; нетthrow, и функции не поднимаются (hoisting). Любая из этих мелочей тихо ломала агента на роутере. С hoisting я наступал на грабли ещё дважды: forward declaration, которым я пытался это обойти, в версии ucode из OpenWrt не работает. - Агент на каждом цикле запрашивал у netifd состояние интерфейсов и грузил CPU на 40%. Теперь изменения приходят событиями, а полная сверка идёт раз в 30 секунд.
23 августа появился экспорт в IPFIX. Сначала я проверял его, вручную
разбирая дамп tcpdump, потом добавил в CI end-to-end тест с goflow2. 25
августа роль для роутеров в моём Ansible-репозитории перешла на obserwrt, а
три прежних экспортёра — и мой init-скрипт для pmacct — ушли. Адрес
экспортёра я оставил тем же IPv4, чтобы Akvorado считал их прежними
экспортёрами. На следующий день роутеры начали смотреть ещё LAN-бридж и WAN.
Когда obserwrt заработал на всей сети, всплыло следующее:
- Верификатор не пропускал программу, если та проходила два VLAN-тега, пока указатель на IP-заголовок не получил свою проверку границ.
- Поток может вылететь из LRU-карты между тем, как агент получил список
ключей, и тем, как он прочитал значение. Агенту надо быть готовым получить
там
null. - Раз в 5 секунд агент заново экспортировал все живые потоки, включая простаивающие: в коллектор летели нулевые записи, а одно ядро было занято целиком, когда карта приближалась к пределу (на самой нагруженной площадке до 32k потоков). Теперь поток уходит, только если он новый, изменился или истёк.
Переписывание на C++
4–5 сентября. Но и после этого виртуальная машина ucode съедала слишком много
CPU и памяти на слабых MIPS-роутерах, и я переписал агент на C++23.
eBPF-программа и модель потоков остались прежними, а golden-векторы, снятые с
версии на ucode, гарантировали, что IPFIX на выходе совпадает до байта. Вместо
netifd/ubus теперь rtnetlink — ядро само сообщает об интерфейсах вместе с их
ifIndex. Заодно агент стал собираться в .deb для обычного Linux.
С тех выходных стоит запомнить два бага:
Не приходили события об интерфейсах. Список интерфейсов при старте читался, а вот
ifup/ifdownагент не замечал. Netlink-сокет подписывался на группуRTMGRP_LINKчерезsetsockopt, а затем делал bind сnl_groups = 0. Bind перезаписывает набор групп, и подписка терялась. Правильно — указывать группу сразу в адресе для bind:struct sockaddr_nl sa = { .nl_family = AF_NETLINK, .nl_groups = RTMGRP_LINK }; bind(fd, (struct sockaddr *)&sa, sizeof(sa));Карта нулевого размера. После перепрошивки одного роутера демон падал с
EINVALи перезапускался по кругу. Makefile для OpenWrt передавал-DFLOW_MAP_ENTRIES=с пустой переменной: макрос оказывался определён, но пуст, значение по умолчанию в#ifndefне подставлялось, и в объектный файл попадалmax_entries = 0. Этот define я убрал, размер карты задаётся при загрузке: 8192 по умолчанию и 32768 на самой нагруженной площадке.
Не только роутеры
Октябрь. Я поставил тот же экспортёр на ноды Proxmox, и вылез баг, который
на роутерах был не виден. Программа отличала IP от Ethernet по первому байту
пакета, то есть по номеру версии IP. Но в Ethernet первый байт — это MAC
получателя, и любой кадр на MAC, начинающийся с 4x: или 6x:, разбирался как
IP-пакет. Получался мусор, и поток пропадал. Теперь для каждого интерфейса
агент при подключении смотрит тип линка и вешает нужную пару программ: _eth
или _l3. В том же релизе я исправил длину extension-заголовков IPv6 (она
считалась с ошибкой на 8 байт, и, например, MLD за Hop-by-Hop терялся) и подсчёт
GRO/TSO: такой склеенный пакет раньше считался за один. Теперь всё это
проверяется в CI — eBPF-программы прогоняются через BPF_PROG_TEST_RUN, и
старая версия проваливает все 11 проверок.
Попутно поменялись две мелочи в настройках роутеров. Файрвол теперь не выпускает IPFIX в WAN: если туннели лежат, данные о потоках не уйдут провайдеру по маршруту по умолчанию. А snmpd получил минимальный read-only конфиг — только таблицы интерфейсов, которые опрашивает Akvorado.
И маршруты по BMP
17 октября. Оставались маршруты: BIRD отдаёт их в Akvorado по BMP. Akvorado видел, как поднимаются пиры, но от одного хаба не пришло ни одного маршрута. Оказалось, что BIRD до 3.3.3 в режиме pre-policy без import tables вообще не отправляет маршруты (issue #463 в BIRD), а на этом хабе стоит 3.3.2. Пока он не обновлён, BMP включён только в режиме post-policy.