diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9b8bb..f28bf90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` : 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 diff --git a/scripts/devis_opnsense.py b/scripts/devis_opnsense.py index b20a55e..f44d046 100644 --- a/scripts/devis_opnsense.py +++ b/scripts/devis_opnsense.py @@ -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 += [ ]