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:
1 parent
b2b7306a3c
commit
1e287ee81c
9 files changed
+88
-2
No files matched your search
@@ -140,6 +140,7 @@ ansible/tests/render-check.sh # golden-diff: рендер frr.conf
|
|||||||
- **Основной канал** — сегмент к «Основная площадка» (внешняя сторона, ASN `65104`, пиры M1/M2) — анонсы **без** as-path prepend.
|
- **Основной канал** — сегмент к «Основная площадка» (внешняя сторона, ASN `65104`, пиры M1/M2) — анонсы **без** as-path prepend.
|
||||||
- **Резервный канал** — сегмент к «Резервная площадка» (внешняя сторона, ASN `65103`, пиры B1/B2) — анонсы **с** as-path prepend (на всех 4 узлах, `AS_PATH_PREPEND_COUNT` из env, по умолчанию 3), чтобы при исправном основном канале трафик шёл через него.
|
- **Резервный канал** — сегмент к «Резервная площадка» (внешняя сторона, ASN `65103`, пиры B1/B2) — анонсы **с** as-path prepend (на всех 4 узлах, `AS_PATH_PREPEND_COUNT` из env, по умолчанию 3), чтобы при исправном основном канале трафик шёл через него.
|
||||||
- **sNAT** (nftables, masquerade) на внешнем интерфейсе — на всех 4 узлах: трафик на anycast-адрес транслируется для выхода в интернет.
|
- **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.
|
Все значения — в `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
|
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).
|
Детали и разбор диаграммы: [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).
|
||||||
@@ -12,6 +12,10 @@ frr_apt_series: "{{ lookup('env', 'FRR_APT_SERIES') | default('frr-stable', true
|
|||||||
enable_snat: true
|
enable_snat: true
|
||||||
as_path_prepend_count: "{{ lookup('env', 'AS_PATH_PREPEND_COUNT') }}"
|
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-сегментах, поэтому список
|
# M1/M2/B1/B2 сидят на общих для всех 4 узлов L2-сегментах, поэтому список
|
||||||
# соседей — один на всё окружение, а не per-node (см. host_vars/vr0N.yml).
|
# соседей — один на всё окружение, а не per-node (см. host_vars/vr0N.yml).
|
||||||
bgp_neighbors:
|
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'ов
|
as_path_prepend_count: 0 # >0 включает route-map с этим числом prepend'ов
|
||||||
enable_snat: false # true включает nftables-задачи и sysctl ip_forward
|
enable_snat: false # true включает nftables-задачи и sysctl ip_forward
|
||||||
nftables_conf_path: /etc/nftables.conf
|
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: ""
|
||||||
@@ -23,6 +23,17 @@
|
|||||||
success_msg: "Внешний интерфейс {{ ext_iface }} на месте."
|
success_msg: "Внешний интерфейс {{ ext_iface }} на месте."
|
||||||
when: enable_snat | bool
|
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-ключ
|
- name: FRR APT-репозиторий | добавить GPG-ключ
|
||||||
ansible.builtin.get_url:
|
ansible.builtin.get_url:
|
||||||
url: https://deb.frrouting.org/frr/keys.gpg
|
url: https://deb.frrouting.org/frr/keys.gpg
|
||||||
@@ -93,7 +104,7 @@
|
|||||||
state: present
|
state: present
|
||||||
when: enable_snat | bool
|
when: enable_snat | bool
|
||||||
|
|
||||||
- name: sNAT | конфиг /etc/nftables.conf (masquerade на внешнем интерфейсе)
|
- name: sNAT | конфиг /etc/nftables.conf (DNAT anycast + masquerade на внешнем интерфейсе)
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: nftables.conf.j2
|
src: nftables.conf.j2
|
||||||
dest: "{{ nftables_conf_path }}"
|
dest: "{{ nftables_conf_path }}"
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
flush ruleset
|
flush ruleset
|
||||||
|
|
||||||
table inet nat {
|
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 {
|
chain postrouting {
|
||||||
type nat hook postrouting priority srcnat; policy accept;
|
type nat hook postrouting priority srcnat; policy accept;
|
||||||
oifname "{{ ext_iface }}" masquerade
|
oifname "{{ ext_iface }}" masquerade
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ PRIV_MAIN_IFACE=ens7 # сегмент к "Основная площадка
|
|||||||
PRIV_BACKUP_IFACE=ens8 # сегмент к "Резервная площадка" (B1/B2), розовый/резервный канал
|
PRIV_BACKUP_IFACE=ens8 # сегмент к "Резервная площадка" (B1/B2), розовый/резервный канал
|
||||||
EXT_IFACE=ens9 # внешний интерфейс — интернет-egress для sNAT
|
EXT_IFACE=ens9 # внешний интерфейс — интернет-egress для sNAT
|
||||||
FRR_APT_SERIES=frr-8 # ветка репозитория deb.frrouting.org
|
FRR_APT_SERIES=frr-8 # ветка репозитория deb.frrouting.org
|
||||||
|
DNAT_TARGET_IP=95.163.53.117 # DNAT: трафик на ANYCAST_PREFIX -> этот адрес (до masquerade)
|
||||||
|
|
||||||
# SSH/подключение (пользователь и ключ, которые Terraform выдал ВМ при создании)
|
# SSH/подключение (пользователь и ключ, которые Terraform выдал ВМ при создании)
|
||||||
SSH_USER=ansible
|
SSH_USER=ansible
|
||||||
|
|||||||
@@ -53,6 +53,17 @@ for n in vr01 vr02 vr03 vr04; do
|
|||||||
nft="$OUT_DIR/$n/nftables.conf"
|
nft="$OUT_DIR/$n/nftables.conf"
|
||||||
check "$n nftables masquerade" "$nft" "masquerade" 1
|
check "$n nftables masquerade" "$nft" "masquerade" 1
|
||||||
check "$n nftables oifname" "$nft" "oifname \"$EXT_IFACE\"" 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
|
done
|
||||||
|
|
||||||
exit "$STATUS"
|
exit "$STATUS"
|
||||||
@@ -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 обновлены.
|
||||||
@@ -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`.
|
||||||
Reference in new issue
Block a user