Skip to content

Repository files navigation

Traceflow

🇬🇧 English documentation: README.en.md

Диагностика сетевого пути для overlay-сетей (OVN) на базе eBPF.

Traceflow отвечает на вопрос, недоступный обычным инструментам: через какие именно хосты, туннели и VNI прошёл этот пакет? Он отправляет специально маркированный пробный пакет, и каждый хоп по пути фиксирует, что видел его. Это как traceroute, но вместо одних лишь IP-маршрутизаторов он видит data plane гипервизора — tap-порты, OVS-мосты, VXLAN-туннели и VNI, в котором ехал кадр, — в том числе между зонами доступности.

Метка невидима для обычного трафика и дёшево отбрасывается: eBPF-программа на каждом интерфейсе отсеивает 99.99% пакетов одной инструкцией и внимательно смотрит только на те, что несут метку.


Оглавление


Как это работает

Три взаимодействующих части: клиент внедряет пробу, агент на каждом гипервизоре наблюдает её (а на хосте назначения — отвечает), а коллектор группирует записи с каждого хопа по 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
Loading

Метка: двухуровневая фильтрация

Проба распознаётся в два этапа, начиная с самого дешёвого.

Уровень Проверка Зачем
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 (см. Аутентификация).


Управляющий блок (traceflow_meta)

Сразу за транспортным заголовком (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

Что делает eBPF-программа

Один и тот же двухуровневый фильтр и парсеры работают либо на 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])
Loading

Парсеры обрабатывают 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 того же интерфейса; на XDPXDP_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'а.


Модель наблюдения — tap vs туннель

Две точки 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 type 0x6558 (внутренний Ethernet-кадр — то, что шлют OVN/OVS) и пакеты без O-бита (OAM/control). Сами option-TLV пропускаются, но не декодируются: logical ingress/egress port keys OVN (class 0x0102) не извлекаются — в 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

Каждый маркированный пакет становится одной 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)

Агент

По одному агенту на гипервизор. При старте он:

  1. грузит eBPF-программу через cilium/ebpf (pure Go, без CGO);
  2. пишет config map (iface_type, respond, tcp_respond, hmac), карту локальных VM ((ifindex, адрес) → направление ответа) и, при --hmac-key, SHA-256-midstate'ы, по которым ядро проверяет и переподписывает (сам ключ агента не покидает);
  3. прикрепляет программу;
  4. читает 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, поэтому агент освобождает мёртвую привязку по netlink RTM_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 100

Коллектор

collector принимает 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).


OTLP-экспорт (distributed tracing)

Трассируемый путь ложится 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.


Аутентификация (HMAC)

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.0

ci.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

Агент — гипервизор (compute-нода)

Каждый гипервизор запускает агента в режиме --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)

Сетевая нода несёт меж-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), чьи собственные тесты пересчитывают каждую чексумму с нуля.


Стенд 2 AZ через VXLAN

Два шасси в стиле 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 / AF_XDP

При OVS-DPDK datapath работает в userspace, поэтому TC-путь может не видеть трафик:

  • OVS с type=afxdp netdev'ами — 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-паре native XDP_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; текущие используют отличительный pref 0x5446 и чужие фильтры не трогают. Осиротевший pref-1 фильтр traceflow — старый агент, убитый без teardown — распознаётся по имени traceflow_* и удаляется автоматически при attach (с записью в лог), так что на tap'е никогда не работают два responder'а.
  • Встроенный коллектор — это in-memory референсный приёмник; для продакшен-хранения подставьте свой ingester или используйте OTLP.

About

eBPF path diagnostics for overlay networks (OVN/OVS)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages