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
## 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)
### 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
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
`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
- **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
deux intrants côté frontière : `opnsense_if_transit` (l'interface qui porte le VLAN 40) et
`opnsense_prochain_saut` (`10.0.4.1`).
switch émet déjà son SVI et ses routes. Reste à figer, une fois le boîtier raccordé, un
seul intrant : `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. Le prochain saut, lui, **dérive** du
transit (voir §4).
- **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é à
`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,
)
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
# (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()
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
# de machines mais le supernet du tenant. Evite un alias de 14 adresses.
ROLE_SOCLE = "serveur_debian"
@ -177,7 +196,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
})
intrants = intrants_frontiere()
saut = _ou_marqueur(intrants, "opnsense_prochain_saut", PROCHAIN_SAUT)
transit = transit_underlay()
saut = prochain_saut(transit)
routes = [
{
"reseau": supernet_de(n["index"]),
@ -198,6 +218,13 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
"routes": routes,
"regles": regles,
"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": [
f"ip route {cidr} -> {saut} (retour vers l'administration)"
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.",
"# ============================================================",
"",
"## 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",
"# 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)"]
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 += [
"",
"## 1. Interfaces",
f"# Gestion : {devis['api_url'] or '(intrant opnsense_api_url a renseigner)'}",
f"# WAN : {devis['if_wan']}",
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)",
]

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_if_wan", "frontiere", "defaut", "Frontière", "Interface WAN", "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).
INTRANTS_CLES_INTERDITES = {