# Потоки данных подсистемы L3-маршрутизации **Дата:** 2026-08-17 **Область:** транзитная маршрутизация узла `hpnn-lb` — всё, что проходит через таблицы 20 (LPM) и 21 (adjacency). Балансировка (таблицы 10–12) показана только как точка ответвления и подробно описана в [STEP2_SUMMARY.md](STEP2_SUMMARY.md). **Источник:** `lb/pipeline.sh`, `lb/topology.env` (состояние на 4 члена пула). Ключевое свойство: сетевой стек ядра контейнера в транзите не участвует. В ядре живут только два адреса — `10.20.0.253` и `10.30.0.253` на internal-портах для health-проб. Все решения о пересылке принимает OpenFlow. ## 1. Топология портов моста `br-lb` ```mermaid flowchart LR CL["Клиент
192.168.5.0/24"] 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 .21"] P2["p2 — ofport 2
02:42:0a:14:00:01
шлюз 10.20.0.1"] P3["p3 — ofport 3
02:42:0a:1e:00:01
шлюз 10.30.0.1"] HC2["hcif-p2 — ofport 4
10.20.0.253 в ядре"] HC3["hcif-p3 — ofport 5
10.30.0.253 в ядре"] end BE1["be1 10.20.0.2"] BE3["be3 10.20.0.3"] BE2["be2 10.30.0.2"] BE4["be4 10.30.0.3"] HCD["hcd — демон
health-check"] CL <--> PUB P2 <--> BE1 P2 <--> BE3 P3 <--> BE2 P3 <--> BE4 HCD <--> HC2 HCD <--> HC3 ``` ## 2. Конвейер таблиц: путь маршрутизируемого пакета Красным помечены точки отбрасывания, пунктиром — ответвление в балансировку. ```mermaid flowchart TD IN(["Пакет на порту моста"]) --> T0 T0{"Таблица 0
классификация по in_port
и типу трафика"} T0 -->|"arp"| T5["5 ARP-респондер
для .20/.21/10.20.0.1/
10.30.0.1/.253"] T0 -->|"icmp echo
на адреса узла"| T6["6 ICMP echo-респондер"] T0 -->|"in_port=1 (pub)
+ dst ∈ {VIP, узел,
10.20/24, 10.30/24}"| LEARN["learn → запись
adjacency клиента
в таблицу 21
(priority 90, 300 c)"] T0 -->|"in_port=2,3 (priv)"| T15["15 обратный путь"] T0 -->|"in_port=4,5 (hcif)"| T20 T0 -->|"иначе"| DROP0(["drop"]) LEARN --> T10{"Таблица 10
листенеры"} T10 -.->|"tcp dst=VIP:80"| LB["11/12 слот → член пула,
DNAT ct(commit,nat)"] T10 -->|"прочий IP
(прямое обращение
к бэкенду)"| T20 T10 -->|"IP на VIP
без листенера"| DROPV(["drop"]) LB --> T20 T15 -->|"tcp"| CT["ct(zone=1, nat)
снятие DNAT,
если сессия известна"] --> T16["16 пост-ct"] --> T20 T15 -->|"прочий IP"| T20 T20{"Таблица 20 — LPM
приоритет = длина префикса"} T20 -->|"/32 10.20.0.253
/32 10.30.0.253
адрес узла, TTL не трогаем"| T21 T20 -->|"/24 10.20.0.0
dec_ttl, src MAC = p2"| T21 T20 -->|"/24 10.30.0.0
dec_ttl, src MAC = p3"| T21 T20 -->|"/24 192.168.5.0
dec_ttl, src MAC = pub0"| T21 T20 -->|"нет маршрута
default отсутствует"| DROP20(["drop"]) T21{"Таблица 21 — adjacency
next-hop MAC + порт"} T21 -->|"prio 100: be1..be4,
hcif — статически"| OUT(["output: 2 / 3 / 4 / 5"]) T21 -->|"prio 90: клиент,
из действия learn"| OUTC(["output: 1"]) T21 -->|"MAC неизвестен"| DROP21(["drop"]) classDef d fill:#7f1d1d,stroke:#ef4444,color:#fff class DROP0,DROPV,DROP20,DROP21 d ``` Почему нумерация именно такая: `goto_table` в OpenFlow разрешает переход только вперёд, поэтому обратный путь (15/16) обязан стоять **до** общей маршрутизации (20), а не после неё. ## 3. Четыре потока маршрутизации ### R1. Клиент → бэкенд напрямую (публичный → приватный сегмент) Балансировка не участвует: клиент обращается на `10.20.0.2:8080`, имея маршрут через `192.168.5.20`. ```mermaid sequenceDiagram autonumber participant C as Клиент 192.168.5.13 participant P as pub0 (1) participant OF as Таблицы OpenFlow participant Q as p2 (2) participant B as be1 10.20.0.2 C->>P: ARP who-has 192.168.5.20 P->>OF: таблица 5 — ARP-респондер OF-->>C: is-at 02:42:c0:a8:05:14 C->>P: IP src=192.168.5.13 dst=10.20.0.2, TTL=64 P->>OF: t0 learn (adj клиента в t21) → t10 Note over OF: t10: не VIP → goto 20 Note over OF: t20: LPM /24 10.20.0.0
dec_ttl → 63, src MAC = p2 Note over OF: t21: dst MAC = 02:42:0a:14:00:02, порт 2 OF->>Q: пересылка Q->>B: пакет, src остался клиентским B->>Q: ответ src=10.20.0.2 dst=192.168.5.13 Q->>OF: t0 → t15 → ct(nat) сессии нет → t16 → t20 Note over OF: t20: LPM /24 192.168.5.0
dec_ttl, src MAC = pub0 Note over OF: t21: prio 90 — запись, созданная learn OF->>P: порт 1 P->>C: ответ ``` ### R2. Транзит между приватными сегментами (be1 → be2) Единственный поток, где узел работает как чистый маршрутизатор в обе стороны. ```mermaid flowchart LR BE1["be1
10.20.0.2"] -->|"dst=10.30.0.2:8080
TTL 64"| A["p2 (2)"] A --> B["t0: in_port=2 → t15"] B --> C["t15: tcp → ct(zone=1,nat)
сессии в таблице нет —
пакет не меняется"] C --> D["t16 → t20"] D --> E["t20: /24 10.30.0.0
dec_ttl → 63
src MAC = 02:42:0a:1e:00:01"] E --> F["t21: dst MAC =
02:42:0a:1e:00:02
output:3"] F --> G["p3 (3)"] --> BE2["be2
10.30.0.2
client=10.20.0.2"] ``` Обратный путь `be2 → be1` симметричен: `t0 → t15 → t16 → t20 (/24 10.20.0.0) → t21 → p2`. Никакой трансляции ни в одном направлении. ### R3. Обратный трафик балансируемой сессии (бэкенд → клиент) Тот же путь, что и R1-ответ, но с обязательным проходом через `ct`: именно там `src` возвращается с адреса бэкенда на VIP. ```mermaid flowchart LR BE["be1 10.20.0.2:8080"] -->|"src=10.20.0.2
dst=192.168.5.13"| A["p2 (2)"] A --> B["t0: in_port=2 → t15"] B --> C["t15: ct(zone=1, nat)
un-DNAT:
src → 192.168.5.21:80"] C --> D["t16 → t20"] D --> E["t20: /24 192.168.5.0
dec_ttl, src MAC = pub0"] E --> F["t21 prio 90:
MAC клиента из learn
output:1"] F --> CL["Клиент
видит ответ от VIP"] ``` Почему обратный трафик вообще приходит на узел: у бэкендов профиль A — маршрут на клиентский префикс `192.168.5.0/24` направлен на шлюз сегмента (`10.20.0.1` / `10.30.0.1`), а `default` остаётся на шлюзе Docker. Собственный исходящий трафик бэкенда через балансировщик не идёт. ### R4. Health-пробы — единственный трафик, порождённый узлом ```mermaid sequenceDiagram autonumber participant H as hcd (ядро контейнера) participant I as hcif-p2 (4) participant OF as OpenFlow participant Q as p2 (2) participant B as be1 Note over H: Dialer.LocalAddr = 10.20.0.253 H->>I: SYN 10.20.0.253 → 10.20.0.2:8080 I->>OF: t0: in_port=4 → сразу t20
без learn и без ct Note over OF: t20: /24 10.20.0.0, t21: output:2 OF->>Q: → Q->>B: проба GET /healthz B->>Q: SYN-ACK 10.20.0.2 → 10.20.0.253 Q->>OF: t0 → t15 → t16 → t20 Note over OF: t20: /32 10.20.0.253 prio 32
TTL не уменьшается — пакет
адресован самому узлу Note over OF: t21: output:4 OF->>I: → I->>H: ответ, член up ``` Пробы идут с `.253`, а не с VIP, намеренно: иначе ответ бэкенда попал бы в логику обратной трансляции (`t15`) и до пробера не дошёл. ## 4. Таблица решений | Таблица | Матч | Действие | Комментарий | |---|---|---|---| | 0 | `in_port=1`, dst ∈ {VIP, .20, 10.20/24, 10.30/24} | `learn` → t10 | обучение сужено, чтобы в t21 не попадал broadcast-шум LAN | | 0 | `in_port=1`, прочий IP | drop | | | 0 | `in_port=2,3` | → t15 | обратный путь и транзит | | 0 | `in_port=4,5` | → t20 | трафик стека узла, минуя ct и learn | | 10 | `ip`, не VIP | → t20 | точка входа маршрутизации из публичного сегмента | | 15 | `tcp` | `ct(zone=1, nat)` → t16 | un-DNAT для балансируемых сессий, no-op для транзита | | 20 | `/32` адреса `.253` | prio 32, → t21 | TTL не уменьшается | | 20 | `/24` три префикса | prio 24, `dec_ttl` + `mod_dl_src` | LPM через приоритет | | 20 | остальное | prio 0, drop | **дефолтного маршрута нет** | | 21 | `nw_dst` = be1..be4, hcif | prio 100, `mod_dl_dst` + `output` | статические MAC из `docker-compose.yml` | | 21 | `nw_dst` = клиент | prio 90, из `learn` | hard_timeout 300 с | ## 5. Где поток обрывается | Точка | Причина | Как увидеть | |---|---|---| | t0 prio 0 | не-IP и не-ARP: IPv6, DHCP, broadcast | `lbctl flows 0` | | t0 prio 100 (pub) | IP из публичного сегмента не стенду | `lbctl flows 0` | | t10 prio 150 | IP на VIP без листенера | `lbctl flows 10` | | t20 prio 0 | назначение без маршрута | `lbctl flows 20 \| grep priority=0` | | t21 prio 0 | next-hop MAC неизвестен (запись `learn` истекла) | `lbctl flows 21` | ## 6. Чего в этих потоках нет - **ICMP-ошибок узел не генерирует** — ни `TTL exceeded`, ни `fragmentation needed`. Пакет с TTL=1 просто исчезает, `traceroute` через стенд узел не покажет, PMTUD не работает. - **Фрагменты IP не обрабатываются**: правила матчат L4-заголовок, которого во втором и последующих фрагментах нет. - **ARP-резолвера next-hop нет.** MAC бэкендов статичны, MAC клиентов узнаются действием `learn` — то есть только для тех, кто первым обратился к стенду. - **Дефолтного маршрута нет** по построению: узел маршрутизирует только три известных префикса. - **Только IPv4.**