Add FRR anycast lab: HLD, containerized topology, docs

Реализует HLD-дизайн (hld.md) в виде развёртываемого Docker Compose
окружения: 5 FRR-роутеров (A1-A3 anycast 10.200.200.1/32 на AS 65001,
B1/B2 на AS 65002/65003) + 6 клиентских контейнеров, ECMP-балансировка
трафика клиентов площадки B к anycast-адресу площадки A.

- docker-compose.yml, frr/*, entrypoints/* — топология и конфиги FRR
- scripts/verify.sh, scripts/traffic-test.sh — проверка BGP/ECMP и трафика
- hld.md — HLD-дизайн + Mermaid-диаграмма топологии
- README.md — запуск, структура, команды проверки на роутерах и клиентах
- docs/ — планы внедрения и summary по каждому изменению

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bc3HkMiDwooSyLBLoMKNyF
This commit is contained in:
ayurishchevandClaude Sonnet 5 committed 2026-09-03 12:52:44 +03:00
commit eaa577e6b7
30 files changed
+1035

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
# План внедрения: контейнерный POC anycast-топологии FRR
**Дата:** 2026-09-03
**Источник:** [hld.md](../hld.md)
## Цель
Развернуть в Docker Compose топологию из `hld.md` (5 FRR-роутеров, anycast `10.200.200.1/32` на площадке A, клиентские сети площадки B) и убедиться, что трафик от клиентов B реально распределяется по ECMP между тремя anycast-узлами A1/A2/A3.
## Проверка окружения (выполнена)
- Docker 28.3.2 + Compose plugin v2.38.2 — доступны, `NET_ADMIN`-контейнеры работают.
- `frrouting/frr:latest` (Alpine 3.16) — тянется из Docker Hub, содержит `vtysh`, `zebra`, `bgpd`, `staticd`, `ip`, `ping`, `python3`, `nc`. Кастомный образ не нужен.
- Модуль ядра `dummy` доступен — anycast-интерфейс на A-узлах создаётся в рантайме через `ip link add dummy0 type dummy`.
- `containerlab` не устанавливаем — Docker Compose с `internal`-сетями на каждый p2p-линк полностью закрывает задачу без дополнительных зависимостей.
## Архитектура
Каждый линк HLD (`/30` A↔B, `/24` клиентские сегменты) — отдельная Docker-сеть `internal: true` со статическими IP на участниках (без Docker-gateway, который иначе занял бы адрес в `/30`).
| Сеть | CIDR | Участники |
|---|---|---|
| `link-a1-b1` | 10.0.1.0/30 | a1 (.1), b1 (.2) |
| `link-a1-b2` | 10.0.2.0/30 | a1 (.1), b2 (.2) |
| `link-a2-b1` | 10.0.3.0/30 | a2 (.1), b1 (.2) |
| `link-a2-b2` | 10.0.4.0/30 | a2 (.1), b2 (.2) |
| `link-a3-b1` | 10.0.5.0/30 | a3 (.1), b1 (.2) |
| `link-a3-b2` | 10.0.6.0/30 | a3 (.1), b2 (.2) |
| `net-b1-clients` | 10.100.1.0/24 | b1 (.1), c1 (.11), c2 (.12), c3 (.13) |
| `net-b2-clients` | 10.100.2.0/24 | b2 (.1), c4 (.14), c5 (.15), c6 (.16) |
- **a1/a2/a3**: `frrouting/frr:latest`, `cap_add: [NET_ADMIN, NET_RAW]`. Кастомный entrypoint создаёт `dummy0` (`10.200.200.1/32`) и поднимает HTTP-responder на этом адресе (отдаёт hostname контейнера — видно, какой узел ответил), затем стартует штатный `docker-start` FRR. Конфиг — прямая транскрипция A1/A2/A3 из `hld.md`.
- **b1/b2**: `frrouting/frr:latest`, конфиг — транскрипция B1/B2 из `hld.md` (`bgp bestpath as-path multipath-relax`, `maximum-paths 3`).
- **c1…c6**: `nicolaka/netshoot` (ping/curl/traceroute/iperf3/mtr), entrypoint выставляет статический IP и `ip route add default via <B-роутер>`.
## Файлы
```
docker-compose.yml
frr/{a1,a2,a3,b1,b2}/{daemons,frr.conf,vtysh.conf}
entrypoints/{a-entrypoint.sh,b-entrypoint.sh,client-entrypoint.sh}
scripts/{verify.sh,traffic-test.sh}
docs/2026-09-03-anycast-lab-plan.md (этот файл)
docs/2026-09-03-anycast-lab-summary.md (по завершении)
README.md (дополняется)
```
## Шаги
1. FRR-конфиги для 5 узлов (daemons, frr.conf, vtysh.conf).
2. Entrypoint-скрипты (dummy0 + anycast responder для A; статическая адресация для клиентов).
3. `docker-compose.yml` — 8 сетей + 5 роутеров + 6 клиентов.
4. `scripts/verify.sh` — проверка BGP-сессий (Established) и ECMP-маршрута до `10.200.200.1/32` (3 пути) на b1/b2.
5. `scripts/traffic-test.sh` — ping/traceroute/curl-цикл с клиентов, демонстрирующий распределение трафика по A1/A2/A3.
6. Поднятие (`docker compose up -d`), прогон проверок.
7. Обновление `README.md`.
8. Summary-артефакт с результатами.
## Definition of done
- Все 13 контейнеров поднимаются без ошибок.
- На b1/b2: 3 Established BGP-сессии, 3 ECMP-пути до `10.200.200.1/32`.
- С клиентов `ping 10.200.200.1` проходит; `traffic-test.sh` показывает ответы от разных anycast-узлов (a1/a2/a3) при варьировании src-port.
+40
View File
@@ -0,0 +1,40 @@
# Summary: контейнерный POC anycast-топологии FRR
**Дата:** 2026-09-03
**План:** [2026-09-03-anycast-lab-plan.md](2026-09-03-anycast-lab-plan.md)
## Что сделано
Топология из `hld.md` полностью развёрнута в Docker Compose (`docker-compose.yml`): 5 FRR-контейнеров (a1/a2/a3 — AS 65001, площадка A; b1/b2 — AS 65002/65003, площадка B) + 6 клиентских контейнеров (`nicolaka/netshoot`) на площадке B. 8 Docker-сетей воспроизводят все линки HLD (6 межплощадочных + 2 клиентских сегмента).
Созданные файлы: `docker-compose.yml`, `frr/{a1,a2,a3,b1,b2}/{daemons,frr.conf,vtysh.conf}`, `entrypoints/{a-entrypoint.sh,b-entrypoint.sh,client-entrypoint.sh}`, `scripts/{verify.sh,traffic-test.sh}`, `README.md`.
## Отклонения от первоначального плана (обнаружены и устранены в процессе)
1. **`/30` → `/29` на межплощадочных линках.** Docker всегда резервирует первый usable-адрес подсети под gateway моста, даже при `internal: true`. При буквальном `/30` (только 2 usable-адреса) это конфликтовало с адресом `.1`, который нужен был роутеру A (`Error: Address already in use`). Решение — расширить линковые подсети до `/29` и явно вынести gateway на неиспользуемый адрес (`.6`); адресация роутеров (`.1`/`.2`) осталась как в hld.md. Аналогично для клиентских `/24`-сетей gateway вынесен на `.254`, чтобы не конфликтовать с адресом B-роутера (`.1`).
2. **`sysctl -w` в entrypoint не работал** (`Read-only file system`) — `/proc/sys` внутри контейнера доступен на запись только для sysctl'ов, заданных декларативно через Docker (`sysctls:` в compose), а не вызовом `sysctl` из процесса внутри контейнера. Перенесли `net.ipv4.ip_forward=1` в `docker-compose.yml`.
3. **`cap_sys_admin` требуется FRR для namespace-операций** — с `cap_add: [NET_ADMIN, NET_RAW]` zebra/bgpd не стартовали (`privs_init: initial cap_set_proc failed`). Добавили `SYS_ADMIN` в `cap_add`.
4. **ECMP не реагировал на src-port.** С дефолтной политикой ядра `net.ipv4.fib_multipath_hash_policy=0` next-hop хешируется только по src/dst IP — один клиент всегда получал один и тот же anycast-узел, что противоречило описанной в hld.md 5-tuple-логике. Установили `net.ipv4.fib_multipath_hash_policy=1` на b1/b2.
5. **Убраны `interface ethN` из frr.conf.** Раз Docker Compose уже назначает статические IP через `ipv4_address`, а zebra сам подхватывает существующие адреса интерфейсов, ручное дублирование адресов в frr.conf было источником риска (несовпадение имени интерфейса `ethN` с ожидаемым линком). Оставлен только `dummy0` (единственный интерфейс, которого нет "из коробки" у Docker).
## Результаты проверки
- **BGP:** все 10 eBGP-сессий (a1/a2/a3 × b1/b2) в состоянии Established.
- **ECMP:** у b1 и у b2 — по 3 равнозначных пути (weight 1) до `10.200.200.1/32`, через a1/a2/a3 соответственно.
- **Связность клиент → anycast:** `ping`/`traceroute`/`curl` с любого клиента (c1…c6) до `10.200.200.1` проходят.
- **Распределение трафика (`scripts/traffic-test.sh`):** серии запросов с варьированием src-port показывают ответы от всех трёх anycast-узлов в примерно равной пропорции, например с `c4` (30 запросов): `a1: 13, a2: 2, a3: 15`; с `c2` (24 запроса): `a1: 10, a2: 7, a3: 7`. Ответ каждого узла содержит его hostname (мини HTTP-responder на dummy0), что визуально подтверждает, какой именно anycast-узел обслужил запрос.
- **Идемпотентность:** `docker compose down && docker compose up -d` поднимает окружение с нуля без ручного вмешательства, BGP сходится за секунды.
## Как повторить
```bash
docker compose up -d
./scripts/verify.sh
./scripts/traffic-test.sh c1 30
docker compose down
```
## Дальнейшие возможные шаги (не выполнялись, вне рамок задачи)
- Симуляция отказа одного из A-узлов (`docker compose stop a2`) для проверки конвергенции ECMP на оставшихся 2 путях.
- Нагрузочный тест через `iperf3` (уже есть в образе `nicolaka/netshoot`) вместо HTTP-responder.
@@ -0,0 +1,23 @@
# План внедрения: раздел проверки в README.md
**Дата:** 2026-09-03
## Цель
Дополнить `README.md` разделом «Проверка окружения» с готовым набором команд для маршрутизаторов (`vtysh`) и клиентов (`ping`/`traceroute`/`curl`), с описанием ожидаемого результата — чтобы можно было вручную убедиться, что BGP сошёлся, ECMP работает и трафик от клиентов реально доходит до anycast-адреса.
## Источник
Список команд уже был согласован в чате (см. предыдущий ответ по `vtysh`) и опробован в рамках `scripts/verify.sh` / `scripts/traffic-test.sh`. Задача — перенести это в постоянную документацию README, дополнив клиентской частью.
## Шаги
1. Добавить в `README.md` раздел «Проверка окружения»:
- Команды `vtysh` на роутерах (BGP-сессии, ECMP-маршрут, advertised/received routes, интерфейсы) с ожидаемым результатом.
- Команды на клиентах (`ping`, `traceroute`, `curl` до anycast) с ожидаемым результатом.
- Ссылку на то, что то же самое автоматизировано в `scripts/verify.sh` / `scripts/traffic-test.sh`.
2. Summary-артефакт с итогом изменения.
## Definition of done
- В README есть самодостаточный перечень команд для обоих типов узлов (роутер/клиент), с пояснением, что считать успехом.
@@ -0,0 +1,19 @@
# Summary: раздел проверки в README.md
**Дата:** 2026-09-03
**План:** [2026-09-03-readme-verification-docs-plan.md](2026-09-03-readme-verification-docs-plan.md)
## Что сделано
В `README.md` добавлен раздел «Проверка окружения» с двумя таблицами команд:
- **На маршрутизаторах** (`vtysh`) — `show bgp summary`, `show ip route 10.200.200.1/32`, `show bgp ipv4 unicast 10.200.200.1/32`, `show ip bgp neighbor <ip> advertised-routes`, `show interface brief`, для каждой указано ожидаемое значение (Established, 3×ECMP weight 1, и т.д.).
- **На клиентах** — `ping`, `traceroute`, `curl` до anycast-адреса и серия `curl` с варьированием `--local-port` для демонстрации распределения по A1/A2/A3.
Команды `show ip bgp neighbor ... advertised-routes` проверены вживую на работающем окружении: a1 анонсирует `10.200.200.1/32` в сторону b1, b1 анонсирует `10.100.1.0/24` в сторону a1 — совпадает с описанием в README.
## Изменённые файлы
- `README.md` — новый раздел «Проверка окружения».
- `docs/2026-09-03-readme-verification-docs-plan.md` — план (этот цикл).
- `docs/2026-09-03-readme-verification-docs-summary.md` — этот файл.
+13
View File
@@ -0,0 +1,13 @@
# План внедрения: Mermaid-диаграмма топологии
**Дата:** 2026-09-03
## Цель
Добавить в `hld.md` визуальную схему топологии в формате Mermaid (flowchart) — дополнение к существующей ASCII-схеме, наглядно показывающее full-mesh A↔B, anycast на площадке A и клиентские сети на площадке B.
## Шаги
1. Добавить в `hld.md` раздел с Mermaid-диаграммой (subgraph на площадку A, subgraph на B1/B2 с клиентами, рёбра — 6 линков с подписями подсетей).
2. Добавить ссылку/упоминание в `README.md`.
3. Summary-артефакт.
@@ -0,0 +1,16 @@
# Summary: Mermaid-диаграмма топологии
**Дата:** 2026-09-03
**План:** [2026-09-03-topology-diagram-plan.md](2026-09-03-topology-diagram-plan.md)
## Что сделано
В `hld.md` добавлен раздел «Диаграмма (Mermaid)» — flowchart с 3 подграфами (площадка A: A1/A2/A3 с anycast на dummy0; B1 с клиентами C1–C3; B2 с клиентами C4–C6) и 6 рёбрами A↔B, подписанными реальными `/30`-подсетями из HLD. Диаграмма дополняет уже существующую ASCII-схему, не заменяя её.
`README.md` дополнен пометкой, что в `hld.md` теперь есть визуальная диаграмма.
## Изменённые файлы
- `hld.md` — новый раздел с Mermaid-диаграммой.
- `README.md` — уточнена ссылка на `hld.md`.
- `docs/2026-09-03-topology-diagram-plan.md`, `docs/2026-09-03-topology-diagram-summary.md`.