127 lines
13 KiB
Markdown
127 lines
13 KiB
Markdown
# Шаг 1 — прототип окружения hpnn_v2 (OVS: маршрутизация + балансировка)
|
||
|
||
## Контекст
|
||
|
||
`hpnn_v1` содержит только дизайн-концепцию (`/opt/lvraid/claude/hpnn_v1/docs/design.md`, v0.3) OVS-native балансировщика: весь датапас — OpenFlow, DNAT без SNAT (бэкенд видит реальный IP клиента), возврат трафика маршрутизацией, control plane пишется свой. Кода нет.
|
||
|
||
`hpnn_v2` начинается с шага 1: собрать минимально достаточное контейнерное окружение, на котором можно развивать две подсистемы — **маршрутизации** и **балансировки нагрузки**. Результат шага: клиент из 192.168.5.0/24 обращается на VIP контейнера-1 и видит поочерёдно два разных бэкенда, а бэкенды видят реальный IP клиента; одновременно работает транзитная маршрутизация между приватными сегментами. Всё форвардинг-решение принимает OVS, ядро контейнера-1 транзитный трафик не обрабатывает.
|
||
|
||
Утверждённые решения:
|
||
- публичный сегмент — Docker **macvlan** поверх `enp3s0` (схема уже проверена на этом хосте: `router-functest_lan`, 192.168.5.11);
|
||
- **kernel datapath** OVS (модуль `openvswitch.ko` на хосте есть, не загружен);
|
||
- балансировка — **`group type=select`** + `ct(nat)`;
|
||
- маршрутизация — **целиком в OpenFlow** (свой ARP-респондер, статические next-hop-привязки, ICMP-ошибки на шаге 1 не генерируются);
|
||
- адреса: узел `192.168.5.20`, VIP `192.168.5.21`.
|
||
|
||
## Топология
|
||
|
||
```
|
||
клиент 192.168.5.0/24
|
||
│
|
||
[ enp3s0, macvlan ] сеть 1 «публичная»: 192.168.5.0/24
|
||
│ pub0 (02:42:c0:a8:05:14)
|
||
┌─────┴──────────────────────────────┐
|
||
│ lb-router (privileged, OVS) │ узел .20, VIP .21
|
||
│ br-lb: pub0, p2, p3 │
|
||
└──┬──────────────────────────┬──────┘
|
||
p2 │ 10.20.0.1 │ 10.30.0.1 p3
|
||
сеть 2 │ 10.20.0.0/24 сеть 3 │ 10.30.0.0/24
|
||
be1│ 10.20.0.2 be2│ 10.30.0.2
|
||
```
|
||
|
||
- Docker-шлюзы приватных сетей смещены на `.254` (`ipam.config.gateway`), чтобы `.1` занял OVS-роутер и не конфликтовал с host-бриджем.
|
||
- Дефолт бэкендов остаётся на `.254` (выход в интернет мимо LB — профиль A из §3.2 дизайна v1). Через LB бэкенды маршрутизируют только клиентские префиксы:
|
||
`ip route add 192.168.5.0/24 via 10.20.0.1` и `ip route add 10.30.0.0/24 via 10.20.0.1` (симметрично на be2).
|
||
- MAC бэкендов и `pub0` фиксируются в compose (`mac_address:`) — на них строятся статические adjacency-правила.
|
||
|
||
## Структура репозитория
|
||
|
||
```
|
||
/opt/lvraid/claude/hpnn_v2/
|
||
├── README.md # обзор стенда, запуск, проверка
|
||
├── docs/
|
||
│ ├── STEP1_IMPLEMENTATION_PLAN.md # копия этого плана (артефакт «план внедрения»)
|
||
│ └── STEP1_SUMMARY.md # итоги по факту выполнения
|
||
├── docker-compose.yml
|
||
├── Makefile # up / down / flows / verify / logs
|
||
├── lb/
|
||
│ ├── Dockerfile # debian:bookworm-slim + openvswitch-switch + iproute2 + tcpdump
|
||
│ ├── entrypoint.sh # запуск OVS, сборка br-lb, применение пайплайна
|
||
│ ├── topology.env # единственный источник правды: IP, MAC, порты, VIP
|
||
│ ├── pipeline.sh # генерация OpenFlow-правил из topology.env
|
||
│ └── lbctl.sh # обёртки: dump-flows / group-stats / ofproto-trace
|
||
├── backend/
|
||
│ ├── Dockerfile # multi-stage: golang:1.23-alpine → alpine (нужен iproute2)
|
||
│ ├── entrypoint.sh # установка маршрутов на клиентские префиксы, запуск app
|
||
│ ├── go.mod
|
||
│ └── main.go # hostname, дата/время, IP клиента, локальный адрес
|
||
└── scripts/
|
||
├── host-prereq.sh # modprobe openvswitch; опциональный macvlan-shim для проверки с хоста
|
||
└── verify.sh # e2e-проверки (см. раздел «Верификация»)
|
||
```
|
||
|
||
## Реализация
|
||
|
||
### 1. Контейнер-1: подъём OVS и портов (`lb/entrypoint.sh`)
|
||
|
||
1. `modprobe openvswitch` (хост-модуль виден через `/lib/modules:ro`), запуск `ovsdb-server` + `ovs-vswitchd`, создание `br-lb` с `fail_mode=secure`, `protocols=OpenFlow13,OpenFlow15` и без контроллера.
|
||
2. Для каждого из трёх Docker-интерфейсов: снять IP (`ip addr flush`), внести в мост (`ovs-vsctl add-port br-lb <if>`), закрепить `ofport_request` (pub0=1, p2=2, p3=3), поднять.
|
||
IP-адреса на интерфейсах не нужны — L3 живёт в OpenFlow, IPAM-резервация Docker остаётся за контейнером и предотвращает выдачу этих адресов кому-то ещё.
|
||
3. Применение пайплайна атомарным бандлом: `ovs-ofctl --bundle -O OpenFlow15 replace-flows br-lb <(pipeline.sh)` — обновление без blackhole-окна (принцип §4 дизайна v1).
|
||
|
||
**Риск и запасной вариант.** Подключение macvlan-устройства портом OVS — рабочая, но нечастая схема. Если `ovs-vsctl add-port` не заведётся, откат: внутри контейнера поднять linux-bridge `brpub`, включить в него macvlan и один конец veth-пары, второй конец veth отдать в `br-lb`. Логика OpenFlow при этом не меняется — правится только шаг 2 entrypoint.
|
||
|
||
### 2. OpenFlow-пайплайн (`lb/pipeline.sh`)
|
||
|
||
| Таблица | Назначение |
|
||
|---|---|
|
||
| **0** | Классификация: ARP → t5; ICMP echo на собственные IP → t6; IP из `pub0` → learn-адъяценции + t10; IP из `p2/p3` → t30; иначе drop |
|
||
| **3** (inline `learn` в t0) | Обучение MAC клиентов: `learn(table=21, NXM_OF_IP_DST[]=NXM_OF_IP_SRC[], load:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], load:<pub0 mac>->NXM_OF_ETH_SRC[], output:NXM_OF_IN_PORT[])`, `hard_timeout=300` — заменяет отсутствующий ARP-резолвер для клиентской стороны |
|
||
| **5** | ARP-респондер для `192.168.5.20`, `192.168.5.21`, `10.20.0.1`, `10.30.0.1` (op=2, swap SHA/THA, `output:IN_PORT`) |
|
||
| **6** | ICMP echo-reply для тех же адресов (swap eth/ip, `icmp_type=0`) — даёт `ping` до узла и VIP |
|
||
| **10** | Листенеры: `tcp,nw_dst=192.168.5.21,tp_dst=80 → group:100`; остальной IP → t20 (обычная маршрутизация) |
|
||
| **group 100** | `type=select, selection_method=hash, fields(ip_src,tcp_src)`; 2 бакета, в каждом `ct(commit,zone=1,nat(dst=10.X0.0.2:8080),table=20)`. `tcp_src` в ключе обязателен — иначе все запросы одного клиента лягут на один бэкенд |
|
||
| **20** | Маршрутизация: LPM через приоритет = длина префикса. `10.20.0.0/24`→p2, `10.30.0.0/24`→p3, `192.168.5.0/24`→pub0; действия `dec_ttl`, `mod_dl_src=<MAC порта>`, `resubmit(,21)`. `priority=0` → drop (дефолта нет) |
|
||
| **21** | Adjacency: статически `nw_dst=10.20.0.2 → mod_dl_dst=<be1 mac>, output:p2` (и симметрично be2); клиентские адреса приходят сюда из `learn` |
|
||
| **30** | Возврат из приватных сетей: TCP → `ct(table=31, zone=1, nat)` (снимает DNAT: `src` снова VIP, `dst` — клиент); прочий IP (be↔be, транзит) → t20 |
|
||
| **31** | После ct → t20 |
|
||
|
||
Un-DNAT выполняет `ct(nat)`, отдельных правил обратной трансляции не требуется. Stateless-вариант из §2 дизайна v1 остаётся задачей следующего шага — здесь сознательно взят stateful, чтобы окружение поднялось минимальными средствами.
|
||
|
||
### 3. Бэкенды (`backend/`)
|
||
|
||
`main.go` — HTTP-сервер на `:8080`, отдаёт `text/plain`: hostname, дата/время с таймзоной, `RemoteAddr` (реальный IP клиента — ключевое доказательство DNAT-only), локальный адрес соединения и счётчик запросов. Плюс `/healthz` для будущих проб. Стандартная библиотека, без зависимостей.
|
||
|
||
`entrypoint.sh` (нужен `NET_ADMIN`): ставит маршруты на клиентский префикс и на соседний приватный сегмент через `.1`, затем `exec` приложения.
|
||
|
||
### 4. Compose
|
||
|
||
Три сети: `pub` (macvlan, `parent: enp3s0`, `192.168.5.0/24`), `priv2` (bridge, `10.20.0.0/24`, gateway `.254`), `priv3` (bridge, `10.30.0.0/24`, gateway `.254`).
|
||
`lb-router`: `privileged: true`, `/lib/modules:ro`, все три сети, фиксированные IP и MAC.
|
||
`be1`/`be2`: по одной приватной сети каждый, `cap_add: [NET_ADMIN]`, фиксированные IP и MAC.
|
||
|
||
## Верификация
|
||
|
||
**Подготовка хоста:** `scripts/host-prereq.sh` — `modprobe openvswitch`; при желании проверять с самого хоста (192.168.5.9) он же создаёт macvlan-shim, т.к. macvlan не пропускает трафик хост↔контейнер напрямую.
|
||
|
||
С клиента из 192.168.5.0/24:
|
||
|
||
| Проверка | Команда | Ожидание |
|
||
|---|---|---|
|
||
| Доступность узла и VIP | `ping 192.168.5.20`, `ping 192.168.5.21` | отвечает OpenFlow-респондер (t6) |
|
||
| Балансировка | `for i in $(seq 20); do curl -s http://192.168.5.21/; done` | оба hostname встречаются, распределение близко к 50/50 |
|
||
| Сохранение IP клиента | тот же вывод | в поле «клиент» — реальный адрес 192.168.5.x, не адрес LB |
|
||
| Распределение по бакетам | `docker compose exec lb-router ovs-ofctl -O OpenFlow15 dump-group-stats br-lb` | счётчики обоих бакетов растут |
|
||
| Детерминизм выбора | `ovs-appctl ofproto/trace br-lb <5-tuple>` | одинаковый бакет для одного и того же кортежа |
|
||
| Маршрутизация между сегментами | `docker compose exec be1 curl -s http://10.30.0.2:8080/` | ответ be2, трафик прошёл через t20/t21 |
|
||
| Профиль A (egress мимо LB) | `docker compose exec be1 ping -c1 8.8.8.8` при `tcpdump -ni p2` на LB | пакеты на LB не появляются |
|
||
| Целостность пайплайна | `ovs-ofctl -O OpenFlow15 dump-flows br-lb`, `ovs-vsctl show` | три порта в мосту, все таблицы на месте, счётчик drop-правил t20 не растёт при нормальном трафике |
|
||
|
||
`scripts/verify.sh` прогоняет проверки, выполнимые с хоста, и печатает сводку; клиентские шаги с curl/ping вынесены в README как ручные.
|
||
|
||
## Артефакты (по требованию CLAUDE.md)
|
||
|
||
1. `docs/STEP1_IMPLEMENTATION_PLAN.md` — план внедрения (копия этого документа) — **до** начала работ.
|
||
2. `README.md` — назначение стенда, схема, адресный план, запуск, проверка, устранение неполадок.
|
||
3. `docs/STEP1_SUMMARY.md` — по завершении: что сделано, отличия от плана, известные ограничения (stateful `ct` вместо stateless un-DNAT, отсутствие ICMP-ошибок и health-check, один узел LB вместо ECMP-набора) и заготовка задач шага 2.
|