Files
ayurishchevandClaude Sonnet 5 036f32869b Add Prometheus Node Exporter to frr_anycast role
Устанавливает prometheus-node-exporter из официального репозитория
Ubuntu 24 LTS (universe, сторонний репозиторий не добавляется) на
всех узлах обеих сред (площадка A + Облачная площадка), безусловно.
Сервис enabled+started, слушает стандартный порт :9100 (дефолт
пакета, конфиг не переопределяется). Firewall-правил не требуется —
в nftables.conf.j2 нет table inet filter с policy drop.

Шаблоны FRR/nftables не затронуты: render-check.sh 6/6,
render-check-cloud.sh 84/84, без изменений.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bc3HkMiDwooSyLBLoMKNyF
2026-09-11 13:24:08 +03:00

183 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# poc_frr_anycast
POC anycast-топологии на FRR: две площадки, eBGP full-mesh, ECMP-балансировка трафика клиентов площадки B к anycast-адресу площадки A.
- **Дизайн (HLD):** [hld.md](hld.md) (в т.ч. Mermaid-диаграмма топологии)
- **История изменений:** [docs/](docs/) — на каждое изменение отдельный план внедрения и summary.
## Lab-окружение (Docker Compose)
Топология из `hld.md` поднимается локально в Docker Compose: 5 FRR-роутеров (a1/a2/a3 — площадка A, b1/b2 — площадка B) + 6 клиентских контейнеров площадки B. Каждый линк HLD — отдельная Docker-сеть с фиксированной адресацией, 1:1 соответствующая схеме из `hld.md`.
Детали архитектуры и обоснование решений: [docs/2026-09-03-anycast-lab-plan.md](docs/2026-09-03-anycast-lab-plan.md), результаты — [docs/2026-09-03-anycast-lab-summary.md](docs/2026-09-03-anycast-lab-summary.md).
### Запуск
```bash
docker compose up -d # поднять топологию (13 контейнеров)
./scripts/verify.sh # проверить BGP-сессии и ECMP-маршрут до 10.200.200.1/32
./scripts/traffic-test.sh [client] [count] # прогнать трафик к anycast, увидеть распределение по A1/A2/A3
docker compose down # разобрать окружение
```
Пример: `./scripts/traffic-test.sh c1 30` — 30 запросов с клиента `c1` (площадка B1) к anycast-адресу с варьированием src-port; ответ каждого anycast-узла (`a1`/`a2`/`a3`) содержит его hostname, что показывает распределение трафика.
### Структура
```
hld.md HLD-дизайн топологии
docker-compose.yml 13 контейнеров, 8 сетей (1 на линк)
frr/{a1,a2,a3,b1,b2}/ конфиги FRR (daemons, frr.conf, vtysh.conf)
entrypoints/ создание dummy0 + anycast-responder (A), маршрут по умолчанию (клиенты)
scripts/ verify.sh, traffic-test.sh
docs/ план внедрения + summary по каждому изменению
```
### Проверка окружения
Всё это уже автоматизировано в `./scripts/verify.sh` (BGP + маршруты) и `./scripts/traffic-test.sh` (трафик), но ниже — те же проверки вручную, командами на самих узлах.
#### На маршрутизаторах (`vtysh`)
Выполняются как `docker compose exec -T <node> vtysh -c "<command>"` или интерактивно: `docker compose exec <node> vtysh`.
| Команда | Где | Что должно быть в норме |
|---|---|---|
| `show bgp summary` | все 5 узлов | все соседи в состоянии `Established` (число вместо `Idle`/`Active` в колонке `State/PfxRcd`) — 2 соседа на a1/a2/a3, 3 на b1/b2 |
| `show ip route 10.200.200.1/32` | b1, b2 | 3 записи `* <ip>, via ethN, weight 1` — ECMP-маршрут до anycast через a1/a2/a3 |
| `show bgp ipv4 unicast 10.200.200.1/32` | b1, b2 | тот же префикс в BGP-таблице, 3 валидных пути, `Local AS 65001` у всех |
| `show ip bgp neighbor <ip> advertised-routes` | a1/a2/a3 (`<ip>` — сосед в сторону B) | среди анонсируемых — `10.200.200.1/32` |
| `show ip bgp neighbor <ip> advertised-routes` | b1, b2 (`<ip>` — сосед в сторону A) | среди анонсируемых — `10.100.1.0/24` (b1) / `10.100.2.0/24` (b2) |
| `show interface brief` | любой | все интерфейсы `up`, IP соответствуют `hld.md` |
Пример:
```bash
docker compose exec -T b1 vtysh -c "show bgp summary"
docker compose exec -T b1 vtysh -c "show ip route 10.200.200.1/32"
docker compose exec -T a1 vtysh -c "show ip bgp neighbor 10.0.1.2 advertised-routes"
```
#### На клиентах (площадка B, доверенная сеть)
Выполняются как `docker compose exec -T <client> <command>`, где `<client>` — один из `c1`…`c6`.
| Команда | Что должно быть в норме |
|---|---|
| `ping -c 4 10.200.200.1` | `0% packet loss`, ответы приходят с `ttl=63` (1 транзитный хоп — B-роутер) |
| `traceroute -n 10.200.200.1` | 2 хопа: локальный B-роутер (`10.100.1.1` / `10.100.2.1`), затем сразу `10.200.200.1` |
| `curl -s http://10.200.200.1/` | в ответе — hostname одного из `a1`/`a2`/`a3` (мини-HTTP на dummy0, см. `entrypoints/a-entrypoint.sh`) |
| серия `curl` с разным `--local-port` (см. `scripts/traffic-test.sh`) | ответы приходят от **разных** anycast-узлов — подтверждает, что ECMP реально распределяет трафик по 5-tuple, а не всегда уходит в один и тот же узел |
Пример:
```bash
docker compose exec -T c1 ping -c 4 10.200.200.1
docker compose exec -T c1 traceroute -n 10.200.200.1
docker compose exec -T c1 curl -s http://10.200.200.1/
./scripts/traffic-test.sh c1 30 # готовый прогон с разбивкой по узлам
```
Если `curl`/`ping` не проходят — сначала проверить BGP (`show bgp summary` на затронутых узлах), затем ECMP-маршрут (`show ip route 10.200.200.1/32` на b1/b2).
### Важные технические нюансы
- Линки между A и B заведены как `/29` (не `/30`, как в hld.md буквально) — Docker резервирует первый адрес подсети под gateway моста даже для `internal: true`-сетей, поэтому реальным peering-адресам (`.1`/`.2`, как в hld.md) нужно место рядом с ним. IP-адресация роутеров и логика BGP при этом полностью соответствуют hld.md.
- ECMP-балансировка на b1/b2 требует `net.ipv4.fib_multipath_hash_policy=1` (в `docker-compose.yml`) — по умолчанию (`0`) Linux хеширует next-hop только по src/dst IP, без учёта портов, из-за чего один клиент всегда попадал бы в один и тот же anycast-узел.
## Развёртывание площадки A на ВМ (Ansible)
`ansible/` — playbook для конфигурации FRR на реальных ВМ площадки A (a1/a2/a3), продолжение того же HLD, но не в Docker, а «в проде». Provisioning ВМ (создание, 3 сетевых интерфейса: 1 внешний/управляющий + 2 приватных к B1/B2) выполняется отдельным Terraform-сценарием (вне рамок этого репозитория) — приватные интерфейсы уже сконфигурированы к моменту запуска playbook; Ansible создаёт `dummy0` (anycast) и разворачивает FRR.
Все специфичные для площадки A значения (ASN, anycast-префикс, router-id и BGP-соседи каждой ноды, имена интерфейсов, SSH-доступ) вынесены в единый файл переменных окружения `ansible/site-a.env` — сама роль `frr_anycast` полностью параметризована и не содержит захардкоженных адресов/ASN. `frr.conf.j2` при рендере воспроизводит уже проверенную в Docker-лабе BGP-конфигурацию (`frr/a{1,2,3}/frr.conf`), за вычетом блока `interface dummy0` — в VM-варианте этот адрес назначается через netplan (`dummy0.netplan.yml.j2`), а не через frr.conf.
Роль `frr_anycast` обобщена: список BGP-соседей узла — не 2 жёстко именованных (`neighbor_b1`/`neighbor_b2`), а `bgp_neighbors` произвольной длины, каждый с флагом `prepend` (as-path prepend на исходящих анонсах). Опционально (`enable_snat: true`) роль также поднимает sNAT через `nftables` (masquerade на внешнем интерфейсе) — оба узла ниже используют эту одну и ту же роль с разными `group_vars`.
**Outbound-фильтр обязателен у всех соседей.** Каждому соседу назначен outbound route-map (`RM-OUT` или, если `prepend: true`, `RM-OUT-PREPEND`), матчащий `ip prefix-list PL-ANYCAST-OUT` (permit только `anycast_prefix`) с явным `deny` в конце — наружу уходит исключительно анонсируемый anycast-префикс, что бы ни попало в локальную BGP-таблицу от других соседей. Это фикс реального прод-инцидента: без фильтра FRR реэкспортировал случайно полученный `10.0.0.0/8` обратно в сторону других eBGP-соседей (см. [docs/2026-09-10-bgp-outbound-filter-summary.md](docs/2026-09-10-bgp-outbound-filter-summary.md)).
Детали: [docs/2026-09-03-ansible-site-a-plan.md](docs/2026-09-03-ansible-site-a-plan.md) / [docs/2026-09-03-ansible-site-a-summary.md](docs/2026-09-03-ansible-site-a-summary.md) — исходная реализация; [docs/2026-09-09-ansible-site-cloud-plan.md](docs/2026-09-09-ansible-site-cloud-plan.md) / [docs/2026-09-09-ansible-site-cloud-summary.md](docs/2026-09-09-ansible-site-cloud-summary.md) — доработка под второе окружение (см. ниже).
### Структура
```
ansible/
├── site-a.env / site-cloud.env источники site-специфичных значений (2 окружения)
├── run.sh общий для всех окружений (SITE_ENV_FILE + имя playbook'а)
├── inventory/{site-a,site-cloud}.yml ansible_host из lookup('env', ...)
├── group_vars/{site_a,site_cloud}.yml site-wide переменные (ASN, anycast, интерфейсы, bgp_neighbors)
├── host_vars/{a1,a2,a3,vr01..vr04}.yml per-node переменные (router-id, [bgp_neighbors])
├── site-a.yml / site-cloud.yml playbook'и (role: frr_anycast — общая для обоих)
├── roles/frr_anycast/ задачи, handlers, шаблоны (daemons.j2, frr.conf.j2,
│ dummy0.netplan.yml.j2, nftables.conf.j2)
└── tests/ render-check.sh (площадка A, golden-diff),
render-check-cloud.sh (cloud, grep-инварианты)
```
### Запуск
```bash
cd ansible
./run.sh site-a.yml # применить конфигурацию к a1/a2/a3 (env по умолчанию: site-a.env)
./run.sh site-a.yml --check --diff # сухой прогон
```
Если `ansible-playbook` не установлен в системе — поставить локально в venv, не трогая системный Python: `python3 -m venv venv && venv/bin/pip install ansible` (каталог `venv/` в `.gitignore`, не коммитится); `run.sh` сам подхватит `venv/bin/ansible-playbook`, если он есть.
`run.sh` универсален для всех окружений в этом репозитории: инвентарь берётся по имени playbook'а (`X.yml` → `inventory/X.yml`), env-файл — из `SITE_ENV_FILE` (по умолчанию `site-a.env`).
### Проверка без живых ВМ
На момент подготовки playbook ВМ площадки A ещё не подняты (Terraform-провижининг — отдельный шаг), поэтому функциональная проверка (`vtysh`, реальный apply) не выполнялась и не входит в текущую поставку. Доступна и выполнена офлайн-проверка:
```bash
ansible/run.sh site-a.yml --syntax-check # синтаксис
ansible/tests/render-check.sh # golden-diff: рендер frr.conf/daemons для a1/a2/a3
# сравнивается с уже работавшими в Docker-лабе frr/a{1,2,3}/
```
После того как Terraform поднимет ВМ — реальный прогон `./run.sh site-a.yml`, затем те же `vtysh`-команды, что и в разделе «[Проверка окружения](#проверка-окружения)» выше (заменить `docker compose exec -T <node> vtysh ...` на `ssh <node> vtysh ...` или `ansible site_a -m command -a "vtysh -c 'show bgp summary'"`).
## Второе окружение: «Облачная площадка» (anycast + sNAT, 2 канала)
Второе, независимое от площадки A окружение (та же роль `frr_anycast`, другие `group_vars`/`host_vars`): 4 узла `vr01`…`vr04`, один AS `65200`, общий anycast-адрес `172.31.255.10/32`. У каждого узла — 2 приватных интерфейса-сегмента (не point-to-point, а общий L2 с обоими пирами стороны):
- **Основной канал** — сегмент к «Основная площадка» (внешняя сторона, ASN `65104`, пиры M1/M2) — анонсы **без** as-path prepend.
- **Резервный канал** — сегмент к «Резервная площадка» (внешняя сторона, ASN `65103`, пиры B1/B2) — анонсы **с** as-path prepend (на всех 4 узлах, `AS_PATH_PREPEND_COUNT` из env, по умолчанию 3), чтобы при исправном основном канале трафик шёл через него.
- **sNAT** (nftables, masquerade) на внешнем интерфейсе — на всех 4 узлах: трафик на anycast-адрес транслируется для выхода в интернет.
- **DNAT** (nftables, `prerouting`) — на всех 4 узлах трафик на anycast-адрес (`ANYCAST_PREFIX`) сначала перенаправляется на `DNAT_TARGET_IP` (реальный backend, напр. `95.163.53.117`), и только потом, в `postrouting`, применяется sNAT-masquerade. Включается автоматически, если в env задан `DNAT_TARGET_IP` (не нужен отдельный булев флаг); имеет смысл только вместе с `enable_snat: true`, иначе `nftables.conf` не разворачивается — при нарушении playbook останавливается на pre-flight `assert`.
Все значения — в `ansible/site-cloud.env` (ASN, anycast-префикс, IP всех 4 пиров, имена интерфейсов, per-node router-id). Т.к. пиры общие для всех 4 узлов, список `bgp_neighbors` задан один раз в `group_vars/site_cloud.yml`, а не per-node.
```bash
cd ansible
SITE_ENV_FILE=site-cloud.env ./run.sh site-cloud.yml # применить
SITE_ENV_FILE=site-cloud.env ./run.sh site-cloud.yml --check --diff # сухой прогон
```
### Проверка без живых ВМ
ВМ этого окружения тоже не подняты — golden-файла для сравнения тоже нет (окружение новое), поэтому проверка через grep-инварианты вместо byte-diff:
```bash
SITE_ENV_FILE=site-cloud.env ansible/run.sh site-cloud.yml --syntax-check
ansible/tests/render-check.sh # регресс: площадка A рендерится как прежде
ansible/tests/render-check-cloud.sh # 4 соседа, prepend только на резервном канале, sNAT
```
`render-check-cloud.sh` проверяет: у каждого из 4 узлов ровно 4 `neighbor ... remote-as`, outbound `route-map` навешан на всех — `RM-OUT-PREPEND` (фильтр + `set as-path prepend 65200 65200 65200`) на 2×65103, `RM-OUT` (только фильтр) на 2×65104, `nftables.conf` содержит DNAT (`dnat to 95.163.53.117`) и `masquerade` на заданном внешнем интерфейсе, а также (если `nft` доступен на control node) валидирует синтаксис каждого `nftables.conf` через `nft -c -f`.
Детали и разбор диаграммы: [docs/2026-09-09-ansible-site-cloud-plan.md](docs/2026-09-09-ansible-site-cloud-plan.md) / [docs/2026-09-09-ansible-site-cloud-summary.md](docs/2026-09-09-ansible-site-cloud-summary.md).
## Защита от auto-update-триггеров и networkd route-flush
Прод-инцидент: `apt-daily-upgrade` обновил `libc6` → postinst вызвал рестарт зависимых юнитов → перезапустился `systemd-networkd` → он вычистил из ядра BGP-маршруты zebra (не «свои» для него) → трафик встал до ручного `systemctl restart frr`. Роль `frr_anycast` теперь защищает от этого класса сбоя безусловно, на всех узлах обеих сред:
- **Обновления только по требованию администратора**: `/etc/apt/apt.conf.d/20auto-upgrades` отключает периодические `apt update`/unattended-upgrade, дополнительно замаскированы `apt-daily.timer`/`apt-daily-upgrade.timer` и их `.service`. `apt upgrade` по-прежнему доступен вручную.
- **`systemd-networkd` не вычищает чужие маршруты**: drop-in `/etc/systemd/networkd.conf.d/99-keep-configuration.conf` (`KeepConfiguration=yes`) — защита не от конкретного пакета-триггера, а от самого механизма, на случай **любого** будущего рестарта networkd.
Не влияет на рендер `frr.conf`/`daemons`/`nftables.conf` — `render-check.sh`/`render-check-cloud.sh` проходят без изменений. Детали: [docs/2026-09-11-system-hardening-plan.md](docs/2026-09-11-system-hardening-plan.md) / [docs/2026-09-11-system-hardening-summary.md](docs/2026-09-11-system-hardening-summary.md).
## Мониторинг: Prometheus Node Exporter
На всех узлах (площадка A + «Облачная площадка») безусловно ставится `prometheus-node-exporter` из официального репозитория Ubuntu 24 LTS (`universe`, сторонний репозиторий не добавляется) — сервис `prometheus-node-exporter.service`, стандартный порт `:9100`, конфиг пакета не переопределяется. Дополнительных nftables-правил не нужно — в `nftables.conf.j2` нет `table inet filter`/policy drop, трафик к 9100 ничем не блокируется.
Детали: [docs/2026-09-11-node-exporter-plan.md](docs/2026-09-11-node-exporter-plan.md) / [docs/2026-09-11-node-exporter-summary.md](docs/2026-09-11-node-exporter-summary.md).