frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié

Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.

  route add 10.21.0.0/16 via 10.0.4.41
  route add 10.27.0.0/16 via 10.0.4.41

Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.

Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.

Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.

30 preuves OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-06 12:46:58 -04:00
parent e6c259be4d
commit f8977d2d29
2 changed files with 124 additions and 3 deletions

View file

@ -1,5 +1,72 @@
# CHANGELOG — Set-OPS
## 2026-08-06 — le chemin nord-sud devient dérivable
### L'EVPN tourne
Les commutateurs configurés, le trunk vérifié : `10.0.5.41` joint `.43` et `.47`. Le SDN
appliqué a fait basculer les tunnels — ils sortaient de **`192.168.11.41`**, la carte de
gestion, et sont maintenant sur `vlan11`. C'est ce que l'objection du 4 août visait, et ce
n'est vrai que depuis cette application.
`bgpd` et `bfdd` étaient à `no` sur les trois nœuds : FRR tournait avec `zebra` seul, donc
aucune session EVPN. Activés un nœud à la fois — `vishnu`, `gandalf`, `asgard` — avec
vérification du quorum et de la route par défaut après chacun. **Maillage complet : six
sessions établies**, 16 préfixes échangés.
Sauvegarde `/etc/frr/daemons.avant-bgpd` posée sur chaque nœud avant modification.
### Une alarme retirée
J'avais annoncé les VRF « inversés » — `vrf_t11` portant les sous-réseaux de Chezlepro. Le
noyau tranche : `10.21.16.0/24 is directly connected, t11fron` dans `vrf_t11`. Chaque VRF
porte bien ses propres sous-réseaux, par les passerelles anycast de ses VNets. Les
`ip route … null0` sont ceux des *autres* zones — et avec deux tenants, « les autres » et
« inversés » se ressemblaient exactement.
### Une brèche réelle, à filtrer avant la première VM
Une fois les nœuds de sortie actifs, une VM tenant atteint **tous les réseaux directement
connectés sur son propre hyperviseur** — gestion Proxmox, stockage iSCSI, Ceph, et le
transport VXLAN lui-même. Mesuré avant la suppression de la VM d'essai.
Ce n'est pas un défaut de configuration : c'est le prix du routage inter-VRF. Une route
connectée l'emporte sur la route par défaut, donc **déplacer celle-ci (D-57) n'y suffira
pas**. Il faut une règle « supernet tenant → réseaux de l'hyperviseur : DROP », que
`make devis-proxmox-fw` sait déjà dériver.
**Déclencheur : avant la première VM tenant.** À ce stade — plateforme en construction,
zéro locataire — c'est un chantier, pas un incident.
### La frontière : identifiant vérifié, nœud de sortie dérivé
La clé d'API fonctionne (OPNsense 26.1.2_5). L'API des règles donne elle-même la liste des
identifiants qu'elle accepte :
```
lan → « GESTION » opt1 → « TENANTS » wan → « WAN »
```
Ni le périphérique (`vlan040`), ni le libellé. **L'intrant `opnsense_if_transit: opt1`
était juste** — la question ouverte depuis deux jours est tranchée par la mesure.
Le prochain saut des routes tenants ne s'écrit plus `<NOEUD-DE-SORTIE-EVPN>` : il dérive.
Deux déclarations doivent concorder, et c'est voulu — l'hébergeur **nomme** le nœud
(`proxmox_sdn.sortie_primaire`, propriété du cluster), l'underlay dit **son adresse sur le
lien de frontière**. Nommer un nœud absent du lien rend le devis muet plutôt que faux.
```
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
```
Le devis explique pourquoi cette adresse-là, et pourquoi **un seul** saut : une route
statique n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique — la réponse reviendrait par une interface où l'état
n'a pas été créé.
30 preuves OK.
## 2026-08-04 (suite 4) — le numéro est l'adresse
Les deux commutateurs deviennent **`bifrost-3`** et **`bifrost-4`**, avec leurs adresses

View file

@ -24,6 +24,7 @@ Usage :
from __future__ import annotations
import argparse
import os
import ipaddress
import json
import sys
@ -123,13 +124,47 @@ def prochain_saut(transit: dict | None) -> str:
fois dans l'underlay) ouvrirait la porte a deux valeurs contradictoires — c'est
exactement le mode de panne qu'on ferme. Marqueur tant qu'aucun transit n'existe.
"""
if underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn":
underlay = underlay_mod.charger()
if underlay_mod.routage_tenants(underlay) == "sdn":
# En EVPN, le commutateur ne route plus les tenants : le voisin de la frontiere
# est un NOEUD DE SORTIE (hyperviseur), pas le SVI. Emettre le SVI produirait
# des routes vers un equipement qui n'a aucun chemin vers le tenant.
return NOEUD_SORTIE
#
# C'est le noeud PRIMAIRE, et son adresse SUR LE LIEN DE TRANSIT — pas son
# adresse d'underlay ni de gestion : la frontiere n'atteint que celle-la.
# Une seule route statique par tenant, donc un seul saut : deux noeuds actifs
# en sortie avec une seule route en entree donneraient un chemin asymetrique.
return adresse_noeud_de_sortie(underlay, transit) or NOEUD_SORTIE
return str((transit or {}).get("passerelle") or "").strip() or PROCHAIN_SAUT
def adresse_noeud_de_sortie(underlay: dict | None, transit: dict | None) -> str:
"""Adresse du noeud de sortie PRIMAIRE sur le lien de frontiere. '' si indeterminee.
Deux declarations doivent concorder, et c'est voulu : l'hebergeur nomme le noeud
(`proxmox_sdn.sortie_primaire`, propriete du cluster), l'underlay dit son adresse
sur ce lien. Nommer un noeud absent du lien rend le devis muet plutot que faux.
"""
primaire = str((_sdn_hebergeur() or {}).get("sortie_primaire") or "").strip()
if not primaire or not transit:
return ""
for h in underlay_mod.hotes(underlay):
if (h.get("nom") == primaire and h.get("reseau") == transit.get("nom")
and h.get("ip")):
return str(h["ip"])
return ""
def _sdn_hebergeur() -> dict:
"""`proxmox_sdn` du depot de l'HEBERGEUR — meme derivation que devis_sdn (D-35)."""
lien = Path(os.environ.get("SETOPS_UNDERLAY") or (RACINE / "underlay.yml"))
if not lien.exists():
return {}
fichier = lien.resolve().parent / "proxmox-hebergeur.yml"
if not fichier.is_file():
return {}
return (yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}).get("proxmox_sdn") or {}
# Le socle est porte par TOUS les hotes : sa destination n'est pas une poignee
# de machines mais le supernet du tenant. Evite un alias de 14 adresses.
ROLE_SOCLE = "serveur_debian"
@ -300,6 +335,10 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
for nom, _pfx, n in tenants
]
# Adresse du noeud de sortie primaire sur le lien de frontiere, calculee une fois :
# elle sert au prochain saut des routes ET au commentaire qui l'explique.
_ip_sortie = adresse_noeud_de_sortie(underlay_mod.charger(), transit)
return {
"role_frontiere": "nord-sud",
"instance_active": nom_actif,
@ -315,6 +354,9 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
"tenants_sans_inventaire": tenants_sans_inventaire,
"tenants_sans_admin": sorted(set(tenants_sans_admin)),
"admin_prives": admin_prives,
"noeud_sortie": ({"nom": str((_sdn_hebergeur() or {}).get("sortie_primaire") or ""),
"ip": _ip_sortie}
if _ip_sortie else None),
"transit": {
"nom": (transit or {}).get("nom"),
"vlan": (transit or {}).get("vlan"),
@ -410,7 +452,19 @@ def rendre(devis: dict) -> str:
"# /!\\ LE PROCHAIN SAUT N'EST PAS LE COMMUTATEUR. En SDN EVPN, il ne route plus",
"# les tenants : une route pointee vers son SVI arriverait sur un equipement sans",
"# chemin vers le tenant. Le voisin de la frontiere est un NOEUD DE SORTIE — un",
"# hyperviseur qui porte la sortie du VRF. A figer apres le spike EVPN.",
"# hyperviseur qui porte la sortie du VRF.",
"# A DECLARER : `proxmox_sdn.sortie_primaire` chez l'hebergeur, et l'adresse de",
"# ce noeud sur le lien de transit dans underlay.yml. Les deux doivent concorder.",
]
elif devis.get("noeud_sortie"):
out += [
f"# Prochain saut = {devis['noeud_sortie']['nom']}, noeud de sortie PRIMAIRE, a son",
f"# adresse SUR LE LIEN DE FRONTIERE ({devis['noeud_sortie']['ip']}) — ni son underlay,",
"# ni sa gestion : la frontiere n'atteint que celle-la.",
"#",
"# Un seul saut, parce qu'une route statique n'en porte qu'un. Deux noeuds actifs",
"# en sortie avec une seule route en entree donneraient un chemin asymetrique : la",
"# reponse reviendrait par une interface ou l'etat n'a pas ete cree, et serait jetee.",
]
out += [
]