13 KiB
Шаг 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, VIP192.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)
modprobe openvswitch(хост-модуль виден через/lib/modules:ro), запускovsdb-server+ovs-vswitchd, созданиеbr-lbсfail_mode=secure,protocols=OpenFlow13,OpenFlow15и без контроллера.- Для каждого из трёх 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 остаётся за контейнером и предотвращает выдачу этих адресов кому-то ещё. - Применение пайплайна атомарным бандлом:
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)
docs/STEP1_IMPLEMENTATION_PLAN.md— план внедрения (копия этого документа) — до начала работ.README.md— назначение стенда, схема, адресный план, запуск, проверка, устранение неполадок.docs/STEP1_SUMMARY.md— по завершении: что сделано, отличия от плана, известные ограничения (statefulctвместо stateless un-DNAT, отсутствие ICMP-ошибок и health-check, один узел LB вместо ECMP-набора) и заготовка задач шага 2.