# Потоки данных: балансировка публичного трафика **Дата:** 2026-08-18 (обновлено после шага 5 — динамический MAC/ARP) **Область:** путь пакета от клиента из `192.168.5.0/24` на публичный листенер `192.168.5.21:80` и обратно. **Смежный документ:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md) — балансировка внутри приватного сегмента. **Источник:** `lb/pipeline.sh`, `lb/topology.env`, [STEP5_SUMMARY.md](STEP5_SUMMARY.md). Публичная балансировка целиком **маршрутизируемая**: клиент и бэкенды лежат в разных сегментах, узел выступает L3-хопом в обе стороны, TTL уменьшается на единицу на каждом направлении. Сетевой стек ядра контейнера в передаче трафика не участвует — все решения принимает OpenFlow. ## 1. Участники ```mermaid flowchart LR CL["Клиент
192.168.5.13"] subgraph BR["br-lb — OpenFlow datapath"] direction TB PUB["pub0 · ofport 1
macvlan @ enp3s0
02:42:c0:a8:05:14
адрес узла 192.168.5.20
VIP 192.168.5.21:80"] B1["be1 · 10"] B3["be3 · 11"] B2["be2 · 20"] B4["be4 · 21"] end BE1["be1 10.20.0.2:8080"] BE3["be3 10.20.0.3:8080"] BE2["be2 10.30.0.2:8080"] BE4["be4 10.30.0.3:8080"] CL <-->|"L2 клиентской сети"| PUB B1 <--> BE1 B3 <--> BE3 B2 <--> BE2 B4 <--> BE4 ``` Адреса `192.168.5.20` и `192.168.5.21` существуют только в правилах OpenFlow — на интерфейсе они не настроены, за них отвечает ARP-респондер MAC-адресом порта `pub0`. ## 2. Конвейер таблиц публичного пути ```mermaid flowchart TD IN(["Кадр с pub0"]) --> T0["0 сегмент по in_port
reg0 = 1"] T0 --> T1{"1 классификация"} T1 -->|"arp"| T5["5 ARP-респондер
за 192.168.5.20 и .21"] T1 -->|"icmp echo на
адрес узла или VIP"| T6["6 ICMP-респондер"] T1 -->|"ip на VIP, адрес узла
или приватные сети
+ learn adjacency"| T10 T1 -->|"прочий IP"| DROP1(["drop"]) T10{"10 листенер
tcp VIP:80"} T10 -->|"multipath(symmetric_l4)
→ reg1 = слот"| T11["11 таблица слотов
reg1 → reg2
ведёт hcd"] T10 -->|"IP на VIP без
листенера"| DROPV(["drop"]) T10 -->|"прочий IP: клиент
обратился к бэкенду
напрямую"| T20 T11 -->|"пул пуст"| DROPF(["drop — fail-close"]) T11 --> T12["12 DNAT
ct(commit, nat(dst=member:8080))"] T12 --> T20 T20{"20 маршрутизация
LPM по приоритету"} T20 -->|"dec_ttl,
mod_dl_src = MAC сегмента"| T21["21 adjacency
MAC члена + его порт"] T20 -->|"нет маршрута"| DROP20(["drop"]) T21 --> OUT(["output: порт члена пула"]) classDef d fill:#7f1d1d,stroke:#ef4444,color:#fff class DROP1,DROPV,DROPF,DROP20 d ``` Обратный путь идёт по другим таблицам — см. раздел 4. ## 3. Прямой путь: клиент → VIP → бэкенд ```mermaid sequenceDiagram autonumber participant C as Клиент 192.168.5.13 participant P as pub0 · 1 participant OF as Таблицы OpenFlow participant B as be1 10.20.0.2 C->>P: ARP who-has 192.168.5.21 P->>OF: t1 → t5 ARP-респондер OF-->>C: is-at 02:42:c0:a8:05:14 C->>P: SYN 192.168.5.13 → 192.168.5.21:80, TTL 64 Note over OF: t0: reg0=1 Note over OF: t1: адресован стенду →
learn: adjacency клиента в t21 Note over OF: t10: multipath(symmetric_l4, basis=pool_id)
→ reg1 = номер слота Note over OF: t11: слот → reg2 = член пула Note over OF: t12: ct(commit, nat(dst=10.20.0.2:8080))
src не меняется Note over OF: t20: LPM /24 10.20.0.0 → dec_ttl 63,
src MAC = MAC сегмента Note over OF: t21: dst MAC = MAC be1, output порт be1 OF->>B: пакет Note over B: client=192.168.5.13 — исходный адрес,
served=10.20.0.2:8080 — после DNAT ``` Ключевое: **SNAT не выполняется**. Транслируется только адрес и порт назначения, поэтому бэкенд видит реальный адрес клиента. ## 4. Обратный путь: бэкенд → клиент Ответ обязан пройти через узел — иначе клиент получит пакет от `10.20.0.2:8080` вместо `192.168.5.21:80` и разорвёт соединение. Это обеспечивается маршрутом на бэкенде (профиль A, см. раздел 5). ```mermaid flowchart LR BE["be1 10.20.0.2:8080"] -->|"src=10.20.0.2 dst=192.168.5.13
через шлюз сегмента 10.20.0.1"| A["порт be1 · 10"] A --> B["t1: dl_dst = MAC шлюза
сегмента → t15"] B --> C["t15: ct(zone=1, nat)
un-DNAT: src → 192.168.5.21:80"] C --> D["t16 → t20: LPM /24 192.168.5.0
dec_ttl, src MAC = pub0"] D --> E["t21 prio 90: MAC клиента
из действия learn"] E --> CL["Клиент видит ответ
от 192.168.5.21:80"] ``` Трансляция снимается conntrack'ом: `ct(commit, nat)` на прямом пути закрепил привязку за соединением, `ct(nat)` без commit на обратном её применяет в обратную сторону. Пакеты, для которых записи нет (транзит между сегментами, собственные обращения бэкендов), проходят без изменений. ## 5. Условие возврата: профиль A на бэкенде ```mermaid flowchart TD A["Таблица маршрутизации бэкенда"] --> B["192.168.5.0/24 via 10.20.0.1
клиентский префикс — через узел"] A --> C["10.30.0.0/24 via 10.20.0.1
соседний сегмент — через узел"] A --> D["default via 10.20.0.254
шлюз Docker — мимо узла"] B --> E["Ответы на балансируемые сессии
возвращаются на узел и
проходят обратную трансляцию"] D --> F["Собственный egress бэкенда
(обновления, DNS, телеметрия)
узел не маршрутизирует"] ``` Проверить: `docker compose exec be1 ip route`. Признак, что профиль A цел, — кадр к внешнему адресу адресован MAC-у шлюза Docker, а не MAC-у маршрутизатора сегмента (`02:42:0a:14:00:01`). ## 6. Таблица решений публичного пути | Таблица | Матч | Действие | Комментарий | |---|---|---|---| | 0 | `in_port=1` | `reg0 = 1` | публичный сегмент; коммутации в нём нет | | 1 | `arp` | → t5 | штатного ARP-стека у узла нет | | 1 | `ip`, dst ∈ {VIP, `.20`, `10.20/24`, `10.30/24`} | `learn` + → t10 | обучение сужено, чтобы в t21 не попадал широковещательный шум LAN | | 1 | прочий IP с `pub0` | drop | | | 5 | `arp_tpa` = `.20` или `.21` | ответ `IN_PORT` | отвечает MAC-ом `pub0` | | 10 | `tcp`, `nw_dst=VIP`, `tp_dst=80` | `multipath` → t11 | симметричный L4-хэш, basis = `pool_id` | | 10 | `ip`, `nw_dst=VIP` (нет листенера) | drop | видно по счётчику | | 10 | прочий IP | → t20 | прямое обращение клиента к бэкенду | | 11 | `reg1` = слот | `reg2` = член | 1024 правила, заливает `hcd` | | 11 | prio 0 | drop | fail-close: живых членов нет | | 12 | `reg2` = член | `ct(commit, nat(dst=member:8080))` → t20 | без SNAT | | 15 | `tcp` с порта сегмента, адресован шлюзу | `ct(nat)` → t16 | обратная трансляция | | 20 | `/24` префиксы | `dec_ttl` + `mod_dl_src` | LPM через приоритет правила | | 21 | `nw_dst` = член пула | prio 100, `mod_dl_dst` + `output` | MAC резолвит `hcd` через ARP ядра (шаг 5), не `topology.env` | | 21 | `nw_dst` = клиент | prio 90, из `learn` | `hard_timeout` 300 с | ## 7. Где поток обрывается | Точка | Причина | Как увидеть | |---|---|---| | t1 prio 100 | IP из публичного сегмента адресован не стенду | `lbctl flows 1` | | t10 prio 150 | обращение на VIP, для которого нет листенера | `lbctl flows 10` | | t11 prio 0 | пул без живых членов — fail-close | `make slots` | | t20 prio 0 | назначение без маршрута (дефолта у узла нет) | `lbctl flows 20 \| grep priority=0` | | t21 prio 0 | MAC клиента неизвестен — запись `learn` истекла | `lbctl flows 21` | ## 8. Осмотр ```bash make trace SRC=192.168.5.13 SPORT=41234 # путь сессии по таблицам make neigh # MAC членов, резолвленный hcd через ARP make slots # раскладка и per-member счётчики make conns # записи ct docker compose exec lb-router lbctl flows 21 ``` ## 9. Ограничения публичного пути - **Обратный трафик обязан идти через узел.** Без маршрута на клиентский префикс (профиль A) ответ уйдёт мимо и соединение не соберётся. - **Датапас stateful**: обратную трансляцию делает `ct`. Прямой и обратный трафик сессии обязаны проходить через один узел — Active/Active ECMP в такой схеме не работает. - **ICMP-ошибки не генерируются**: ни `TTL exceeded`, ни `fragmentation needed`. PMTUD через стенд не работает, `traceroute` узел не покажет. - **Фрагменты IP не обрабатываются** — правила матчат L4-заголовок. - **Только TCP и только IPv4**; UDP- и L3-листенеров нет. - **MAC клиента живёт 300 секунд** — после простоя первая же новая сессия создаёт запись заново.