# Шаг 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 `), закрепить `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:->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=`, `resubmit(,21)`. `priority=0` → drop (дефолта нет) | | **21** | Adjacency: статически `nw_dst=10.20.0.2 → mod_dl_dst=, 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.