# Шаг 3 — итоги: листенер в приватном сегменте **Дата:** 2026-08-17 **Статус:** выполнено, проверено после холодного перезапуска (86 из 86 проверок) **План:** [STEP3_IMPLEMENTATION_PLAN.md](STEP3_IMPLEMENTATION_PLAN.md) **Предыдущий шаг:** [STEP2_SUMMARY.md](STEP2_SUMMARY.md) ## Что сделано Балансировщик получил листенеры в приватных сегментах — `10.20.0.100:80` и `10.30.0.100:80` — и научился обслуживать клиента, который стоит **в одном сегменте с бэкендами**. Исходный IP клиента сохраняется, порт транслируется 80 → 8080, в ОС виртуальных машин не настраивается ничего. - **`br-lb` стал коммутатором приватного сегмента.** Каждая ВМ подключена отдельным портом (`scripts/attach-segment.sh`), порты `p2`/`p3` остались аплинками сегмента к шлюзу Docker. Появились таблицы 0 (сегментация и обучение), 25 (FDB, broadcast и unknown flood) и 1 (классификация). - **Обратная трансляция коммутируемого трафика — таблица 24.** Ответ бэкенда клиенту-соседу уходит по L2 напрямую, но проходит через мост, где `ct(nat)` возвращает источник к `VIP:80`. - **Выдача на члена своего сегмента — без маршрутизации.** Таблица 12 при совпадении сегмента клиента и члена меняет только MAC назначения и отдаёт кадр в коммутацию: `dec_ttl` не выполняется, клиент и бэкенд остаются L2-соседями. - **Пул общий с публичным листенером** — та же таблица слотов, тот же `pool_id`, тот же дайджест `0a16713c5a9eb94d`. Демон `hcd` не изменён ни строкой. - **Переключатель `PRIV_LISTENERS`** в `topology.env`: пусто — приватных листенеров нет, `"p2"` — один сегмент, `"p2 p3"` — оба. - **Клиенты сегментов** `cli2` (`10.20.0.10`) и `cli3` (`10.30.0.10`) — ВМ без единого маршрута в таблице маршрутизации. ## Почему именно так Клиент и бэкенд — L2-соседи, поэтому ответ бэкенда уходит клиенту напрямую. Снять DNAT можно только там, где этот кадр виден. Остальные пути отпадали по условиям задачи: | Вариант | Почему отвергнут | |---|---| | SNAT | уничтожает сохранение IP клиента | | DSR, VIP на `lo` бэкенда | требует настройки ОС ВМ, не транслирует порт | | Изоляция портов сегмента | блокирует внутрисегментный трафик, неприменима к ВМ | | Дробление подсети маршрутами `/25` | вмешательство в ОС ВМ и в адресацию заказчика | Осталась единственная предпосылка, совпадающая с зоной влияния SDN: **порты ВМ — порты OVS**. В целевой среде это выполняется по построению; стенд приведён к той же модели. ## Результаты проверок | Проверка | Результат | |---|---| | ВМ сегментов подключены портами OVS | 6 портов: be1, be3, cli2, be2, be4, cli3 | | `ping 10.20.0.100` из `cli2` | отвечают ARP- и ICMP-респондеры OVS | | Балансировка из приватного клиента | 24 запроса, задействованы все четыре члена | | **IP клиента на бэкенде** | `client=10.20.0.10` во всех ответах | | **Трансляция порта** | `served=10.20.0.2:8080` при листенере на 80 | | Член в сегменте клиента | обслуживает запросы, ответ транслируется таблицей 24 | | **TTL ответа от VIP** | 64 для членов своего сегмента, 63 — для членов другого | | В ОС клиента нет маршрутов | `ip route` содержит только connected-запись | | Внутрисегментная связность | `cli2 → be1` напрямую, `be1 → be3`, шлюз Docker — всё работает | | Отказ члена под нагрузкой из `cli2` | слоты разошлись 341/342/341, 2 ошибки в окне обнаружения | | Профиль A | egress бэкенда адресован шлюзу Docker, не маршрутизатору сегмента | | `PRIV_LISTENERS="p2"` | листенер в priv3 выключен (счётчик drop растёт), связность цела | | Регрессия шагов 1–2 | публичный листенер, транзит, health-check, fail-close, дренаж — без изменений | ## Главное измерение: TTL как признак пути Ответы от `be1`/`be3` (сегмент клиента) приходят с **TTL 64**, от `be2`/`be4` (другой сегмент) — с **TTL 63**. Это прямое доказательство того, что для соседей по сегменту балансировщик не стал L3-хопом: он подменил MAC назначения, оттранслировал адрес и отдал кадр коммутацией. С точки зрения ВМ ничего не изменилось — она общается с соседом по подсети, как и раньше. Для членов из другого сегмента работает прежний маршрутизируемый путь с `dec_ttl`. Оба пути живут на одном пуле и одной таблице слотов, и клиент их не различает. ## Отклонения от плана 1. **`load:0->NXM_OF_IN_PORT[]` не понадобился.** План предполагал hairpin — выдачу в порт входа — и сброс `in_port`, чтобы OVS не подавил вывод. После перевода каждой ВМ на собственный порт hairpin исчез: клиент и бэкенд входят и выходят через разные порты. Риск снят самой архитектурой. 2. **Отдельная сеть для egress не потребовалась.** План предусматривал служебную docker-сеть под default-маршрут. Оказалось достаточно оставить `p2`/`p3` аплинками сегмента: бэкенд адресует кадр MAC-у шлюза Docker, мост его коммутирует, и профиль A сохраняется в прежнем виде. 3. **Появилась таблица 1.** В плане классификация оставалась в таблице 0. Но таблица 0 занята определением сегмента и обучением (два действия `learn` на каждый порт), поэтому классификация вынесена отдельно — иначе правила пришлось бы дублировать на каждый порт сегмента. 4. **`make` больше не требует `sudo`.** Под root его в системе может не быть; цели подготовки хоста теперь подставляют `sudo` только при запуске не от root. 5. **Проверки 7 и 9 в `verify.sh` переформулированы.** Обе слушали аплинк `p2`, через который health-пробы и egress больше не проходят: пробы идут коммутацией на порт ВМ, а признаком профиля A стал MAC назначения (шлюз Docker, а не маршрутизатор сегмента), а не отсутствие трафика на `p2`. ## Известные ограничения - **Предпосылка архитектуры:** порты ВМ должны быть портами OVS. Если между клиентом и бэкендом окажется коммутатор, который SDN не контролирует, кейс не решается ничем, кроме SNAT. - **Хрупкость стенда:** veth, созданные `attach-segment.sh`, исчезают при пересоздании контейнера — нужен `make attach`. В целевой среде порты создаёт гипервизор. - **Весь внутрисегментный трафик проходит через `br-lb`** — пропускная способность узла становится пропускной способностью сегмента. Замер k6 не выполнен: в системе нет клиента k6; цель для сравнения — `http://10.20.0.100/` против `http://192.168.5.21/`. - **Обратная трансляция опирается на `ct`** — как и прежде. Stateless-вариант (`nw_src=member, tp_src=8080 → VIP:80`) остаётся заделом: в сегменте он потребует аккуратного исключения трафика, который бэкенд сам инициирует с порта 8080. - **Unicast ARP внутри сегмента рассылается флудом**, если MAC ещё не выучен — обычное поведение обучающегося коммутатора, но без таймера старения записей короче 300 с. - **Приватный листенер только TCP и только IPv4**, как и публичный. UDP- и L3-листенеров нет. - Кворум, control plane в OVSDB, ICMP-ошибки и фрагменты — по-прежнему вне рамок. ## Задел на шаг 4 1. **Stateless-датапас** для обоих путей: явный un-DNAT вместо `ct(nat)`. На коммутируемом пути это снимет последнюю зависимость от состояния. 2. **Control plane:** модель `Load_Balancer → Listener → Pool → Member` в OVSDB вместо `topology.env`. Сейчас листенер, сегмент и состав пула описаны в трёх местах, а добавление ВМ требует правки `topology.env`, `hc-config.sh` и запуска `attach-segment.sh`. 3. **Старение FDB и защита от переполнения** — сейчас записи живут 300 с без ограничения на число MAC в сегменте. 4. Второй узел LB: кворум наблюдений, генерации пулов, сравнение дайджестов.