Files
hpnn-proto/docs/STEP4_PUBLIC_STATELESS_ASSESSMENT.md
ayurishchevandClaude Opus 5 fa389f119b Шаг 5: динамическое изучение MAC/ARP для бэкендов
MAC каждого члена пула больше не статическая константа в topology.env,
а резолвится демоном hcd через обычный ARP ядра — в реальном
окружении MAC бэкенда заранее не известен (сервер ещё не подключён,
NIC может замениться), топология не описывается статически, в отличие
от стенда.

hcif-порты (единственные адреса узла в ядре) уже были настоящими
L3-интерфейсами в тех же сегментах, что и бэкенды — единственное, что
мешало обычному ARP, это permanent-записи ip neigh в entrypoint.sh.
Убрав их и добавив hc/neigh.go (читает ip -json neigh show, точечно
заливает бандл в таблицу 21 на том же тикере, что и health-пробы),
получили резолвер без нового OpenFlow-контроллера.

Таблица 21 стала единственным источником MAC для обоих путей —
маршрутизируемого и коммутируемого (шаг 3): таблица 12 больше не
дублирует MAC инлайново, ct(commit) ведёт сразу в таблицу 21.

Исправлен попутно найденный баг: после apply.sh (replace-flows)
таблица 21 не восстанавливалась, поскольку syncNeighbors сравнивал
MAC с памятью демона, а не с датапасом. Добавлен force-режим по
аналогии с Agent.apply(), плюс регрессионная проверка в verify.sh.

Проверено на живом стенде: MAC всех четырёх членов резолвлен и
совпадает с реальными интерфейсами; смена MAC "железа" обнаружена и
применена без вмешательства за счёт штатного старения ARP ядра.
Регрессия: 94 из 94 проверок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:33:48 +03:00

220 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Шаг 4 — оценка: публичная балансировка без conntrack
**Дата:** 2026-08-18
**Статус:** оценка осуществимости (не план внедрения и не реализация)
**Область:** только публичный листенер `192.168.5.21:80`
**Источник:** [PUBLIC_LB_DATAFLOW.md](PUBLIC_LB_DATAFLOW.md), `lb/pipeline.sh`
(таблицы 12, 15, 16)
**Смежные документы:** [PRIVATE_LB_DATAFLOW.md](PRIVATE_LB_DATAFLOW.md),
[MAGLEV_SLOT_TABLE.md](MAGLEV_SLOT_TABLE.md)
## Задача
Оценить переход публичного пути с текущей stateful-схемы (`ct(commit,nat)` на
прямом пути, `ct(nat)` на обратном) на схему без conntrack. Главное условие,
заданное на входе: **бэкенд обязан видеть реальный IP клиента** — адрес, с
которым клиент обратился на балансировщик, до всех преобразований.
Уточнение по ходу обсуждения: клиент **всегда** обращается на адрес
балансировщика (VIP), не на бэкенд напрямую. Он должен получить ответ, который
его TCP-стек и приложение примут как продолжение своего же соединения — то есть
`src` ответа обязан остаться `VIP:80`.
## Вывод
Технически осуществимо для нового трафика через публичный листенер, при двух
явных условиях периметра и одном принимаемом изменении поведения:
1. бэкенды не должны быть достижимы клиентами напрямую, в обход VIP (условие
периметра, не техническое ограничение датапаса);
2. правило обратной трансляции обязано быть сужено по адресу назначения,
иначе оно ломает трафик между бэкендами;
3. установленные сессии перестают быть защищены от смены состава пула — это
принимаемый компромисс, а не дефект.
## 1. Что меняется в датапасе
| Таблица | Сейчас | Предлагается |
|---|---|---|
| 12 (прямой DNAT) | `ct(commit, zone=1, nat(dst=member:8080))` | `mod_nw_dst:<member_ip>, mod_tp_dst:<member_port>` — без `ct` вообще |
| 15 (обратный путь) | одно правило `tcp actions=ct(table=16,zone=1,nat)` на весь трафик | по одному правилу на каждого члена: `tcp,nw_src=<member_ip>,tp_src=<member_port>,nw_dst=$PUB_NET actions=mod_nw_src:$VIP,mod_tp_src:$VIP_PORT,goto_table:16` |
| 16 | пост-`ct` — счётчики | не нужна для этого пути; сохраняется для транзита, где `ct` ещё используется |
`src` клиента в т.12 не трогается ни сейчас, ни в новой схеме — сохранение IP
клиента никогда не зависело от `ct`, поэтому по этой части поведение не
меняется вообще.
Обоснование замены в т.15 — §2.2 дизайна v1: DNAT переписывает адрес назначения
в фиксированный `member_ip:member_port`, а не в случайный порт, как SNAT.
Поэтому пара `(member_ip, member_port, proto)` однозначно определяет
`(VIP, listener_port)`, и обратную трансляцию можно сделать статическим
правилом без какого-либо состояния.
## 2. Условие периметра: прямой доступ к бэкенду должен быть закрыт
Таблица 1 отправляет в т.15 весь трафик с `dl_dst = MAC шлюза сегмента`, не
различая, был ли пакет частью балансируемой сессии. Сегодняшний стенд умеет и
поддерживает прямой доступ клиента к бэкенду в обход VIP (`table=10,
priority=100,ip actions=goto_table:20`, проверяется в `verify.sh` секция 5 и
`MANUAL_TEST_PLAN` блок C.2) — ответ на такое обращение и ответ на сессию через
VIP дают на выходе бэкенда идентичные заголовки (`src=member_ip:8080,
dst=client_ip:client_port`), различить их без состояния невозможно.
При заданном требовании — клиент всегда обращается на VIP — эта коллизия не
относится к реальному клиентскому трафику. Она становится значимой только если
прямой доступ к порту бэкенда остаётся разрешён одновременно со stateless-схемой.
Вывод: **предпосылка корректности схемы — бэкенды недостижимы клиентами
напрямую**, что и так является типовой практикой для облачных LB (изоляция
периметра, security groups), а не новым ограничением, придуманным ради этого
перехода.
## 3. Связность между бэкендами
Уточняющий вопрос по ходу оценки: не потеряют ли бэкенды способность общаться
друг с другом. Ответ разделяется по топологии.
**Бэкенды в разных сегментах.** Ответ `be1 → be2:8080` (транзит между
сегментами) идёт через ту же таблицу 15, что и обратный трафик клиентских
сессий, потому что оба используют один и тот же MAC шлюза сегмента как
классификатор. Правило `nw_dst=$PUB_NET` в предложенном un-DNAT решает это:
адресные пространства клиентов (`192.168.5.0/24`) и бэкендов (приватные
сегменты) не пересекаются, поэтому транзитный трафик просто не попадает под
условие правила и проходит немодифицированным. **Связность между сегментами
сохраняется**, если это условие включено в правило.
**Бэкенды в одном сегменте.** Это территория приватного пути (таблица 24,
шаг 3), не публичного, и в этой оценке не менялась — она по-прежнему на `ct`.
Если в будущем этот путь тоже переводить на stateless-схему, префиксный
критерий не сработает: в шаге 3 клиент и бэкенды намеренно живут в одной
подсети, поэтому ответ `be3 → be1` и ответ `be3 → реальный клиент cli2`
неотличимы по заголовкам. Решение для этого случая другое — не диапазон
адресов, а явный список исключений (для каждого члена сегмента: «если
назначение — адрес другого известного члена этого же сегмента, не
транслировать»), которые должны иметь приоритет выше общего правила un-DNAT.
Список членов конечен и известен заранее, в отличие от адресов произвольных
клиентов, поэтому это решаемо, но требует отдельного набора правил и не входит
в объём этой оценки (она ограничена публичным путём).
## 4. Изменение поведения: установленные сессии больше не защищены
Сейчас `ct(commit)` закрепляет привязку за соединением на весь срок его жизни
независимо от таблицы слотов — это подтверждено в `STEP2_SUMMARY.md`: сессия,
обслуживаемая выведенным из пула членом, не рвётся (`verify.sh` секция 13,
`MANUAL_TEST_PLAN` блок I).
Без `ct` трансляция каждого пакета определяется исключительно текущим
состоянием таблицы слотов. При выводе члена из пула следующий пакет уже
установленной к нему сессии уйдёт на другого члена — без контекста
TCP-соединения, и сессия оборвётся. Минимальное возмущение Maglev ограничивает
**долю** пострадавших сессий (переезжают только слоты выбывшего члена, у
остальных ничего не меняется), но не защищает сами эти сессии.
Это ровно тот компромисс, который был предсказан заранее в `STEP2_SUMMARY.md`:
«ценность таблицы слотов раскроется при переходе на stateless-датапас, где
единственной защитой от переезда сессий останется минимальное возмущение
раскладки». Переход на публичном пути — первое место, где это предсказание
становится проверяемым поведением, а не наблюдением.
Практическое следствие: тест «сессия переживает дренаж» (`verify.sh` секция 13)
для публичного пути перестаёт быть верным и требует пересмотра или явного
исключения из проверок при внедрении.
## 5. Узкое остаточное ограничение (из дизайна v1)
Если сам бэкенд когда-либо инициирует исходящее TCP-соединение с src-портом,
равным его собственному порту прослушивания (8080), и адресует его в
клиентскую сеть — такой пакет попадёт под правило un-DNAT и будет неверно
переписан. Маловероятный случай (обычно исходящие соединения используют
эфемерный порт источника), уже документирован в дизайне v1 (§3.2, «остаточное
ограничение», §15.4). Переход на stateless ничего не меняет в этом отношении —
ограничение существовало бы в любой чисто заголовочной схеме un-DNAT.
## 6. Что выигрывается
- `ct` и зависимость от `CT_ZONE` уходят из публичного пути целиком — оба
направления становятся чистым `match + rewrite`.
- Меньше recirculation в датапасе: на момент оценки в датапасе стенда
зафиксировано 6 потоков с `ct(...)`, каждый вызывает `recirc`; на публичном
пути они исчезают.
- Снимается ограничение, зафиксированное в `PUBLIC_LB_DATAFLOW.md` (раздел 9):
«прямой и обратный трафик сессии обязаны проходить через один узел». Это
предпосылка, без которой Active/Active ECMP между несколькими узлами LB не
имеет смысла обсуждать — устраняется она именно этим шагом.
## 7. Что не меняется
Маршрутизация (таблицы 20/21), таблица слотов (таблица 11), алгоритм Maglev,
детерминизм `multipath` — не затрагиваются вообще. Ограничения по ICMP-ошибкам
и фрагментации сохраняются в прежнем виде — они не были связаны с `ct` и
раньше.
## 8. Отдельный вопрос: доставка до физического сервера в EVPN-фабрике
Поднят по ходу обсуждения, зафиксирован здесь как связанный, но не
пересекающийся с темой `ct`.
Вопрос: если нода HPNN подключена к EVPN-фабрике, и MAC сетевого порта
физического сервера-бэкенда доходит до HPNN через фабрику — будет ли трафик
доставлен до сервера?
**Ответ: это не зависит от наличия или отсутствия `ct`.** Трансляция адреса
(таблицы 12/15) и доставка кадра до правильного MAC (таблица 21, adjacency) —
независимые механизмы. `ct` в доставке кадра не участвует ни сейчас, ни в
предложенной схеме.
Реальная зависимость — в существующей модели adjacency: MAC членов пула сейчас
**статически** прописаны в `topology.env` и заливаются в таблицу 21 константами
(`mod_dl_dst:$BE1_MAC`). Это уже задокументированное ограничение —
`STEP1_SUMMARY.md`: «MAC бэкендов прописаны статически. ARP-резолвер next-hop
отсутствует». Дизайн v1 предполагал агента, резолвящего MAC next-hop через
packet-in/packet-out по своей neigh-таблице (§4.3, §8.2), но это не
реализовано ни в стенде, ни где-либо ещё.
То, что MAC физического порта доходит до HPNN по EVPN-фабрике, — свойство самой
фабрики (leaf-коммутаторы разучивают MAC локально, публикуют по EVPN Type-2
остальным VTEP); HPNN в этом процессе не участвует, для него это просто кадры
на порту аплинка. Вопрос не в том, дойдёт ли MAC до HPNN (дойдёт), а в том,
знает ли пайплайн HPNN, какой MAC подставить в `mod_dl_dst`, когда решает
отправить пакет этому серверу. При текущей статической модели — знает только
то, что вручную прописано заранее. Если реальный MAC карты сервера не совпадает
с прописанным (замена карты, переключение LACP/бондинга на другой физический
линк, сервер ещё не был известен на момент конфигурации) — таблица 21 либо
отправит кадр не туда, либо, если совпадения нет вовсе, попадёт под
`priority=0 actions=drop`: трафик до сервера не дойдёт, несмотря на то что
фабрика прекрасно знает его настоящий MAC.
Это существующий пробел архитектуры, не имеющий отношения к переходу на схему
без conntrack — он не блокирует и не усложняет этот шаг. Требует отдельной
оценки: динамический ARP-резолвер next-hop (как и предполагал дизайн v1) либо
интеграция с тем, как EVPN-фабрика публикует MAC-привязки, чтобы control plane
HPNN получал их не вручную, а из фабрики.
> **Обновление (шаг 5, [STEP5_SUMMARY.md](STEP5_SUMMARY.md)):** первая часть
> этого пробела закрыта — `hcd` резолвит MAC членов пула динамически через
> обычный ARP ядра, статических `BE*_MAC` в таблице 21 больше нет. Это не
> решает вопрос EVPN-фабрики целиком: `hcd` резолвит MAC в пределах L2-домена,
> видимого узлу напрямую (те же hcif-порты, что и для health-проб), а не через
> интеграцию с EVPN Type-2. Если фабрика доставляет сегмент до узла как один
> L2-домен (что и предполагает текущая архитектура), тот же ARP-путь работает
> без изменений; отдельная интеграция с control plane EVPN остаётся вне рамок.
## 9. Итоговая сводка условий и рисков
| # | Пункт | Тип | Статус |
|---|---|---|---|
| 1 | Бэкенды недостижимы клиентами напрямую, только через VIP | предпосылка периметра | требует явного решения при внедрении |
| 2 | Правило un-DNAT сужено `nw_dst=$PUB_NET` | техническое условие | зафиксировано в предложенной схеме |
| 3 | Связность между сегментами (be↔be, разные сегменты) | проверено | сохраняется при условии (2) |
| 4 | Связность внутри сегмента (be↔be, один сегмент) | вне объёма этой оценки | не тронуто (путь на `ct`); при переносе на stateless требует отдельного набора исключений |
| 5 | Установленные сессии при смене состава пула | изменение поведения | принимаемый компромисс, требует пересмотра теста `verify.sh` §13 |
| 6 | Исходящий трафик бэкенда с src-портом = 8080 в клиентскую сеть | остаточное ограничение | узкое, уже документировано в дизайне v1 |
| 7 | Доставка до физического сервера по MAC (EVPN) | отдельный вопрос | не связан с `ct`; существующий пробел модели adjacency |
## 10. Что дальше
Это оценка осуществимости, не план внедрения. Следующий шаг, если решение
принято, — оформить отдельный план и итоги по конвенции репозитория
(`STEP4_IMPLEMENTATION_PLAN.md` / `STEP4_SUMMARY.md`), с явной фиксацией
решений по пунктам 1 и 5 таблицы выше как условий приёмки, и отдельной оценкой
пункта 7 при планировании физического окружения.