Files
hpnn-proto/docs/STEP1_IMPLEMENTATION_PLAN.md

13 KiB
Raw Permalink Blame History

Шаг 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.