← Заметки

obserwrt: экспорт потоков на OpenWrt после netobserv, pmacct и softflowd

· ipfix, ebpf, openwrt, akvorado, ucode, cpp

В августе мне захотелось посмотреть, что на самом деле ходит по 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. Ушёл день, и проблем оказалось три:

  1. Нет интерфейсов в потоке. pmacct кладёт входной и выходной интерфейс, только если в aggregate указаны in_iface,out_iface. А штатный init-скрипт опцию aggregate из UCI в конфиг pmacctd просто не передавал. Пришлось подложить свой.
  2. Не те номера интерфейсов. Скрипт нумеровал интерфейсы в карте pmacct подряд: 1, 2, 3… Akvorado же берёт интерфейсы из SNMP, где у них ifIndex из ядра, — и ничего не совпадало. Теперь номер берётся из /sys/class/net/<dev>/ifindex.
  3. Не тот адрес экспортёра. 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, так что это взгляд задним числом, а не записи того вечера:

obserwrt на ucode

21 августа. Идея простая: eBPF-программы на входе и выходе нужных интерфейсов обновляют карту потоков в ядре, а user space работает с потоками, а не с пакетами. Значит, и сэмплирование не нужно. Агента я сначала написал на ucode — это скриптовый язык OpenWrt, и пакет получался совсем маленьким.

Чтобы роутер посчитал хоть какие-то пакеты, пришлось разобраться с этим:

23 августа появился экспорт в IPFIX. Сначала я проверял его, вручную разбирая дамп tcpdump, потом добавил в CI end-to-end тест с goflow2. 25 августа роль для роутеров в моём Ansible-репозитории перешла на obserwrt, а три прежних экспортёра — и мой init-скрипт для pmacct — ушли. Адрес экспортёра я оставил тем же IPv4, чтобы Akvorado считал их прежними экспортёрами. На следующий день роутеры начали смотреть ещё LAN-бридж и WAN.

Когда obserwrt заработал на всей сети, всплыло следующее:

Переписывание на C++

4–5 сентября. Но и после этого виртуальная машина ucode съедала слишком много CPU и памяти на слабых MIPS-роутерах, и я переписал агент на C++23. eBPF-программа и модель потоков остались прежними, а golden-векторы, снятые с версии на ucode, гарантировали, что IPFIX на выходе совпадает до байта. Вместо netifd/ubus теперь rtnetlink — ядро само сообщает об интерфейсах вместе с их ifIndex. Заодно агент стал собираться в .deb для обычного Linux.

С тех выходных стоит запомнить два бага:

Не только роутеры

Октябрь. Я поставил тот же экспортёр на ноды 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.