Add DNAT anycast -> backend in frr_anycast nftables config

Трафик на anycast-адрес (172.31.255.10) теперь сначала DNAT'ится на
реальный backend (DNAT_TARGET_IP=95.163.53.117 для site-cloud) в
prerouting, и только потом проходит уже существующий sNAT/masquerade
в postrouting — обе цепочки в одной table inet nat, порядок хуков
гарантирует правильную последовательность.

DNAT включается автоматически по факту заданного dnat_target_ip (без
отдельного булева флага), выключен по умолчанию — площадка A не
затронута. Pre-flight assert не даёт задать dnat_target_ip без
enable_snat (иначе nftables.conf вообще не разворачивается).

render-check-cloud.sh дополнен проверкой DNAT-правила и валидацией
синтаксиса каждого nftables.conf через `nft -c -f`.

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-10 17:25:09 +03:00
1 parent b2b7306a3c
commit 1e287ee81c
9 files changed
+88 -2

No files matched your search

+2 -1
View File
@@ -140,6 +140,7 @@ ansible/tests/render-check.sh # golden-diff: рендер frr.conf
- **Основной канал** — сегмент к «Основная площадка» (внешняя сторона, 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.
@@ -159,6 +160,6 @@ ansible/tests/render-check.sh # регресс: площадка A ре
ansible/tests/render-check-cloud.sh # 4 соседа, prepend только на резервном канале, sNAT
```
`render-check-cloud.sh` проверяет: у каждого из 4 узлов ровно 4 `neighbor ... remote-as` (2×65104 без route-map, 2×65103 с `route-map RM-PREPEND-OUT out`), `route-map` содержит `set as-path prepend 65200 65200 65200`, `nftables.conf` содержит `masquerade` на заданном внешнем интерфейсе.
`render-check-cloud.sh` проверяет: у каждого из 4 узлов ровно 4 `neighbor ... remote-as` (2×65104 без route-map, 2×65103 с `route-map RM-PREPEND-OUT out`), `route-map` содержит `set as-path prepend 65200 65200 65200`, `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).
+4
View File
@@ -12,6 +12,10 @@ frr_apt_series: "{{ lookup('env', 'FRR_APT_SERIES') | default('frr-stable', true
enable_snat: true
as_path_prepend_count: "{{ lookup('env', 'AS_PATH_PREPEND_COUNT') }}"
# DNAT anycast -> dnat_target_ip. Задан DNAT_TARGET_IP — DNAT включается
# автоматически (см. roles/frr_anycast/defaults/main.yml).
dnat_target_ip: "{{ lookup('env', 'DNAT_TARGET_IP') | default('', true) }}"
# M1/M2/B1/B2 сидят на общих для всех 4 узлов L2-сегментах, поэтому список
# соседей — один на всё окружение, а не per-node (см. host_vars/vr0N.yml).
bgp_neighbors:
@@ -19,3 +19,9 @@ netplan_dummy_config_path: /etc/netplan/90-anycast-dummy0.yaml
as_path_prepend_count: 0 # >0 включает route-map с этим числом prepend'ов
enable_snat: false # true включает nftables-задачи и sysctl ip_forward
nftables_conf_path: /etc/nftables.conf
# DNAT anycast -> dnat_target_ip, применяется в prerouting ДО srcnat-masquerade
# из enable_snat. Работает только когда enable_snat: true (иначе nftables.conf
# вообще не разворачивается). Включается автоматически, как только задан
# dnat_target_ip — отдельного булева флага в env не требуется.
dnat_target_ip: ""
+12 -1
View File
@@ -23,6 +23,17 @@
success_msg: "Внешний интерфейс {{ ext_iface }} на месте."
when: enable_snat | bool
- name: Pre-flight | dnat_target_ip задан только вместе с enable_snat
ansible.builtin.assert:
that:
- enable_snat | bool
fail_msg: >-
dnat_target_ip задан ({{ dnat_target_ip }}), но enable_snat: false —
nftables.conf в этом случае вообще не разворачивается, DNAT-правило
применено не будет. Включите enable_snat: true.
success_msg: "DNAT на {{ dnat_target_ip }} будет применён (enable_snat: true)."
when: dnat_target_ip | length > 0
- name: FRR APT-репозиторий | добавить GPG-ключ
ansible.builtin.get_url:
url: https://deb.frrouting.org/frr/keys.gpg
@@ -93,7 +104,7 @@
state: present
when: enable_snat | bool
- name: sNAT | конфиг /etc/nftables.conf (masquerade на внешнем интерфейсе)
- name: sNAT | конфиг /etc/nftables.conf (DNAT anycast + masquerade на внешнем интерфейсе)
ansible.builtin.template:
src: nftables.conf.j2
dest: "{{ nftables_conf_path }}"
@@ -2,6 +2,12 @@
flush ruleset
table inet nat {
{% if dnat_target_ip %}
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
ip daddr {{ anycast_prefix.split('/')[0] }} dnat to {{ dnat_target_ip }}
}
{% endif %}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "{{ ext_iface }}" masquerade
+1
View File
@@ -22,6 +22,7 @@ PRIV_MAIN_IFACE=ens7 # сегмент к "Основная площадка
PRIV_BACKUP_IFACE=ens8 # сегмент к "Резервная площадка" (B1/B2), розовый/резервный канал
EXT_IFACE=ens9 # внешний интерфейс — интернет-egress для sNAT
FRR_APT_SERIES=frr-8 # ветка репозитория deb.frrouting.org
DNAT_TARGET_IP=95.163.53.117 # DNAT: трафик на ANYCAST_PREFIX -> этот адрес (до masquerade)
# SSH/подключение (пользователь и ключ, которые Terraform выдал ВМ при создании)
SSH_USER=ansible
+11
View File
@@ -53,6 +53,17 @@ for n in vr01 vr02 vr03 vr04; do
nft="$OUT_DIR/$n/nftables.conf"
check "$n nftables masquerade" "$nft" "masquerade" 1
check "$n nftables oifname" "$nft" "oifname \"$EXT_IFACE\"" 1
check "$n nftables dnat" "$nft" "ip daddr 172.31.255.10 dnat to $DNAT_TARGET_IP" 1
if command -v nft >/dev/null 2>&1; then
if nft -c -f "$nft" >/tmp/render-check-cloud-nft.err 2>&1; then
echo "OK $n nftables.conf: синтаксис валиден (nft -c)"
else
echo "FAIL $n nftables.conf: nft -c провалился:" >&2
cat /tmp/render-check-cloud-nft.err >&2
STATUS=1
fi
fi
done
exit "$STATUS"
+20
View File
@@ -0,0 +1,20 @@
# План внедрения: DNAT anycast → 95.163.53.117 в роли frr_anycast
**Дата:** 2026-09-10
## Цель
Добавить в роль `frr_anycast` опциональную DNAT-трансляцию: трафик на anycast-адрес перенаправляется на `95.163.53.117`, до уже существующей SNAT/masquerade в `postrouting` (см. обсуждение в чате).
## Подход
- `table inet nat` дополняется цепочкой `prerouting` (`hook prerouting priority dstnat`) с правилом `ip daddr <anycast-host> dnat to <dnat_target_ip>` — рендерится только когда `dnat_target_ip` задан.
- `enable_dnat`/`dnat_target_ip` — новые опциональные переменные роли (дефолт выключено), выводятся из `DNAT_TARGET_IP` в env-файле: если задан — DNAT включается автоматически, отдельный булев флаг в env не нужен.
- DNAT имеет смысл только при уже включённом `enable_snat` (иначе `nftables.conf` вообще не разворачивается) — добавляется pre-flight `assert`.
- Применяется в окружении `site-cloud` (`DNAT_TARGET_IP=95.163.53.117` в `site-cloud.env`), площадка A не затрагивается (дефолт выключен).
## Definition of done
- `nftables.conf.j2` рендерит `prerouting`-цепочку с DNAT при заданном `dnat_target_ip`, без него — не рендерит (площадка A не меняется, у неё `enable_snat: false`).
- `render-check-cloud.sh` подтверждает: `dnat to 95.163.53.117` присутствует у всех 4 узлов, синтаксис `nft -c` валиден.
- README и summary обновлены.
+26
View File
@@ -0,0 +1,26 @@
# Summary: DNAT anycast → 95.163.53.117 в роли frr_anycast
**Дата:** 2026-09-10
**План:** [2026-09-10-nftables-dnat-plan.md](2026-09-10-nftables-dnat-plan.md)
## Что сделано
- `templates/nftables.conf.j2`: добавлена цепочка `prerouting` (`hook prerouting priority dstnat`) с `ip daddr <anycast-host> dnat to {{ dnat_target_ip }}`, рендерится только при заданном `dnat_target_ip`. Отрабатывает до уже существующей `postrouting`/masquerade (порядок гарантирован хуками nftables, явных зависимостей не требуется).
- `defaults/main.yml`: новая переменная `dnat_target_ip: ""` (по умолчанию выключено, площадка A не затронута).
- `tasks/main.yml`: pre-flight `assert` — если `dnat_target_ip` задан, а `enable_snat: false`, playbook останавливается с понятной ошибкой (иначе `nftables.conf` вообще не разворачивается и DNAT молча не применится).
- `site-cloud.env` / `group_vars/site_cloud.yml`: `DNAT_TARGET_IP=95.163.53.117` — DNAT включается автоматически по факту наличия значения, отдельный булев флаг не нужен.
- `render-check-cloud.sh`: добавлена проверка `dnat to 95.163.53.117` на всех 4 узлах + валидация синтаксиса каждого `nftables.conf` через `nft -c -f` (если `nft` доступен на control node).
## Результаты проверки
- `render-check.sh` (площадка A) — 6/6 OK, без изменений (роль не переиспользует `dnat_target_ip`, когда он пуст).
- `render-check-cloud.sh` — все проверки пройдены, включая новую DNAT-проверку и `nft -c` для всех 4 узлов (`vr01`–`vr04`).
- `--syntax-check` — OK для обоих playbook'ов.
## Изменённые файлы
- `ansible/roles/frr_anycast/{defaults/main.yml,tasks/main.yml,templates/nftables.conf.j2}`.
- `ansible/site-cloud.env`, `ansible/group_vars/site_cloud.yml`.
- `ansible/tests/render-check-cloud.sh`.
- `README.md` — раздел «Второе окружение» дополнен описанием DNAT.
- `docs/2026-09-10-nftables-dnat-plan.md`, `docs/2026-09-10-nftables-dnat-summary.md`.