🇬🇧 English documentation: README.en.md
Диагностика сетевого пути для overlay-сетей (OVN) на базе eBPF.
Traceflow отвечает на вопрос, недоступный обычным инструментам: через какие именно
хосты, туннели и VNI прошёл этот пакет? Он отправляет специально маркированный
пробный пакет, и каждый хоп по пути фиксирует, что видел его. Это как traceroute, но
вместо одних лишь IP-маршрутизаторов он видит data plane гипервизора — tap-порты,
OVS-мосты, VXLAN-туннели и VNI, в котором ехал кадр, — в том числе между зонами
доступности.
Метка невидима для обычного трафика и дёшево отбрасывается: eBPF-программа на каждом интерфейсе отсеивает 99.99% пакетов одной инструкцией и внимательно смотрит только на те, что несут метку.
- Как это работает
- Метка: двухуровневая фильтрация
- Управляющий блок (
traceflow_meta) - Что делает eBPF-программа
- Модель наблюдения — tap vs туннель
- Что фиксирует каждая observation
- Компоненты
- Агент
- Клиент
- Коллектор
- OTLP-экспорт (distributed tracing)
- Аутентификация (HMAC)
- Сборка
- Контейнер
- Релизные образы
- Запуск контейнеров
- Тесты
- Стенд 2 AZ через VXLAN
- OVS-DPDK / AF_XDP
- Заметки и ограничения
Три взаимодействующих части: клиент внедряет пробу, агент на каждом гипервизоре наблюдает её (а на хосте назначения — отвечает), а коллектор группирует записи с каждого хопа по run id и восстанавливает путь.
client hypervisor A hypervisor B
┌─────────┐ marked pkt ┌────────────┐ overlay ┌────────────┐
│ client │ ──────────────▶│ agent+eBPF │ ──────────────▶│ agent+eBPF │
└────┬────┘ DSCP62+magic │ TC / XDP │ VXLAN / tap │ TC / XDP │
│ ▲ └─────┬──────┘ └─────┬──────┘
│ │ response │ observations (ring buffer) │
│ │ (dst only) ▼ ▼
│ └─────────────────── collector ◀──── HTTP / OTLP ──────┘
└─────────────────────▶ (group by run id → path)
Полный поток, от начала до конца:
sequenceDiagram
autonumber
participant C as client
participant S as agent · source host
participant T as agent · transit / VTEP
participant D as agent · destination host
participant K as collector
C->>S: marked probe (DSCP=62, TTL=255, meta{id})
S-->>K: observation (hop=0)
S->>T: forward
T-->>K: observation (hop=n)
T->>D: forward over overlay (VXLAN)
D-->>K: observation (hop=n)
Note over D: dst VM за этим iface? проверка HMAC (в ядре)
D-->>C: response (eBPF переписывает пробу на месте, переподписывает, bpf_redirect / XDP_TX)
C->>C: match reply by run id → latency
Проба распознаётся в два этапа, начиная с самого дешёвого.
| Уровень | Проверка | Зачем |
|---|---|---|
| L1 | DSCP=62 (байт ToS 0xF8 в IP-заголовке) |
Одна инструкция на горячем пути. Почти весь трафик имеет другой DSCP и отбрасывается мгновенно, payload при этом даже не читается. |
| L2 | magic 0x54464C4F ("TFLO") в payload |
Один лишь DSCP=62 не уникален; magic-слово подтверждает, что пакет действительно наш. |
Отправитель также фиксирует TTL=255 (hop limit для IPv6), поэтому каждый хоп
может вычислить hop = 255 - TTL без какого-либо общего состояния. Чистая L2-коммутация
TTL не трогает, так что несколько observations на одном L3-хопе делят одно значение
hop, но отличаются node_id — именно так различаются два гипервизора внутри одного
логического хопа. Опционально метку аутентифицирует HMAC (см.
Аутентификация).
Сразу за транспортным заголовком (ICMP / UDP / TCP) проба несёт 30-байтовый управляющий
блок. С --hmac-key к нему приписывается 32-байтовый HMAC-SHA256, и payload становится
62 байта.
offset 0 4 20 21 22 30 62
┌────────┬──────────────────────────┬────┬────┬───────────────┬──── HMAC ─────┐
│ magic │ id (UUIDv4, 16 bytes) │ in │ nr │ timestamp │ SHA-256, 32B │
│ 4 B │ │ 1B │ 1B │ 8 B (ns) │ (optional) │
└────────┴──────────────────────────┴────┴────┴───────────────┴───────────────┘
TFLO run id, shared by all hops │ │ send time
deliver ┘ └ need_response
| Поле | Размер | Значение |
|---|---|---|
magic |
4 | 0x54464C4F ("TFLO"), network byte order |
id |
16 | UUIDv4 — идентификатор запуска, общий для всех хопов |
deliver |
1 | 0 (по умолчанию) = агент перехватывает у получателя; 1 = доставить оригинал в VM |
need_response |
1 | 1 = хост назначения должен ответить |
timestamp |
8 | время отправки, unix-наносекунды (little-endian) — для latency |
Один и тот же двухуровневый фильтр и парсеры работают либо на TC-хуке (по
умолчанию, ingress + egress), либо на XDP-хуке (--xdp, только ingress;
--xdp-tc-egress дополнительно вешает TC-egress-программу, закрывая исходящее
направление). Для каждого пакета:
flowchart TD
P([packet on TC / XDP hook]) --> D{DSCP == 62?}
D -- no --> PASS1([pass · no work])
D -- yes --> T{iface_type}
T -- vxlan / geneve / auto --> V{outer UDP:4789 + VXLAN<br/>or UDP:6081 + Geneve?}
V -- yes --> IN[parse inner frame<br/>extract 24-bit VNI<br/>Geneve: skip option TLVs]
V -- "no (auto)" --> RG[parse as regular frame]
T -- regular --> RG
IN --> M{magic == TFLO?}
RG --> M
M -- no --> PASS2([pass · not ours])
M -- yes --> OBS[[emit observation → ring buffer]]
OBS --> G{answer here?<br/>need_response · not deliver<br/>answerable: echo request / TCP SYN·PSH·FIN / UDP<br/>dst VM привязан к ЭТОМУ iface + direction<br/>not encapsulated}
G -- "no" --> PASS3([pass · forward])
G -- yes --> H{HMAC key set?}
H -- "yes, invalid" --> DROP([drop · count auth failure])
H -- "no / valid" --> RW[rewrite IN PLACE:<br/>swap MAC/IP/ports · ICMP type · TTL=255<br/>TCP SYN-ACK/ACK/FIN-ACK · clear need_response · re-sign]
RW --> TX([TC egress: bpf_redirect в свой ingress<br/>TC ingress: bpf_redirect egress<br/>XDP: XDP_TX])
Парсеры обрабатывают IPv4 и IPv6 (ограниченный обход extension-заголовков), теги VLAN
802.1Q / 802.1AD (включая аппаратный offload через skb->vlan_tci) и VXLAN или Geneve
(ограниченный пропуск option-TLV) поверх IPv4- или IPv6-underlay. Записи попадают в ring buffer на 1 MiB; per-CPU stats-map
считает наблюдённые пакеты, дропы из-за переполнения ring buffer, ответы, отданные
ядром, ошибки HMAC, устаревшие (вне replay-окна) пробы и поглощённые пробы. Агент
добавляет на /metrics userspace-счётчики, в частности
traceflow_local_ip_update_failures_total (провалы регистрации в local_ips —
каждый провал это адрес, за который responder молча не отвечает) и пару здоровья
replay-часов traceflow_clock_age_seconds /
traceflow_clock_update_failures_total.
Ответ строится в ядре. Userspace-responder'а нет: eBPF-программа сама превращает
пробу в ответ на месте и отправляет его обратно. На TC-хуке egress (tap-модель:
OVS → tap → VM) переписанный кадр перенаправляется в ingress того же tap'а
(bpf_redirect(ifindex, BPF_F_INGRESS)), то есть входит в OVS ровно так, как если бы его
отправила VM — с теми же MAC/IP, так что OVN port security его пропускает. На TC-хуке
ingress (агент сидит в netns назначения) кадр перенаправляется в egress того же
интерфейса; на XDP — XDP_TX. Размер никогда не меняется (санированный payload
эхо-отражается), чексуммы правятся инкрементально, а при --hmac-key программа
проверяет HMAC запроса и переподписывает ответ по предвычисленным SHA-256-midstate'ам.
Перехватываются только кадры, на которые responder реально может ответить — ICMP echo
request, TCP SYN/FIN/PSH-с-данными (не RST и не голый ACK), UDP-датаграмма.
Маркированный кадр вне этого множества форвардится, а не поглощается, даже с
установленным need_response: он может сам быть ответом — если на хосте назначения
нет агента, стек самой VM отвечает на пробу, эхо-отражая meta несанированной, и этот
ответ должен дойти до tap'а клиента, а не быть съеден на нём (клиент по-прежнему не
примет такой несанированный ответ за ответ на пробу — кроме случая --deliver, где
это ожидаемо). Перехваченная проба никогда не дропается молча: она отвечена, либо —
при невалидном HMAC (traceflow_auth_failures_total), устаревшем timestamp
(traceflow_stale_probes_total) или когда пакет не переписать (слишком глубокие
заголовки, усечён: traceflow_consumed_total) — дропнута и посчитана.
Это не вопрос вкуса: на OVS-tap'е проба к VM — egress-событие, а sch_handle_egress()
выполняется в __dev_queue_xmit() раньше AF_PACKET tx-tap'а, поэтому userspace-responder
не мог ни увидеть перехваченную пробу, ни отправить ответ в нужную сторону (кадр,
записанный в сокет tap'а, уходит в VM, а не в OVS). bpf_redirect — единственный
примитив, который кладёт кадр в RX-путь tap'а.
Две точки attach отражают реальный деплой OVN/OVS: внутри AZ агент сидит на tap VM, между AZ — на underlay / туннельном netdev.
intra-AZ (tap model) inter-AZ (VXLAN / Geneve)
agent on the VM tap agent on the underlay / tunnel netdev
VM ──tap──▶ [eBPF] ──▶ OVS ──▶ ... ... ──▶ [eBPF] ──▶ VXLAN/Geneve ──▶ other AZ
inner frame, DSCP+magic visible outer Eth/IP/UDP/tunnel + inner
iface_type = REGULAR iface_type = VXLAN|GENEVE, VNI from wire
Есть несколько вариантов того, как VNI присутствует (или отсутствует) на проводе, и агент покрывает их все:
- Regular / tap (
iface_type=regular) — внутренний (tenant) кадр[Eth][VLAN?][IPv4/IPv6][L4][meta], наблюдаемый до инкапсуляции или после декапсуляции. VNI в байтах нет. - VXLAN underlay (
iface_type=vxlan) — агент пропускает внешнийEth/IP/UDP:4789/VXLAN, читает 24-битный VNI прямо с провода и парсит inner-кадр. Внешний заголовок может быть IPv4 или IPv6. - Geneve underlay (
iface_type=geneve) — то же для дефолтной меж-chassis инкапсуляции OVN: агент пропускаетEth/IP/UDP:6081/Geneve, читает VNI и проходит option-TLV переменной длины (до 252 байт, ограниченно), чтобы добраться до inner-кадра. Наблюдаются только версия 0, protocol type0x6558(внутренний Ethernet-кадр — то, что шлют OVN/OVS) и пакеты без O-бита (OAM/control). Сами option-TLV пропускаются, но не декодируются: logical ingress/egress port keys OVN (class0x0102) не извлекаются — в 88-байтовой observation для них нет поля; datapath определяется по VNI (поверх Geneve OVN кладёт туда ключ датапаса без сдвига). - VXLAN netdev (per-tenant устройство) — ядро уже декапсулировало, поэтому eBPF
видит inner-кадр без VNI в байтах. Netlink-watcher читает VNI устройства
(
Vxlan.VxlanId) в map по ключуifindex, а eBPF тегирует observation поskb->ifindex. Устройстваcollect_metadata(VNI per-packet) покрываются черезbpf_skb_get_tunnel_key(заметьте: helper не сообщает тип туннеля, поэтому декапсулированный кадр, обогащённый этим путём, тегируетсяVXLANдаже на Geneve-устройствеcollect_metadataвроде OVS'ногоgenev_sys_6081). auto(по умолчанию) — на каждый пакет сначала попытка VXLAN/Geneve, откат на regular. В observation всегда указывается обнаруженныйiface_type, никогда неauto.
Перехват привязан к интерфейсу и направлению VM назначения. Карта локальных VM
ключуется парой (ifindex, адрес), и каждая запись говорит, на каком направлении хука
она отвечается: egress для VM за tap'ом (проба уходит с хоста в сторону VM),
ingress для адреса, локального для netns агента (проба приходит на интерфейс). Ядро
поглощает пробу, только если само ответит на неё: выставлен need_response, deliver
сброшен, внутренний dst_ip привязан к этому интерфейсу в этом направлении, кадр не
инкапсулирован в VXLAN/Geneve, и этот агент отвечает — и тогда отвечает на месте, так что стек VM
её не видит и не может ответить тоже. Транзит и источник никогда не совпадают; в частности,
проба VM→VM между двумя tap'ами одного хоста проходит tap VM-источника (ingress, от VM)
нетронутой и отвечается только на tap'е VM назначения (egress). Проба с --deliver
вместо этого доставляется в VM, а агент молчит.
Каждый маркированный пакет становится одной observation. Агент выдаёт её как JSON (и отправляет в коллектор / OTLP):
{
"id": "3f2b1c9e-8a7d-4e6f-b012-9c8d7e6f5a4b",
"node_id": "hv-07",
"hop": 1,
"direction": "INGRESS",
"vni": 100,
"vlan": 42,
"action": "FORWARDED",
"protocol": "TCP",
"ip_version": 4,
"ifindex": 7,
"iface": "tap0abc",
"logical_switch": "ls-tenant-a",
"az": "az-east-1",
"src_ip": "10.10.0.1",
"dst_ip": "10.10.0.2",
"src_port": 40000,
"dst_port": 80,
"tcp_flags": "SYN|ACK",
"iface_type": "VXLAN",
"timestamp": "2026-08-19T12:00:00.123456789Z",
"capture_ns": 51230948120,
"latency_ns": 351200
}| Поле | Источник | Значение |
|---|---|---|
id |
пакет | run UUID; ключ связывания всех хопов |
node_id |
агент | какой хост выдал запись (--node-id, по умолчанию hostname) |
hop |
пакет | 255 - TTL; общий для observations на одном L3-хопе |
direction |
ядро | INGRESS / EGRESS (сторона TC-хука) |
vni |
провод / устройство | VXLAN/Geneve VNI (0, когда без инкапсуляции) |
vlan |
провод / offload | 802.1Q VID (опускается, если без тега) |
action |
eBPF | FORWARDED / INTERCEPTED |
protocol |
пакет | ICMP / ICMPv6 / TCP / UDP |
ip_version |
пакет | 4 или 6 |
ifindex, iface |
ядро | индекс интерфейса и разрешённое имя |
logical_switch |
агент | OVN logical switch для VNI (best-effort, резолвится асинхронно: первые observations VNI могут прийти с пустым полем, следующие обогащаются; для OVN-native VXLAN — ovn-encap-type=vxlan, где VNI на проводе = dp_key<<12 | port_key — после точного ключа пробуется ключ датапаса vni>>12; VNI, увиденный поверх Geneve, ищется только по точному ключу — Geneve-датапасы OVN ключует без сдвига) |
az |
агент | зона доступности (--az) |
src_ip, dst_ip |
пакет | внутренние адреса |
src_port, dst_port |
пакет | для TCP/UDP |
tcp_flags |
пакет | напр. SYN|ACK (только TCP) |
iface_type |
eBPF | REGULAR, VXLAN или GENEVE |
timestamp |
агент | wall-clock захвата (RFC 3339) |
capture_ns |
ядро | монотонное время захвата (bpf_ktime_get_ns) |
latency_ns |
агент | now − meta.timestamp, клампится в ≥ 0 |
clock_skew |
агент | true, когда сырая разница была отрицательной (часы хостов рассинхронены) |
bpf/traceflow.c eBPF program: 2-level filter, VXLAN/Geneve, VLAN, IPv6, ring buffer
bpf/traceflow.h structs shared between eBPF and Go (kept byte-for-byte in sync)
internal/tfmeta meta constants, (de)serialisation and HMAC of traceflow_meta
internal/pkt packet builders: Eth/IP{4,6}/ICMP{,v6}/UDP/TCP/VXLAN/Geneve + checksums
internal/observation ring-buffer decode → enriched JSON record
internal/ovs OVS/OVN discovery (ovs-vsctl / ovn-nbctl / ovn-sbctl) + parsers
agent/ eBPF loader, ring reader, local-VM map, watchers, metrics, emitters
client/ marked-probe sender + filtered AF_PACKET reply sniffer
collector/ HTTP collector: group observations by run id, assemble paths
scripts/ demo-netns.sh, lab-2az-vxlan.sh
tests/ Go unit tests + integration suite (netns / VXLAN / OVN)
По одному агенту на гипервизор. При старте он:
- грузит eBPF-программу через
cilium/ebpf(pure Go, без CGO); - пишет config map (
iface_type,respond,tcp_respond,hmac), карту локальных VM ((ifindex, адрес) → направление ответа) и, при--hmac-key, SHA-256-midstate'ы, по которым ядро проверяет и переподписывает (сам ключ агента не покидает); - прикрепляет программу;
- читает observations из ring buffer и печатает JSON (или отправляет дальше).
Ядро отвечает только за VM назначения, на её собственном интерфейсе — задача агента держать эту карту в согласии с VM.
Attach. По умолчанию — TC-хук (ingress + egress): TCX на ядре ≥ 6.6, с
автоматическим откатом на классический clsact + cls_bpf через netlink. С --xdp прикрепляется к XDP-хуку (только ingress) — см.
OVS-DPDK / AF_XDP; добавьте --xdp-tc-egress, чтобы рядом с
XDP повесить TC-egress-программу и наблюдать также трафик, отправляемый стеком.
Ответы. Observations снимаются на каждом хопе, но ответ на need_response=1
строится, только если внутренний dst_ip — локальный адрес VM, привязанный к интерфейсу,
на котором проба (--local-ip или резолвит watcher). Транзитные и VTEP-узлы работают в
режиме observe-only. Ответ порождает сама eBPF-программа (см.
Что делает eBPF-программа): проба переписывается на месте —
разворот адресов переиспользует MAC/IP самой VM, ровно то, что ожидает OVN port security, —
и отправляется обратно через RX-путь tap'а (tap-модель), в egress того же интерфейса
(агент в netns назначения) или через XDP_TX. Оригинал до VM не доходит, поэтому её стек
не ответит тоже; проба с --deliver вместо этого доставляется в VM, а агент молчит.
В ядре отвечаются ICMP/ICMPv6 echo, UDP и TCP; TCP — без состояния (SYN → SYN/ACK с ISN,
выведенным из 4-tuple; данные → PSH/ACK-эхо; FIN → FIN/ACK; наш seq — это ack клиента),
а --tcp-respond=false оставляет TCP реальному стеку VM.
--local-ip и направление. В статическом режиме адреса привязываются к --iface;
в watch-режимах — к каждому наблюдаемому интерфейсу (tap или VXLAN-netdev) — это
документированный способ отвечать, когда OVN-резолв недоступен. --local-ip-dir
выбирает направление хука: auto (по умолчанию) — ingress, если адрес настроен на
интерфейсе этого netns, иначе egress (VM за интерфейсом). Адреса, найденные
--watch, всегда привязываются к своему tap'у на egress.
Watch-режимы (динамический attach).
--watch— находит OVN VM-tap'ы черезovs-vsctl(external_ids:iface-id), attach'ит/detach'ит eBPF по мере появления/исчезновения VM и резолвит IP каждой VM из OVN NB DB (ovn-nbctl, best-effort), так что responder отвечает автоматически. Вся NB DB читается одним сканомlist Logical_Switch_Portна каждое пересканирование (не exec на порт), а адреса уже подключённых tap'ов ресинхронизируются на каждом скане: VM, сменившая IP при неизменном tap'е, перерегистрируется. Привязка держится за ifindex, а не за имя: tap, удалённый и пересозданный с тем же именем (hard reboot / rebuild VM), — это новый netdev, поэтому агент освобождает мёртвую привязку по netlinkRTM_DELLINKи attach'ит новый netdev (заново резолвя его IP), как только тот появляется; периодический скан--watch-intervalтоже сравнивает ifindex'ы — как fallback.--watch-vxlan— следит за per-tenant VXLAN-netdev'ами через netlink link events (создаются/удаляются OVN или любым контроллером) и тегирует observations VNI устройства.
Ключевые флаги.
| Флаг | По умолчанию | Значение |
|---|---|---|
--iface NAME |
— | attach к одному интерфейсу (статический режим) |
--iface-type |
auto |
auto | regular | vxlan | geneve |
--node-id |
hostname | идентификатор в каждой observation |
--respond |
true |
отвечать на need_response=1 для локальных IP VM (в ядре) |
--tcp-respond |
true |
отвечать и на TCP; false отдаёт TCP стеку VM |
--local-ip |
— | локальные IP VM через запятую; привязываются к --iface или к каждому наблюдаемому интерфейсу |
--local-ip-dir |
auto |
auto | ingress | egress: направление хука, на котором отвечается --local-ip |
--watch / --watch-vxlan |
false |
динамический attach (см. выше) |
--watch-interval |
5s |
с --watch: период пересканирования OVS (ovs-vsctl + ovn-nbctl) — fallback за netlink link events |
--hmac-key |
— | общий секрет; ядро никогда не отвечает на пробу с невалидным/отсутствующим HMAC. Предпочитайте --hmac-key-file или env TRACEFLOW_HMAC_KEY: argv видна всем (/proc/*/cmdline, ps, docker inspect) |
--hmac-key-file |
— | читать HMAC-секрет из файла (завершающий перевод строки отбрасывается); взаимоисключим с --hmac-key |
--hmac-window |
30s |
с --hmac-key: проба, чей meta.timestamp дальше этого от часов агента, дропается и считается (traceflow_stale_probes_total), а не отвечается — ограничивает replay перехваченных подписанных проб; 0 выключает проверку; явно заданное без --hmac-key — ошибка запуска |
--az |
— | зона доступности, добавляемая в observations |
--metrics-addr |
— | host:port для Prometheus /metrics + /healthz |
--collector-url |
— | POST каждой observation как JSON на этот URL |
--collector-token |
— | bearer-токен в каждом POST в коллектор (пара к --auth-token коллектора); предпочитайте env TRACEFLOW_COLLECTOR_TOKEN, а не argv |
--otlp-endpoint |
— | экспорт каждой observation как OTLP-span |
--otlp-tls |
false |
TLS (https) для OTLP-экспорта; выключено = plain HTTP |
--otlp-tls-ca |
— | с --otlp-tls: PEM-набор CA, доверяемый в дополнение к системным |
--otlp-tls-skip-verify |
false |
с --otlp-tls: не проверять сертификат (только для стендов) |
--xdp |
false |
attach на XDP вместо TC |
--xdp-tc-egress |
false |
вместе с --xdp: дополнительно TC-egress-программа (XDP видит только ingress); включается автоматически, когда какой-либо адрес отвечается на egress — с --watch / --watch-vxlan при ответах и с --iface, когда --local-ip резолвится в egress |
Отправитель проб. Каждая проба несёт DSCP=62, TTL=255 и блок traceflow_meta, так
что агенты её наблюдают.
| Команда | Что отправляет |
|---|---|
icmp |
ICMP / ICMPv6 echo request (--response ждёт echo reply) |
udp |
UDP-датаграмму (--response ждёт UDP-ответ) |
htcp |
только TCP handshake: SYN → SYN/ACK → ACK (всегда отвечает агент — --response подразумевается) |
tcp |
полный TCP-диалог: handshake + data + FIN (всегда отвечает агент — --response подразумевается) |
vxlan |
VXLAN-инкапсулированный маркированный ICMP с заданным VNI |
IPv4 без VLAN отправляется через AF_INET raw socket (IP_HDRINCL), и маршрутизирует
его ядро. IPv6 или тег VLAN требуют L2-кадра, поэтому проба уходит через AF_PACKET
(--iface + --dst-mac, опционально --vlan). --as-vm заполняет source IP/MAC из
OVS-tap'а, чтобы OVN port security принял внедрённый кадр. --hmac-key подписывает
пробу. По умолчанию агент назначения перехватывает пробу и отвечает сам; добавьте
--deliver, чтобы проба дошла до VM и ответил её собственный стек. tcp и htcp
всегда запрашивают ответ: run id несёт только SYN/ACK, собранный агентом в ядре, иначе
диалог завершиться не может (SYN/ACK настоящего стека meta не содержит; с --deliver
клиент предупреждает, и handshake завершается таймаутом). Ответы ловятся
AF_PACKET-снифером и сопоставляются по run id — а затем аутентифицируются: с
--hmac-key ответ обязан верифицироваться под ключом (агент переподписывает его в
ядре), а пакет, в котором всё ещё стоит need_response=1, ответом агента не является
(кроме --deliver, где стек VM эхо-ит пробу как есть); кадр, переданный самим хостом
(PACKET_OUTGOING), в котором ещё стоит запрос ответа, — это собственная проба клиента
по пути наружу, и она отбрасывается даже при --deliver, тогда как исходящий кадр со
снятым флагом принимается: ответ, который бридж форвардит в сниффаемый порт (к VM,
имперсонируемой через --as-vm), виден только как исходящий. Всё остальное, что лишь
повторяет run id, логируется и игнорируется. На сокете стоит classic-BPF-фильтр
(IPv4/IPv6 с DSCP 62 или UDP/4789|6081, за не более чем двумя VLAN-тегами), так что ядро
копирует в userspace только кадры-кандидаты; при L2-отправке (--iface) сокет привязан
к этому интерфейсу, иначе слушает все интерфейсы, чтобы асимметричный обратный путь
(policy routing, ECMP, anycast, VXLAN-underlay) всё равно доставил ответ.
# ICMP echo через overlay, ждём ответ
sudo bin/client icmp --dst 10.10.0.2 --response
# Полный TCP-диалог на порт 80, с подписью (tcp/htcp подразумевают --response)
sudo bin/client tcp --dst 10.10.0.2 --dport 80 --hmac-key s3cret
# Только handshake
sudo bin/client htcp --dst 10.10.0.2 --dport 80
# Внедрение от имени VM (OVN port security) через её tap, VLAN 42
sudo bin/client icmp --dst 10.10.0.2 --iface tap0abc --dst-mac 02:00:00:00:00:01 \
--vlan 42 --as-vm --response
# VXLAN-инкапсулированная проба с явным VNI
sudo bin/client vxlan --dst 192.0.2.9 --inner-src 10.0.0.1 --inner-dst 10.0.0.2 --vni 100
# Geneve-инкапсулированная проба (несёт один OVN-style option TLV, class 0x0102)
sudo bin/client geneve --dst 192.0.2.9 --inner-src 10.0.0.1 --inner-dst 10.0.0.2 --vni 100collector принимает observations по HTTP и группирует их по run id — ключу,
стабильному даже там, где NAT переписывает 5-tuple, так что путь через SNAT/DNAT
собирается как один поток. Агенты отправляют в него с --collector-url.
POST / one observation as JSON (what the agent ships) → 204
GET /paths list run ids with observation counts
GET /path?id=<uuid> the run's observations, ordered by hop then time
GET /healthz
Флаги: --addr (по умолчанию :8080), --max-runs (по умолчанию 10000; при
превышении вытесняется run, появившийся раньше всех — FIFO по первому наблюдению, а не по
последнему обращению), --auth-token (требовать этот bearer-токен на всех эндпоинтах,
кроме /healthz; агенты передают его через --collector-token; сравнение —
constant-time поверх SHA-256-дайджестов, так что по таймингу не утекают ни байты
токена, ни его длина). Токен лучше передавать через env TRACEFLOW_AUTH_TOKEN (а на
стороне агента — TRACEFLOW_COLLECTOR_TOKEN), а не в argv: командная строка видна всем
через /proc/*/cmdline. Хранилище — in-memory: коллектор является референсным приёмником; для продакшена
направьте --collector-url на свой ingester или используйте OTLP.
Приём захарден для открытого порта: тело запроса ограничено 64 KiB, run id обязан
быть каноническим UUID (нормализуется в нижний регистр — это ключ map'а, и он
возвращается в выдаче), у HTTP-сервера выставлены read/write/idle-таймауты против
slow-loris-клиентов, а observations упорядочиваются по распарсенному времени
захвата, так что разные UTC-смещения или усечённые доли секунды от разных агентов не
перемешают путь (агенты с этого же изменения ставят метки в UTC).
Трассируемый путь ложится 1:1 на tracing-спаны, поэтому агент может экспортировать прямо в любой OpenTelemetry-бэкенд (Tempo, Jaeger, …) — без собственного UI:
sudo bin/agent --watch --az az-1 --otlp-endpoint otel-collector:4318Каждая observation становится span; run-UUID и есть 128-битный trace id (так
observations всех хостов попадают в один trace); поля становятся атрибутами
(traceflow.node_id/az/hop/vni/direction/…, source.address, destination.address).
Спаны прогона делят синтетический parent, выведенный из UUID, и упорядочены по атрибуту
hop — сетевой путь не является деревом вызовов, поэтому корреляция идёт по trace id +
hop, а не по причинной вложенности. Транспорт — OTLP/HTTP (protobuf POST на
<endpoint>/v1/traces) — по умолчанию plain HTTP, что нормально для localhost-коллектора.
Для удалённого бэкенда добавьте --otlp-tls (https с системными корневыми
сертификатами), --otlp-tls-ca <pem> для приватного CA и — только для стендов —
--otlp-tls-skip-verify (шифрование без аутентификации); последние два требуют
--otlp-tls.
DSCP=62 + magic тривиально подделывается. --hmac-key <секрет> включает HMAC-SHA256
поверх 30-байтового ядра meta. eBPF-responder проверяет его в ядре и никогда не
отвечает без валидного HMAC (проба перехватывается, дропается и учитывается как
traceflow_auth_failures_total), что закрывает амплификацию и неаутентифицированное
зондирование. Ответы переподписываются поверх санированного ядра meta, так что обратный
путь остаётся валиден, а клиент проверяет эту подпись, прежде чем принять ответ: run
id виден всем, кто видел пробу, поэтому пакет, который его лишь повторяет (устаревший
HMAC или не снятый need_response), игнорируется, а не выдаётся за ответ.
Как ядро может себе это позволить: HMAC = SHA256((K⊕opad) ‖ SHA256((K⊕ipad) ‖ m)), а
30-байтовое ядро и 32-байтовый внутренний дайджест помещаются в один дополненный блок
SHA-256. Агент предвычисляет состояния хэша после двух ключевых блоков (midstate'ы) и
отдаёт ядру только их — сам ключ в map не попадает, — так что проверка или подпись стоят
ровно два compression'а, и выполняются они только для проб, на которые вот-вот будет
ответ. Одна честная оговорка: midstate'ы в map'е hmac_state эквивалентны ключу для
подделки MAC'ов над этими сообщениями — кто может прочитать этот map, может и
подписывать пробы. Чтение требует CAP_BPF на хосте агента, т.е. той же привилегии,
с которой ключ можно было бы достать и из памяти агента, так что граница доверия не
меняется — но «ключ не попадает в map» не значит «содержимое map безобидно». Всё
остальное остаётся на двухинструкционном DSCP-fast-path; observations
остаются advisory (любой, кто знает DSCP и magic, может добиться, чтобы пакет наблюдался).
Защита от replay. Валидной подписи самой по себе мало: подписанное ядро meta —
константная строка байт, так что перехваченную need_response-пробу можно было бы
воспроизводить вечно, выдаивая из responder'а ответы. --hmac-window (по умолчанию
30 с, 0 = выкл.) это ограничивает: агент ведёт wall-clock-map (обновляется раз в
секунду, записывается до attach), и ядро отвергает — drop + traceflow_stale_probes_total
— любую пробу-кандидата на ответ, чей meta.timestamp лежит вне окна в любую сторону,
до того, как потратит на неё SHA-256-compression'ы. Timestamp уже входит в подписанное
30-байтовое ядро, так что replayer не может освежить его без ключа, а формат на проводе
не изменился (старые клиенты совместимы, пока часы отправителя и агента сходятся в
пределах окна; рассинхрон виден по счётчику stale — расширьте окно или почините
синхронизацию). Отдельного LRU-фильтра по id за окном сознательно нет: один run id
легитимно отвечается несколько раз (SYN, данные и FIN TCP-диалога несут один id и одно
подписанное ядро), так что replay-фильтр по id сломал бы сам протокол пробы. Остаточный
риск — replay подписанной пробы в пределах окна, что даёт атакующему ровно то, что дала
исходная проба: ограниченное, неамплифицирующее эхо исходному отправителю.
sudo apt install -y clang llvm libbpf-dev linux-libc-dev make
# Go >= 1.25 (в дистрибутивах часто старее — берите с go.dev/dl или собирайте в контейнере)
make deps # go mod tidy
make generate # bpf2go: bpf/traceflow.c → agent/traceflow_bpf*.go
make build # → bin/{agent,client,collector}Рантайм: Linux (TCX требует ≥ 6.6; на старых ядрах используется clsact-fallback).
Агент запускается от root или с CAP_BPF + CAP_NET_ADMIN + CAP_PERFMON (загрузка
и attach программ; CAP_PERFMON — то, что позволяет verifier'у принять арифметику над
указателями на пакет); на ядрах < 5.8 вместо них CAP_SYS_ADMIN, а ядрам < 5.11
дополнительно нужен CAP_SYS_RESOURCE для поднятия memlock rlimit. Агент не открывает
сокетов — ответ собирается в ядре, — поэтому CAP_NET_RAW ему не нужен; он нужен
клиенту (raw- и AF_PACKET-сокеты). Проверено capsh --drop на ядре 6.8.
Таргеты make: deps, generate, build, agent, client, collector, test,
itest, image, image-itest, images, images-push, clean.
make image # unit tests run in the builder stage
podman run --rm --privileged traceflow itest # integration suite (rootful for real BPF)Rootless podman может собрать образ и прогнать unit-тесты, но загрузка eBPF и
ip netns exec требуют настоящих BPF/net-прав — интеграционные тесты SKIP (не падают),
когда их нет. Запускайте rootful или на хосте.
Каждый компонент поставляется отдельным минимальным образом, собираемым из
deploy/docker/Dockerfile (общая кросс-компилирующая builder-стадия + по одной лёгкой
финальной стадии на компонент):
| Образ | База | Назначение |
|---|---|---|
traceflow-agent |
debian-slim + iproute2 + OVS/OVN CLI | загружает eBPF; --watch на гипервизорах, --watch-vxlan на сетевых нодах (privileged) |
traceflow-client |
debian-slim | внедряет маркированные пробы |
traceflow-collector |
distroless static (nonroot) | HTTP-агрегатор путей |
Локальная сборка (одна арх., загружается в движок):
make images # все три, тег :dev
make image-agent VERSION=v1.4.0 # только одинПуш тега vX.Y.Z запускает .github/workflows/release.yml: каждый образ собирается
мультиарх (linux/amd64 + linux/arm64) и пушится в GHCR, затем создаётся GitHub
Release с авто-заметками и тарболами статических бинарников:
git tag v1.4.0 && git push origin v1.4.0# теги :X.Y.Z, :X.Y и (для не-prerelease тегов) :latest
docker pull ghcr.io/slepwin/traceflow-agent:v1.4.0
docker pull ghcr.io/slepwin/traceflow-client:v1.4.0
docker pull ghcr.io/slepwin/traceflow-collector:v1.4.0ci.yml на каждый PR прогоняет gofmt / vet / build / unit-тесты, staticcheck
и govulncheck (оба блокирующие), eBPF-тесты под
root — программы реально загружаются, с TRACEFLOW_REQUIRE_BPF=1, чтобы регрессия
verifier'а падала, а не скипалась, — интеграционный набор под root (advisory-job) и
собирает все три образа (без пуша), так что релизный путь проверяется ещё до того, как
поставлен тег.
Три образа соответствуют трём ролям. Скачайте их один раз (Podman — drop-in-замена:
подставьте podman вместо docker в любой команде ниже):
docker pull ghcr.io/slepwin/traceflow-agent:v0.1.0
docker pull ghcr.io/slepwin/traceflow-client:v0.1.0
docker pull ghcr.io/slepwin/traceflow-collector:v0.1.0Каждый гипервизор запускает агента в режиме --watch (по умолчанию для compute-ноды):
он находит OVN VM-tap'ы через ovs-vsctl и резолвит IP каждой VM из OVN NB DB,
attach'ит/detach'ит eBPF по мере появления/исчезновения VM. Образ содержит OVS + OVN
клиентские утилиты, поэтому --watch работает из коробки — нужны лишь сетевое
пространство имён хоста, BPF + net-права и сокет OVS DB:
docker run -d --name traceflow-agent \
--network host --privileged \
-v /run/openvswitch:/run/openvswitch \
ghcr.io/slepwin/traceflow-agent:v0.1.0 \
--watch --az az-east-1 \
--collector-url http://collector.host:8080 \
--metrics-addr :9090Если OVN NB DB удалённая, укажите её через -e OVN_NB_DB=tcp:<central>:6641 (резолв IP —
best-effort; без него передавайте --local-ip явно). Вариант с минимумом прав — вместо
--privileged явные capabilities (--cap-add SYS_ADMIN вместо BPF/PERFMON на ядрах
< 5.8; на ядрах < 5.11 добавьте --cap-add SYS_RESOURCE):
docker run -d --name traceflow-agent --network host \
--cap-add BPF --cap-add NET_ADMIN --cap-add PERFMON \
-v /run/openvswitch:/run/openvswitch \
ghcr.io/slepwin/traceflow-agent:v0.1.0 --watch --az az-east-1Статический режим (--iface eth0 --local-ip 10.10.0.2) тоже доступен, когда нужно
зафиксировать один интерфейс и IP VM, за которые отвечаете, без OVS-обнаружения.
Сетевая нода несёт меж-AZ VXLAN-туннели, поэтому её агент работает в режиме
--watch-vxlan: следит за per-tenant VXLAN-netdev'ами через netlink и тегирует
каждую observation VNI устройства. Режим только netlink — сокеты OVS/OVN не нужны (это
тот же образ, что и на compute-ноде; OVS/OVN-утилиты здесь просто не используются):
docker run -d --name traceflow-agent \
--network host --privileged \
ghcr.io/slepwin/traceflow-agent:v0.1.0 \
--watch-vxlan --az az-east-1 \
--collector-url http://collector.host:8080Отправка маркированных проб требует raw-сокета и доступа к реальной сети, поэтому — сеть
хоста + CAP_NET_RAW. Клиент одноразовый: отправляет, ждёт ответ (при --response),
печатает результат и выходит — так что используйте --rm.
docker run --rm --network host --cap-add NET_RAW \
ghcr.io/slepwin/traceflow-client:v0.1.0 \
icmp --dst 10.10.0.2 --responseВсе подкоманды (icmp, udp, htcp, tcp, vxlan, geneve) и флаги работают ровно как в
разделе Клиент — например, подписанный полный TCP-диалог:
docker run --rm --network host --cap-add NET_RAW \
ghcr.io/slepwin/traceflow-client:v0.1.0 \
tcp --dst 10.10.0.2 --dport 80 --hmac-key s3cretКоллектор — обычный HTTP-сервис без привилегий (работает под non-root пользователем на
distroless). Опубликуйте порт и направьте на него --collector-url агентов:
docker run -d --name traceflow-collector \
-p 8080:8080 \
ghcr.io/slepwin/traceflow-collector:v0.1.0 --addr :8080Затем читайте собранные пути:
curl http://collector.host:8080/paths
curl "http://collector.host:8080/path?id=<run-uuid>"Хранилище — in-memory; для продакшена направьте --collector-url на свой ingester или
используйте --otlp-endpoint (см. OTLP-экспорт).
compute-ноды (гипервизоры) сетевая нода ops-хост
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ traceflow-agent │ │ traceflow-agent │ │ traceflow-collector │
│ --watch │ │ --watch-vxlan │ │ :8080 │
│ (OVN VM tap disco) │ │ (меж-AZ VXLAN VNI) │ │ (группа по run id) │
└──────────┬───────────┘ └──────────┬───────────┘ └───────────▲──────────┘
└────────────── observations по HTTP ─────────────────────────┘
traceflow-client — запускается разово там, откуда начинаете пробу
По одному агенту на гипервизор в --watch (сеть хоста, privileged), по одному на сетевую
ноду в --watch-vxlan, один коллектор в доступном месте, а клиент запускается разово
там, откуда хотите начать пробу.
make test # Go unit-тесты (без root)
make itest # интеграционная suite (нужен root; см. ниже)Каждый интеграционный тест чисто SKIP'ается, когда его пререквизитов нет:
| Тест | Что проверяет |
|---|---|
test_veth_multinetns.sh |
routed ns1 — ns2(router) — ns3: hop 0/1 через L3-роутер, корреляция по run-id, ответ из ядра, прошедший через роутер обратно к клиенту |
test_tap_egress.sh |
tap (egress) модель ответа без OVS: client → router → VM, агент на veth со стороны роутера с --local-ip (auto → egress), VM игнорирует echo request; ICMP, UDP и полный TCP-диалог отвечаются на EGRESS и возвращаются через RX-путь tap'а; клиент игнорирует собственную исходящую пробу |
test_vxlan.sh |
парсинг VXLAN underlay (VNI, структурный intercept-guard) |
test_geneve.sh |
парсинг Geneve underlay (VNI за OVN-style option-TLV, структурный intercept-guard, инкапсуляция собственного geneve-netdev ядра, наблюдаемая с underlay) |
test_ipv6_vlan.sh |
наблюдение ICMPv6 и 802.1Q VLAN |
test_watch_vxlan.sh |
netlink VXLAN-watcher: attach существующего, динамика create/delete, обогащение device-VNI |
test_watch.sh |
OVS tap-watcher (--watch, нужен OVS): обнаружение, ответ из ядра на egress tap'а, tap, удалённый и пересозданный с тем же именем, переattach'ивается (новый ifindex) и снова отвечает |
test_hmac.sh |
подписанная проба отвечается в ядре (клиент получает переподписанный ответ), неподписанная перехватывается и отклоняется (по метрикам); подписанная проба с устаревшим timestamp отклоняется replay-окном; клиент игнорирует ответ с неверным HMAC (responder без ключа) |
test_clsact.sh |
clsact/cls_bpf fallback (TRACEFLOW_FORCE_CLSACT=1) |
test_xdp.sh |
путь attach через XDP (--xdp): observation на ingress, ответ через XDP_TX, ответ наблюдается на peer'е |
test_xdp_tc_egress.sh |
--xdp --xdp-tc-egress: ingress через XDP (+ ответ XDP_TX) + egress через TC-компаньона; статический --xdp --local-ip для VM за интерфейсом включает компаньона автоматически и отвечается на egress |
test_vxlan_2az.sh |
два шасси / две AZ, соединённые VXLAN (нужен OVS); --watch-vxlan --local-ip отвечает на overlay-netdev'е |
Среди Go unit-тестов есть suite на BPF_PROG_TEST_RUN (agent/ebpf_test.go, выполняется
под root, иначе skip — либо fail при TRACEFLOW_REQUIRE_BPF=1, который выставляет CI,
чтобы skip не мог скрыть сломанную программу): она прогоняет все комбинации протокол/семейство/VLAN/HMAC через
настоящие программы TC ingress, TC egress и XDP и сравнивает переписанный кадр побайтно с
чисто-Go референсной моделью преобразования (internal/pkt/reply.go), чьи собственные
тесты пересчитывают каждую чексумму с нуля.
Два шасси в стиле OVN в двух AZ, соединённых VXLAN: межшассийный underlay построен на OVS internal-портах (реальные kernel-netdev'ы), а inter-AZ overlay — это kernel VXLAN-устройство.
host: OVS br-underlay (normal L2 switch)
ula ── moved into ns az1 ulb ── moved into ns az2
┌──────────────── ns az1 (AZ1) ────────┐ ┌──────────── ns az2 (AZ2) ─────────┐
│ ula 172.16.9.1/24 (underlay) │ │ ulb 172.16.9.2/24 │
│ vxlan0 id100 remote .2 ── VXLAN(100) ┼───┼── vxlan0 id100 remote .1 │
│ 10.0.0.1/24 (VM-A) │ │ 10.0.0.2/24 (VM-B) │
└───────────────────────────────────────┘ └───────────────────────────────────┘
A marked ICMP VM-A → VM-B is observed at three points, one run id:
(1) VM-A overlay netdev (device vni=100)
(2) inter-AZ VXLAN leg on the underlay (packet vni=100, outer Eth/IP/UDP/VXLAN)
(3) VM-B overlay netdev (device vni=100)
sudo ovs-ctl start
sudo ./scripts/lab-2az-vxlan.sh up
sudo ./scripts/lab-2az-vxlan.sh agents &
sudo ./scripts/lab-2az-vxlan.sh probe
sudo ./scripts/lab-2az-vxlan.sh downПочему internal-порты: OVS internal-порт — это реальный kernel-netdev, остающийся
прикреплённым к OVS-datapath даже после переноса в netns, так что два netns-«шасси»
соединяются через host-datapath. (ovs-sandbox / make sandbox
использует dummy datapath, где реальные пакеты через kernel-netdev'ы не идут, поэтому
eBPF/TC их не видят — нужен настоящий kernel datapath.)
При OVS-DPDK datapath работает в userspace, поэтому TC-путь может не видеть трафик:
-
OVS с
type=afxdpnetdev'ами — NIC остаётся kernel-netdev'ом, но OVS грузит XDP-программу, котораяXDP_REDIRECT'ит кадры в AF_XDP-сокет до TC-хука, так что TC-программа их не видит.--xdpвешает тот же двухуровневый фильтр и парсеры на XDP-хук (который срабатывает раньше), выдаёт те же observations, отвечает на пробы к локальной VM черезXDP_TX(переписывает на месте, чексуммы правит вручную — на XDP нет csum-helper'ов) и для всего остального возвращаетXDP_PASS, так что OVS всё равно получает кадр. Предпочитается native (driver) режим с откатом на generic (SKB).sudo bin/agent --xdp --iface eth0 --node-id hv-01
Оговорка: одна XDP-программа может владеть netdev'ом, если не используется
xdp-dispatcher(libxdp); на NIC, где уже висит XDP-программа OVS, нужен chaining через libxdp. XDP здесь только ingress: добавьте--xdp-tc-egress, чтобы рядом повесить TC-egress-программу и видеть исходящий трафик, отправляемый стеком. Компаньон включается автоматически (об этом пишется в лог), когда какой-либо адрес отвечается на egress, которого XDP не видит: в watch-режимах (--watch,--watch-vxlan) это каждый адрес наблюдаемой VM (отвечается на egress её tap'а), а в статическом режиме (--iface) — каждый--local-ip, чьё направление резолвится в egress:--local-ip-dir egressлибоautoдля адреса, не локального для netns агента (VM за интерфейсом). Без TC-egress-программы проба дошла бы до VM и её стек ответил бы несанированным эхом. Кадры, попадающие в NIC черезXDP_REDIRECTс другого интерфейса, минуют и стек, и TC — их не увидит и этот режим. ОтветXDP_TXуходит ниже стека, поэтому на отвечающем NIC он не наблюдается на egress. На veth-паре nativeXDP_TXдоставляется, только если на peer'е тоже стоит XDP-программа (свойство veth-драйвера;TRACEFLOW_FORCE_XDP_GENERIC=1включает generic-режим, которому это не нужно). -
OVS-DPDK с DPDK PMD (vfio-pci) — NIC полностью во владении userspace DPDK-драйвера, поэтому kernel-netdev'а нет вообще, и ни TC, ни XDP не прицепить. Наблюдение там требует OVS mirror-порта / нативного OVS-трейса; это вне рамок.
-
vhost-user порты VM — VM подключается через unix-сокет + shared memory, без netdev, поэтому eBPF в принципе не может наблюдать на порту.
- HMAC проверяется responder'ом в ядре (по SHA-256-midstate'ам, только для проб,
на которые будет ответ); observations остаются advisory. При заданном ключе окно
свежести (
--hmac-window, по умолчанию 30 с) ограничивает replay перехваченных подписанных проб; replay в пределах окна возможен by design (см. Аутентификация) и требует синхронизации часов отправителя и агента в пределах окна. - Свежесть часов replay-окна наблюдаема. Окно сравнивает timestamp пробы с
clock_map, который агент обновляет раз в секунду. Если обновления начинают стабильно падать, часы замерзают, и спустя один--hmac-windowкаждая подписанная проба отбрасывается как stale — по одномуtraceflow_stale_probes_totalэтот сбой не отличить от настоящего replay. Следите заtraceflow_clock_age_seconds(в норме ~1 с; алерт — при приближении к окну) иtraceflow_clock_update_failures_total. - Option-TLV Geneve пропускаются, но не декодируются. Underlay-парсер наблюдает
Geneve (UDP 6081) наравне с VXLAN: VNI, inner-кадр — всё, что фиксирует observation.
Не извлекается только содержимое option-TLV — в частности logical ingress/egress
port keys OVN (class
0x0102): в 88-байтовой записи observation для них нет поля, так что логический путь определяется только датапасом (VNI →logical_switch). Geneve-пакеты OAM (O-бит) и не-Ethernet payload (protocol type ≠0x6558) не наблюдаются.--watch-vxlanпо-прежнему следит только за VXLAN-netdev'ами; на Geneve-устройствеcollect_metadata(OVS'ныйgenev_sys_6081) обогащение через tunnel-key работает, но тегирует observation какVXLAN— helper не сообщает тип туннеля. - Shutdown ограничен по времени. По SIGINT/SIGTERM агент отцепляется и дренирует очередь коллектора не дольше 5 с, затем сбрасывает остаток (с логом и количеством) — недоступный коллектор не может затянуть остановку. Второй сигнал завершает немедленно.
- Ответы сохраняют размер и IP identification пробы, сбрасывают TTL/hop limit в 255 и
для TCP не имеют состояния (таблицы потоков нет; наш seq — это ack клиента). Проба,
отвеченная на TC-egress-хуке, заходит обратно через ingress tap'а, где наблюдается
уже как ответ (
need_response=0,FORWARDED). Csum-helper'ы ядра на TC учитываютCHECKSUM_PARTIAL-skb; на XDP кадр берётся как есть на проводе. - Ядро: программе нужны ring buffer (≥ 5.8), ограниченные циклы (≥ 5.3) и глобальные
BPF-функции с указателями в аргументах (≥ 5.12); TCX-attach — ≥ 6.6, ниже работает
откат на clsact/cls_bpf. Диапазон verifier'а реально проверен: полный suite
BPF_PROG_TEST_RUN+ verifier-статистика проходят без изменений на 5.15 и 6.1 LTS (qemu-microVM черезscripts/vm-verify.sh, зеркально — CI-джобаvm-verify) и на 6.8 (~549k/562k из бюджета в 1M инструкций на 5.15/6.1, ~321k/328k на 6.8 — старые verifier'ы хуже прунят арифметику answerability, отсюда разрыв). Ядра в окне 5.12–5.14 удовлетворяют списку фич, но CI их не прогоняет. - Ядра PREEMPT_RT не поддерживаются. Per-CPU scratch-map исходит из того, что
BPF-программы не вкладываются на одном CPU — это верно на не-RT ядрах (TC работает
под
rcu_read_lock_bh, XDP — в softirq), но на PREEMPT_RT BPF-программы вытесняемы подmigrate_disable, так что два потока на одном CPU могут одновременно оказаться внутри обработчика и испортить друг другу scratch (observation, offsets, рабочую область HMAC) — по той же причине ядро 6.10 убралоbpf_redirect_infoиз per-CPU-хранилища. Не запускайте агента на RT-ядре (CONFIG_PREEMPT_RT). - Latency между хостами требует точной синхронизации времени (PTP). Значение
клампится в ≥ 0 и помечается
clock_skew, когда часы хостов расходятся. - IPv6 extension-заголовки обходятся ограниченным циклом; не-первый фрагмент не несёт транспортного заголовка и не маркируется.
- Портируемость: TCX (≥ 6.6) с clsact/cls_bpf netlink-fallback; программа трогает
только стабильный UAPI (
__sk_buff/xdp_md+ байты пакета), поэтому CO-RE не нужен. Заметка про апгрейд (только clsact-fallback): старые агенты ставили cls_bpf-фильтры на tc pref 1; текущие используют отличительный pref0x5446и чужие фильтры не трогают. Осиротевший pref-1 фильтр traceflow — старый агент, убитый без teardown — распознаётся по имениtraceflow_*и удаляется автоматически при attach (с записью в лог), так что на tap'е никогда не работают два responder'а. - Встроенный коллектор — это in-memory референсный приёмник; для продакшен-хранения подставьте свой ingester или используйте OTLP.