150 lines
9.9 KiB
Markdown
150 lines
9.9 KiB
Markdown
# Шаг 3 — план внедрения: листенер в приватном сегменте
|
||
|
||
**Дата:** 2026-08-17
|
||
**Предыдущий шаг:** [STEP2_SUMMARY.md](STEP2_SUMMARY.md)
|
||
**Итоги:** [STEP3_SUMMARY.md](STEP3_SUMMARY.md)
|
||
|
||
## Задача
|
||
|
||
Поддержать листенер балансировщика **в приватном сегменте** — в одном сегменте
|
||
или во всех сразу — при условиях:
|
||
|
||
1. листенер работает как публичный: принимает на порту 80 и транслирует на порт
|
||
бэкенда 8080;
|
||
2. бэкенд может стоять в одном сегменте с клиентом, отправившим запрос;
|
||
3. в ОС клиента и бэкенда не вносится ничего — ни адресов, ни маршрутов, ни
|
||
интерфейсов;
|
||
4. адресное пространство подсети задаёт заказчик, дробить её нельзя;
|
||
5. трафик внутри сегмента не блокируется;
|
||
6. единственный уровень влияния — OVS как data plane SDN и сама нода
|
||
балансировщика.
|
||
|
||
Ключевое требование: на бэкенд приходит пакет с исходным IP клиента.
|
||
|
||
## Почему прежняя схема не годится
|
||
|
||
Клиент и бэкенд — L2-соседи в одной подсети. Ответ бэкенда уходит клиенту
|
||
напрямую, минуя балансировщик: снять DNAT негде, клиент получает пакет от
|
||
`10.20.0.2:8080` вместо `10.20.0.100:80` и рвёт соединение.
|
||
|
||
Условия отсекают все обходные пути:
|
||
|
||
| Вариант | Почему не подходит |
|
||
|---|---|
|
||
| SNAT | уничтожает сохранение IP клиента |
|
||
| DSR (VIP на `lo` бэкенда) | нарушает условие 3, не транслирует порт (условие 1) |
|
||
| Изоляция портов сегмента (private VLAN) | нарушает условие 5, неприменимо к ВМ |
|
||
| Дробление подсети маршрутами `/25` на бэкенде | нарушает условия 3 и 4 |
|
||
|
||
Остаётся единственная предпосылка, совпадающая с условием 6: **порты клиента и
|
||
бэкенда — порты OVS**. Тогда кадр «бэкенд → клиент-сосед» проходит через
|
||
OpenFlow-таблицы даже при прямом L2-обмене, и обратную трансляцию есть где
|
||
выполнить.
|
||
|
||
В целевой среде это выполняется по построению: ВМ подключены к vSwitch. Текущий
|
||
стенд этому не соответствует — приватный сегмент коммутирует docker-бридж
|
||
`hpnn-p2`, а `br-lb` подключён к нему одним портом. Шаг 3 приводит стенд к
|
||
целевой модели.
|
||
|
||
## Решение
|
||
|
||
`br-lb` становится **коммутатором приватного сегмента** и выполняет трансляцию
|
||
на пути кадра. Для внутрисегментного трафика нет ни маршрутизации, ни
|
||
`dec_ttl`: с точки зрения ВМ клиент и бэкенд остаются L2-соседями.
|
||
|
||
**Прямой путь.** Клиент резолвит VIP через ARP-респондер OVS и шлёт кадр на MAC
|
||
шлюза сегмента. Таблица листенера кладёт сессию в слот, таблица DNAT
|
||
переписывает `VIP:80 → member:8080`, `src` не трогает, заменяет dst MAC на MAC
|
||
члена и отдаёт кадр в коммутацию.
|
||
|
||
**Обратный путь.** Бэкенд отвечает соседу напрямую: `src=member:8080`,
|
||
`dst=клиент`. Кадр приходит на порт OVS, где `ct(nat)` снимает трансляцию —
|
||
`src` возвращается к `VIP:80` — и кадр коммутируется в порт клиента.
|
||
|
||
**Прочий трафик сегмента** (`be↔be`, клиент к бэкенду напрямую, egress к
|
||
docker-шлюзу, ARP) коммутируется обычным образом, без трансляции.
|
||
|
||
## Топология после изменения
|
||
|
||
Каждая ВМ сегмента — отдельный порт `br-lb`. Порты `p2`/`p3` остаются как
|
||
аплинки сегмента к docker-шлюзу: через них идёт egress бэкендов (профиль A).
|
||
|
||
| Сегмент | Порт | ofport | Адрес |
|
||
|---|---|---|---|
|
||
| priv2 `10.20.0.0/24` | `p2` (аплинк) | 2 | — |
|
||
| | `hcif-p2` | 4 | `10.20.0.253` |
|
||
| | `be1` | 10 | `10.20.0.2` |
|
||
| | `be3` | 11 | `10.20.0.3` |
|
||
| | `cli2` | 12 | `10.20.0.10` |
|
||
| priv3 `10.30.0.0/24` | `p3` (аплинк) | 3 | — |
|
||
| | `hcif-p3` | 5 | `10.30.0.253` |
|
||
| | `be2` | 20 | `10.30.0.2` |
|
||
| | `be4` | 21 | `10.30.0.3` |
|
||
| | `cli3` | 22 | `10.30.0.10` |
|
||
| pub `192.168.5.0/24` | `pub0` | 1 | маршрутизируемый, без изменений |
|
||
|
||
Шлюз сегмента `10.20.0.1` и VIP `10.20.0.100` отвечают одним MAC — `$P2_MAC`.
|
||
Это и есть признак, по которому пайплайн отличает L3-трафик (адресован
|
||
маршрутизатору) от L2-трафика сегмента.
|
||
|
||
## Карта таблиц
|
||
|
||
Новые — 1, 24, 25; остальные сохраняют назначение шага 2.
|
||
|
||
| Таблица | Назначение |
|
||
|---|---|
|
||
| 0 | определение сегмента по `in_port` (`reg0`), обучение MAC и adjacency |
|
||
| 1 | классификация: ARP, ICMP, листенер, L3-трафик к шлюзу, коммутация |
|
||
| 5 | ARP-респондер (адреса узла и VIP), затем коммутация |
|
||
| 6 | ICMP echo-респондер |
|
||
| 10 | листенеры: публичный и приватные |
|
||
| 11 | таблица слотов (заливает `hcd`, не меняется) |
|
||
| 12 | применение члена пула: DNAT + L2-выдача либо DNAT + маршрутизация |
|
||
| 15/16 | обратный путь L3 (публичный и межсегментный) |
|
||
| 20/21 | маршрутизация и adjacency |
|
||
| **24** | обратная трансляция коммутируемого трафика сегмента |
|
||
| **25** | L2-коммутация сегмента: FDB, broadcast и unknown flood |
|
||
|
||
Регистры: `reg0` — номер сегмента, `reg1` — слот, `reg2` — член пула.
|
||
|
||
## Порядок работ
|
||
|
||
1. `lb/topology.env` — порты и адреса участников сегментов, VIP приватных
|
||
листенеров, переключатель `PRIV_LISTENERS`.
|
||
2. `scripts/attach-segment.sh` — veth из netns контейнера в `br-lb`, адрес, MAC
|
||
и маршруты назначаются снаружи (эмуляция гипервизора и DHCP).
|
||
`docker-compose.yml`: участники сегментов переводятся на `network_mode: none`;
|
||
добавляются клиенты `cli2` и `cli3`.
|
||
3. `lb/pipeline.sh` — таблицы 0/1/5/25: сегментация, обучение, коммутация.
|
||
Проверка связности сегмента до всякой балансировки.
|
||
4. Листенер: таблицы 10/12 и обратная трансляция в таблице 24.
|
||
5. `lb/lbctl.sh` — `trace` по приватному листенеру, `fdb`.
|
||
6. `scripts/verify.sh` — новая секция и регрессия существующих проверок.
|
||
7. Документация: `docs/STEP3_SUMMARY.md`, `README.md`,
|
||
`docs/PRIVATE_LB_DATAFLOW.md`, `docs/MANUAL_TEST_PLAN.md`.
|
||
|
||
## Критерии приёмки
|
||
|
||
- `curl http://10.20.0.100/` из `cli2` обслуживается всеми четырьмя членами;
|
||
- во всех ответах `client=10.20.0.10` — исходный IP клиента;
|
||
- `served=10.20.0.2:8080` — трансляция порта 80 → 8080 выполняется;
|
||
- TTL ответа от VIP равен TTL прямого ответа соседа по сегменту — путь остался
|
||
коммутируемым;
|
||
- внутрисегментная связность не пострадала: `cli2 → be1:8080` напрямую,
|
||
`be1 → be3:8080`, egress бэкенда мимо балансировщика;
|
||
- `PRIV_LISTENERS="p2"` выключает листенер в priv3, не затрагивая связность;
|
||
- все проверки шага 2 проходят.
|
||
|
||
## Риски
|
||
|
||
- **`br-lb` получает функцию L2-коммутатора** — обучение, flood, изоляция
|
||
сегментов. Новый класс правил и главный источник ошибок шага, поэтому этап 3
|
||
проверяется до подключения балансировки.
|
||
- **Хрупкость стенда:** veth, добавленные вручную, теряются при пересоздании
|
||
контейнера. `make attach` восстанавливает; в реальной среде порты создаёт
|
||
гипервизор.
|
||
- **Обратная трансляция опирается на `ct`**, как и прежде. Stateless-вариант —
|
||
задел следующего шага.
|
||
- **Нагрузка сегмента ложится на балансировщик**: весь внутрисегментный трафик
|
||
проходит через `br-lb`.
|