frontière : le prochain saut dérive du transit, il n'est plus saisi

`opnsense_prochain_saut` quitte le panneau Intrants. Il dérive du réseau de
transit de l'underlay — la `passerelle` du réseau portant `passerelle_sortie`.

Un seul bloc alimente les deux devis : 10.0.4.1 est le SVI côté switch ET le
prochain saut des routes tenants côté frontière ; 10.0.4.2 est la route par
défaut du switch. Le saisir en doublon rouvrait la possibilité de deux
valeurs contradictoires pour un seul lien — le mode de panne qu'on venait de
fermer pour les réseaux d'administration.

Le devis frontière expose le bloc `transit` (JSON compris) et cesse de
réclamer en section 0 les routes que `devis-reseau` émet désormais : il dit
lesquelles sont déjà émises, ou signale l'absence de transit déclaré. Sans
underlay, le marqueur revient — le repli reste explicite.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-01 19:38:17 -04:00
parent 3698152b6a
commit 069e549160
4 changed files with 75 additions and 6 deletions

View file

@ -1,5 +1,24 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-08-01 (suite) — le prochain saut dérive du transit
`opnsense_prochain_saut` n'est plus un intrant du panneau : il **dérive** du réseau de
transit de l'underlay (la `passerelle` du réseau portant `passerelle_sortie`).
Un seul bloc de six lignes alimente désormais les deux devis : `10.0.4.1` devient le SVI
côté switch **et** le prochain saut des routes tenants côté frontière ; `10.0.4.2` devient
la route par défaut du switch. Le saisir en doublon dans le panneau rouvrait la possibilité
de deux valeurs contradictoires pour un seul lien — précisément le mode de panne qu'on venait
de fermer pour les réseaux d'administration.
Le devis frontière gagne au passage un bloc `transit` (JSON compris) et cesse de réclamer en
section 0 des routes que `make devis-reseau` émet maintenant : il dit lesquelles sont **déjà
émises**, ou signale l'absence de transit déclaré. Sans underlay, le marqueur
`<PROCHAIN-SAUT-SWITCH>` revient — le repli reste explicite.
Reste un seul intrant à figer au câblage : `opnsense_if_transit`, le nom de l'interface qui
porte le VLAN 40 sur le boîtier — la seule valeur que rien ne peut deviner.
## 2026-08-01 — le lien de transit et les deux routes (la boucle est fermée) ## 2026-08-01 — le lien de transit et les deux routes (la boucle est fermée)
### Ajouté — réseau de transit dans l'underlay (clé `passerelle_sortie`) ### Ajouté — réseau de transit dans l'underlay (clé `passerelle_sortie`)

View file

@ -77,6 +77,12 @@ Un `/29` plutôt qu'un `/30` : pendant une transition, deux pare-feux cohabitent
et un `/30` n'offre que deux adresses. `make underlay` (**preuve P23**) refuse une sortie hors et un `/30` n'offre que deux adresses. `make underlay` (**preuve P23**) refuse une sortie hors
du lien, confondue avec le SVI, sans SVI, ou déclarée deux fois. du lien, confondue avec le SVI, sans SVI, ou déclarée deux fois.
Ce seul bloc alimente **les deux devis** : le `passerelle` (`10.0.4.1`) devient le SVI côté
switch *et* le prochain saut des routes tenants côté frontière ; le `passerelle_sortie`
(`10.0.4.2`) devient la route par défaut du switch. Le prochain saut n'est donc **pas** un
intrant du panneau — le saisir en doublon rouvrirait la possibilité de deux valeurs
contradictoires pour un seul et même lien.
### Les deux routes, et pourquoi il en faut deux ### Les deux routes, et pourquoi il en faut deux
`make devis-reseau` émet alors, en section 5 : `make devis-reseau` émet alors, en section 5 :
@ -155,9 +161,10 @@ ansible-vault edit instance/inventories/principal/group_vars/all/vault.yml
## 6. Ce qui reste ouvert ## 6. Ce qui reste ouvert
- **Le câblage** — le lien de transit est *décidé* (VLAN 40, `10.0.4.0/29`) et le devis - **Le câblage** — le lien de transit est *décidé* (VLAN 40, `10.0.4.0/29`) et le devis
switch émet déjà son SVI et ses routes. Restent à figer, une fois le boîtier raccordé, les switch émet déjà son SVI et ses routes. Reste à figer, une fois le boîtier raccordé, un
deux intrants côté frontière : `opnsense_if_transit` (l'interface qui porte le VLAN 40) et seul intrant : `opnsense_if_transit`, le nom de l'interface qui porte le VLAN 40 sur le
`opnsense_prochain_saut` (`10.0.4.1`). boîtier — la seule valeur que rien ne peut deviner. Le prochain saut, lui, **dérive** du
transit (voir §4).
- **Le second VPN** — pendant la transition, le VPN d'administration actuel (pfSense) reste - **Le second VPN** — pendant la transition, le VPN d'administration actuel (pfSense) reste
en service et OPNsense en montera un second. Son sous-réseau devra être **ajouté à en service et OPNsense en montera un second. Son sous-réseau devra être **ajouté à
`nftables_admin_ssh`**, sans quoi il sera muet de la même façon. `nftables_admin_ssh`**, sans quoi il sera muet de la même façon.

View file

@ -47,6 +47,9 @@ from resoudre_flux import ( # noqa: E402
_sources_admin_ssh, _sources_admin_ssh,
) )
from devis_reseau import decouvrir, prefixe # noqa: E402 from devis_reseau import decouvrir, prefixe # noqa: E402
# Le lien de transit (et donc le prochain saut) vit dans l'underlay : il est PARTAGE
# par tous les tenants, il ne peut deriver d'aucun index. Source unique, pas un intrant.
import underlay as underlay_mod # noqa: E402
# Marqueurs de repli, tant que les intrants de frontiere ne sont pas renseignes # Marqueurs de repli, tant que les intrants de frontiere ne sont pas renseignes
# (meme idiome que devis_reseau). Le panneau « Intrants de base » du GUI les ecrit # (meme idiome que devis_reseau). Le panneau « Intrants de base » du GUI les ecrit
@ -70,6 +73,22 @@ def _ou_marqueur(intrants: dict, cle: str, marqueur: str) -> str:
valeur = str(intrants.get(cle) or "").strip() valeur = str(intrants.get(cle) or "").strip()
return valeur or marqueur return valeur or marqueur
def transit_underlay() -> dict | None:
"""Le reseau de transit declare dans l'underlay, ou None."""
return underlay_mod.reseau_transit(underlay_mod.charger())
def prochain_saut(transit: dict | None) -> str:
"""Prochain saut des routes tenants = le SVI du switch sur le lien de transit.
DERIVE, jamais saisi : c'est le meme lien que celui d'ou `devis_reseau` tire la
route par defaut et les routes de retour. Le declarer deux fois (une fois ici, une
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.
"""
return str((transit or {}).get("passerelle") or "").strip() or PROCHAIN_SAUT
# Le socle est porte par TOUS les hotes : sa destination n'est pas une poignee # 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. # de machines mais le supernet du tenant. Evite un alias de 14 adresses.
ROLE_SOCLE = "serveur_debian" ROLE_SOCLE = "serveur_debian"
@ -177,7 +196,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
}) })
intrants = intrants_frontiere() intrants = intrants_frontiere()
saut = _ou_marqueur(intrants, "opnsense_prochain_saut", PROCHAIN_SAUT) transit = transit_underlay()
saut = prochain_saut(transit)
routes = [ routes = [
{ {
"reseau": supernet_de(n["index"]), "reseau": supernet_de(n["index"]),
@ -198,6 +218,13 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
"routes": routes, "routes": routes,
"regles": regles, "regles": regles,
"admin": admin, "admin": admin,
"transit": {
"nom": (transit or {}).get("nom"),
"vlan": (transit or {}).get("vlan"),
"sous_reseau": (transit or {}).get("sous_reseau"),
"svi_switch": saut,
"adresse_frontiere": (transit or {}).get("passerelle_sortie"),
} if transit else None,
"reciproque_switch": [ "reciproque_switch": [
f"ip route {cidr} -> {saut} (retour vers l'administration)" f"ip route {cidr} -> {saut} (retour vers l'administration)"
for cidr in admin for cidr in admin
@ -214,17 +241,31 @@ def rendre(devis: dict) -> str:
"# Les passerelles de zone restent sur les switches L3 : aucun SVI ici.", "# Les passerelles de zone restent sur les switches L3 : aucun SVI ici.",
"# ============================================================", "# ============================================================",
"", "",
"## 0. Prerequis reciproques — A FAIRE SUR LES SWITCHES", "## 0. Prerequis reciproques — COTE SWITCHES",
"# Sans ces routes de retour, le trafic part mais ne revient pas : la", "# Sans ces routes de retour, le trafic part mais ne revient pas : la",
"# reponse sort par une autre interface et le pare-feu la jette (hors etat).", "# reponse sort par une autre interface et le pare-feu la jette (hors etat).",
] ]
out += [f"# {r}" for r in devis["reciproque_switch"]] or ["# (aucun reseau d'administration declare)"] out += [f"# {r}" for r in devis["reciproque_switch"]] or ["# (aucun reseau d'administration declare)"]
tr = devis.get("transit")
if tr:
out.append(f"# DEJA EMIS par `make devis-reseau` (section 5), derive du transit"
f" '{tr['nom']}' de l'underlay.")
else:
out.append("# A EMETTRE : aucun reseau de transit n'est declare dans underlay.yml")
out.append("# (cle `passerelle_sortie`). Sans lui, ni sortie ni retour ne sont derivables.")
out += [ out += [
"", "",
"## 1. Interfaces", "## 1. Interfaces",
f"# Gestion : {devis['api_url'] or '(intrant opnsense_api_url a renseigner)'}", f"# Gestion : {devis['api_url'] or '(intrant opnsense_api_url a renseigner)'}",
f"# WAN : {devis['if_wan']}", f"# WAN : {devis['if_wan']}",
f"# TRANSIT : {devis['if_transit']} (trunk 802.1Q vers les switches)", f"# TRANSIT : {devis['if_transit']} (trunk 802.1Q vers les switches)",
]
if tr:
out += [
f"# lien '{tr['nom']}' {tr['sous_reseau']} — VLAN {tr['vlan']} a etiqueter",
f"# switch {tr['svi_switch']} <-> frontiere {tr['adresse_frontiere']}",
]
out += [
"", "",
"## 2. Routes statiques (vers les supernets tenants)", "## 2. Routes statiques (vers les supernets tenants)",
] ]

View file

@ -115,7 +115,9 @@ INTRANTS_SCHEMA = [
("opnsense_api_verifier_certs", "frontiere", "defaut", "Frontière", "Valider le certificat TLS de la frontière", "bool"), ("opnsense_api_verifier_certs", "frontiere", "defaut", "Frontière", "Valider le certificat TLS de la frontière", "bool"),
("opnsense_if_wan", "frontiere", "defaut", "Frontière", "Interface WAN", "str"), ("opnsense_if_wan", "frontiere", "defaut", "Frontière", "Interface WAN", "str"),
("opnsense_if_transit", "frontiere", "defaut", "Frontière", "Interface/trunk vers les switches (transit)", "str"), ("opnsense_if_transit", "frontiere", "defaut", "Frontière", "Interface/trunk vers les switches (transit)", "str"),
("opnsense_prochain_saut", "frontiere", "defaut", "Frontière", "IP du switch L3 qui porte les SVI (prochain saut des routes tenants)", "str"), # `opnsense_prochain_saut` n'est PLUS un intrant : il derive du reseau de transit de
# l'underlay (`passerelle` du reseau portant `passerelle_sortie`). Le saisir ici en
# doublon ouvrirait la porte a deux valeurs contradictoires pour un seul lien.
] ]
# Cles secretes : interdites a l'ecriture par le GUI (garde-fou). # Cles secretes : interdites a l'ecriture par le GUI (garde-fou).
INTRANTS_CLES_INTERDITES = { INTRANTS_CLES_INTERDITES = {