🇬🇧 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 VXLAN
- Что фиксирует каждая 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-local? verify HMAC
D-->>C: response (AF_PACKET, re-signed)
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 / auto --> V{outer UDP:4789<br/>+ VXLAN header?}
V -- yes --> IN[parse inner frame<br/>extract 24-bit VNI]
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/>dst is a local VM · not encapsulated}
G -- yes --> DROP([drop original · agent answers · TC_ACT_SHOT])
G -- "no" --> PASS3([pass · forward to the VM])
Парсеры обрабатывают IPv4 и IPv6 (ограниченный обход extension-заголовков), теги VLAN
802.1Q / 802.1AD (включая аппаратный offload через skb->vlan_tci) и VXLAN поверх
IPv4- или IPv6-underlay. Записи попадают в ring buffer на 1 MiB; per-CPU stats-map
считает наблюдённые пакеты и дропы из-за переполнения ring buffer.
Две точки attach отражают реальный деплой OVN/OVS: внутри AZ агент сидит на tap VM, между AZ — на underlay / VXLAN-netdev.
intra-AZ (tap model) inter-AZ (VXLAN)
agent on the VM tap agent on the underlay / vxlan netdev
VM ──tap──▶ [eBPF] ──▶ OVS ──▶ ... ... ──▶ [eBPF] ──▶ VXLAN ──▶ other AZ
inner frame, DSCP+magic visible outer Eth/IP/UDP/VXLAN + inner
iface_type = REGULAR iface_type = VXLAN, 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. - 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. auto(по умолчанию) — на каждый пакет сначала попытка VXLAN, откат на regular. В observation всегда указывается обнаруженныйiface_type, никогда неauto.
Перехват привязан к получателю. По умолчанию агент дропает оригинальную пробу, когда
она достигает VM назначения, и отвечает за неё сам — чтобы стек VM тоже не ответил. Ядро
дропает её, только если само будет отвечать: выставлен need_response, внутренний
dst_ip — локальный адрес VM (поэтому транзит и источник никогда не дропают), кадр не
VXLAN-инкапсулирован, и этот агент отвечает. Проба с --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 VNI (0, когда не VXLAN) |
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) |
az |
агент | зона доступности (--az) |
src_ip, dst_ip |
пакет | внутренние адреса |
src_port, dst_port |
пакет | для TCP/UDP |
tcp_flags |
пакет | напр. SYN|ACK (только TCP) |
iface_type |
eBPF | REGULAR или VXLAN |
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, 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 + 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, responder, watchers, metrics, emitters
client/ marked-probe sender + 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) и набор локальных IP VM; - прикрепляет observation-программу;
- читает observations из ring buffer и печатает JSON (или отправляет дальше);
- отвечает только за VM назначения.
Attach. По умолчанию — TC-хук (ingress + egress): TCX на ядре ≥ 6.6, с
автоматическим откатом на классический clsact + cls_bpf через netlink (примерно до
4.5). С --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 дропает его), чтобы стек VM
не ответил тоже; проба с --deliver доставляется в VM, и она отвечает сама. Ответы
уходят через AF_PACKET (L2 raw socket), минуя маршрутизацию ядра; разворот адресов
переиспользует MAC/IP самой VM — ровно то, что ожидает OVN port security. TCP
обслуживает маленький, seq-корректный userspace-responder (--tcp-respond=false
позволяет ответить реальному стеку VM). AF_PACKET-responder снимает копию на ingress до
TC-хука, поэтому он видит и отвечает на пробу, даже если оригинал дропнут.
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 отвечает автоматически.--watch-vxlan— следит за per-tenant VXLAN-netdev'ами через netlink link events (создаются/удаляются OVN или любым контроллером) и тегирует observations VNI устройства.
Ключевые флаги.
| Флаг | По умолчанию | Значение |
|---|---|---|
--iface NAME |
— | attach к одному интерфейсу (статический режим) |
--iface-type |
auto |
auto | regular | vxlan |
--node-id |
hostname | идентификатор в каждой observation |
--respond |
true |
отвечать на need_response=1 для локальных IP VM |
--tcp-respond |
true |
отвечать на TCP в userspace; false отдаёт стеку VM |
--local-ip |
— | локальные IP VM через запятую (статический режим) |
--watch / --watch-vxlan |
false |
динамический attach (см. выше) |
--hmac-key |
— | общий секрет; responder отклоняет невалидный/отсутствующий HMAC |
--az |
— | зона доступности, добавляемая в observations |
--metrics-addr |
— | host:port для Prometheus /metrics + /healthz |
--collector-url |
— | POST каждой observation как JSON на этот URL |
--otlp-endpoint |
— | экспорт каждой observation как OTLP-span |
--xdp |
false |
attach на XDP вместо TC |
--xdp-tc-egress |
false |
вместе с --xdp: дополнительно TC-egress-программа (XDP видит только ingress) |
Отправитель проб. Каждая проба несёт 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 |
tcp |
полный TCP-диалог: handshake + data + FIN |
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 и ответил её собственный стек. Ответы ловятся
AF_PACKET-снифером и сопоставляются по run id.
# ICMP echo через overlay, ждём ответ
sudo bin/client icmp --dst 10.10.0.2 --response
# Полный TCP-диалог на порт 80, с подписью
sudo bin/client tcp --dst 10.10.0.2 --dport 80 --hmac-key s3cret --response
# Только 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 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, вытеснение
по LRU). Хранилище — in-memory: коллектор является референсным приёмником; для продакшена
направьте --collector-url на свой ingester или используйте OTLP.
Трассируемый путь ложится 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, insecure по умолчанию).
DSCP=62 + magic тривиально подделывается. --hmac-key <секрет> включает HMAC-SHA256
поверх 30-байтового ядра meta. Responder проверяет его и никогда не отвечает без
валидного HMAC (учитывается как traceflow_auth_failures_total), что закрывает
амплификацию и неаутентифицированное зондирование. Эхо-ответы переподписываются, так что
обратный путь остаётся валиден.
eBPF не может дёшево вычислить HMAC на горячем пути, поэтому observations остаются advisory (любой, кто знает DSCP и magic, может добиться того, чтобы пакет наблюдался); enforcement живёт на responder'е — единственном компоненте, который порождает трафик в ответ.
sudo apt install -y clang llvm libbpf-dev linux-libc-dev make golang
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_NET_RAW.
Таргеты 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 прогоняет unit-тесты и собирает все три образа (без пуша), так что
релизный путь проверяется ещё до того, как поставлен тег.
Три образа соответствуют трём ролям. Скачайте их один раз (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 на ядрах < 5.8 без CAP_BPF):
docker run -d --name traceflow-agent --network host \
--cap-add BPF --cap-add NET_ADMIN --cap-add NET_RAW --cap-add SYS_RESOURCE \
-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) и флаги работают ровно как в
разделе Клиент — например, подписанный полный 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 --responseКоллектор — обычный 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, ответ dst-VM |
test_vxlan.sh |
парсинг VXLAN underlay (VNI, структурный intercept-guard) |
test_ipv6_vlan.sh |
наблюдение ICMPv6 и 802.1Q VLAN |
test_watch_vxlan.sh |
netlink VXLAN-watcher: attach существующего, динамика create/delete, обогащение device-VNI |
test_hmac.sh |
подписанная проба отвечается, неподписанная отклоняется (по метрикам) |
test_clsact.sh |
clsact/cls_bpf fallback (TRACEFLOW_FORCE_CLSACT=1) |
test_xdp.sh |
путь attach через XDP (--xdp): программа на netdev, observation на ingress |
test_xdp_tc_egress.sh |
--xdp --xdp-tc-egress: ingress через XDP + egress через TC-компаньона |
test_vxlan_2az.sh |
два шасси / две AZ, соединённые VXLAN (нужен OVS) |
Два шасси в стиле 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 и возвращает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-программу и видеть исходящий трафик, отправляемый стеком. Кадры, попадающие в NIC черезXDP_REDIRECTс другого интерфейса, минуют и стек, и TC — их не увидит и этот режим. -
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'е; observations остаются advisory (eBPF не считает HMAC на горячем пути).
- Latency между хостами требует точной синхронизации времени (PTP). Значение
клампится в ≥ 0 и помечается
clock_skew, когда часы хостов расходятся. - IPv6 extension-заголовки обходятся ограниченным циклом; не-первый фрагмент не несёт транспортного заголовка и не маркируется.
- Портируемость: TCX (≥ 6.6) с clsact/cls_bpf netlink-fallback; программа трогает
только стабильный UAPI (
__sk_buff+ байты пакета), поэтому CO-RE не нужен. - Встроенный коллектор — это in-memory референсный приёмник; для продакшен-хранения подставьте свой ingester или используйте OTLP.