frontiere : ne declarer que ce qui existe, et reconcilier les routes
Retire les deux routes /16 (les douze /24 attribues suffisent) et retrecit les alias SETOPS_TENANT_* du supernet aux memes /24 : nos propres regles autorisaient jusqu'ici « admin -> tout le /16:22 ». Ajoute la garde qui l'aurait attrape : verifier() exige que l'ensemble des reseaux routes et l'ensemble des reseaux autorises coincident exactement. Attachee a P24, qui ne verifiait que la traduction NAT. Fait entrer les routes dans appliquer_opnsense.py — elles etaient posees a la main, donc reconciliees par rien : identite portee par la description (setopsroute:<tenant>:<reseau>-><saut>), creation avant retrait, perimetre strict. Le nom de passerelle est resolu depuis l'adresse du prochain saut. Le symptome du connect() qui aboutit toujours subsiste et n'est pas de notre fait : l'etat pf porte la regle d'usine « Default allow LAN to any rule ». Mesure qui tranche : depuis une VM du tenant, 172.31.99.99 « s'etablit » en 1 ms sans rendre de banniere. Verifie : frontiere-plan sans ecart (12 routes), flotte 14/14, prouver.py 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
df05d90059
commit
3ef470be76
4 changed files with 182 additions and 14 deletions
43
CHANGELOG.md
43
CHANGELOG.md
|
|
@ -1,5 +1,48 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-08-09 — La frontière ne déclare plus que ce qui existe, et l'applicateur possède enfin ses routes
|
||||||
|
|
||||||
|
Suite directe de l'enquête ci-dessous, menée jusqu'au bout à l'instrument plutôt qu'à
|
||||||
|
l'hypothèse. Trois corrections, dans l'ordre où la mesure les a imposées.
|
||||||
|
|
||||||
|
**1. Retrait des deux routes `/16` de la frontière.** Les douze `/24` réellement attribués
|
||||||
|
étant en place, les supernets ne servaient plus qu'à envoyer vers le nœud de sortie des
|
||||||
|
destinations qui n'existent nulle part. Retirées par l'API après vérification que les
|
||||||
|
quatorze hôtes planifiés tombent tous dans les six `/24`. Flotte : 14/14 avant, 14/14 après.
|
||||||
|
|
||||||
|
**2. Les alias de tenant valaient le supernet.** Le retrait des `/16` n'a rien changé au
|
||||||
|
symptôme, ce qui a désigné le vrai coupable : `SETOPS_TENANT_CHEZ17` valait `10.27.0.0/16`,
|
||||||
|
donc **nos propres règles** autorisaient `admin → tout le /16:22`. L'état pf portait la
|
||||||
|
description de notre règle. Les alias énumèrent désormais les sous-réseaux attribués —
|
||||||
|
même geste que pour les routes, et pour la même raison. Ils servent à la fois de
|
||||||
|
destination aux règles et de source au NAT sortant : les deux se resserrent ensemble.
|
||||||
|
|
||||||
|
**3. Une garde pour que les deux ne divergent plus.** `verifier()` exige maintenant que
|
||||||
|
l'ensemble des réseaux **routés** et l'ensemble des réseaux **autorisés** coïncident
|
||||||
|
exactement. Un alias plus large laisse le filtre approuver l'inexistant ; un alias plus
|
||||||
|
étroit fait acheminer vers ce que le filtre refuse. Les deux pannes se voient au devis,
|
||||||
|
plus à l'usage. Attachée à P24, qui ne vérifiait jusqu'ici que la traduction NAT.
|
||||||
|
|
||||||
|
**Et le symptôme, alors ?** Il subsiste, et il n'est ni dans nos règles ni dans nos routes.
|
||||||
|
L'état pf porte désormais le nom de la règle d'usine `Default allow LAN to any rule`, qui
|
||||||
|
répond au SYN à la place de la destination. Mesure qui tranche : **depuis une VM du
|
||||||
|
tenant**, `10.99.99.99` et `172.31.99.99` — des adresses qui n'appartiennent à personne —
|
||||||
|
« s'établissent » en 1 ms, et **aucune ne rend de bannière SSH**, quand la vraie VM rend
|
||||||
|
`SSH-2.0-OpenSSH_10.0p2`. Le `connect()` ne mesure rien sur ce chemin, quel que soit le
|
||||||
|
point de départ ; seule une requête applicative tranche. Les règles héritées appartiennent
|
||||||
|
à l'exploitant : Set-OPS n'y touche pas.
|
||||||
|
|
||||||
|
**Les routes sont enfin réconciliées.** Je les avais posées avec un script hors dépôt :
|
||||||
|
rien ne les comparait au devis, et leur disparition n'aurait été vue par personne — le
|
||||||
|
défaut exact que cet applicateur existe pour empêcher. `appliquer_opnsense.py` les traite
|
||||||
|
maintenant comme les règles et le NAT : identité portée par la description
|
||||||
|
(`setopsroute:<tenant>:<réseau>-><saut>`), création avant retrait, périmètre strict. Le nom
|
||||||
|
de la passerelle est résolu depuis l'adresse du prochain saut plutôt que redemandé en
|
||||||
|
intrant. Les douze routes existantes ont été réétiquetées en place — aucune coupure.
|
||||||
|
|
||||||
|
Vérifié : `make frontiere-plan` → « la frontière dit déjà ce que le devis dit », 12 routes
|
||||||
|
inchangées, flotte 14/14, `prouver.py` code de sortie 0.
|
||||||
|
|
||||||
## 2026-08-09 — Le `connect()` qui « ne prouvait rien » n'était pas de l'anti-usurpation
|
## 2026-08-09 — Le `connect()` qui « ne prouvait rien » n'était pas de l'anti-usurpation
|
||||||
|
|
||||||
L'exploitant : « je ne trouve pas ça normal ». Il avait raison, et pendant deux jours nous
|
L'exploitant : « je ne trouve pas ça normal ». Il avait raison, et pendant deux jours nous
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ restait qu'à la dériver. C'est ce que fait `scripts/devis_opnsense.py`, à par
|
||||||
|
|
||||||
- `roles/*/meta/flux.yml` — les flux `pair: externe`, qui donnent les règles ;
|
- `roles/*/meta/flux.yml` — les flux `pair: externe`, qui donnent les règles ;
|
||||||
- l'inventaire de l'instance active — quels hôtes portent quel rôle, donc les destinations ;
|
- l'inventaire de l'instance active — quels hôtes portent quel rôle, donc les destinations ;
|
||||||
- `../*/plan/nomenclature.yml` — les supernets des tenants fédérés, donc les routes ;
|
- `../*/plan/nomenclature.yml` — les tenants fédérés et leurs zones, donc les routes ;
|
||||||
- l'intrant `nftables_admin_ssh` — les réseaux d'administration.
|
- l'intrant `nftables_admin_ssh` — les réseaux d'administration.
|
||||||
|
|
||||||
Aucun port, aucune adresse et aucun nom d'hôte n'est écrit dans le générateur.
|
Aucun port, aucune adresse et aucun nom d'hôte n'est écrit dans le générateur.
|
||||||
|
|
@ -169,10 +169,23 @@ d'administration. Le trou ne peut plus passer inaperçu.
|
||||||
|
|
||||||
### Le lien vit dans l'underlay, pas dans un tenant
|
### Le lien vit dans l'underlay, pas dans un tenant
|
||||||
|
|
||||||
La frontière route vers **tous** les supernets tenants (`10.21.0.0/16`, `10.27.0.0/16`…) par
|
La frontière route vers **tous** les tenants fédérés par le **même** prochain saut. Le lien
|
||||||
le **même** prochain saut. Le lien qui la relie au routeur est-ouest est donc *partagé* : il
|
qui la relie au routeur est-ouest est donc *partagé* : il n'appartient à aucun tenant et ne
|
||||||
n'appartient à aucun tenant et ne peut dériver d'aucun `index`. Sa place est l'underlay,
|
peut dériver d'aucun `index`. Sa place est l'underlay, cluster-global, au même titre que le
|
||||||
cluster-global, au même titre que le management, l'iSCSI et Ceph.
|
management, l'iSCSI et Ceph.
|
||||||
|
|
||||||
|
**Une route par sous-réseau attribué, jamais une par supernet** (2026-08-09). Router
|
||||||
|
`10.27.0.0/16` faisait porter à la frontière des destinations qui n'existent nulle part :
|
||||||
|
elles atteignaient le nœud de sortie, y arrivaient dans la table *principale* — le VRF n'est
|
||||||
|
atteint que par les `/24` annoncés en BGP — et repartaient vers la passerelle
|
||||||
|
d'administration. Les alias `SETOPS_TENANT_*` énumèrent exactement les mêmes `/24`, et
|
||||||
|
`verifier()` exige que les deux ensembles coïncident : un alias plus large laisse le filtre
|
||||||
|
approuver l'inexistant, un alias plus étroit fait acheminer vers ce que le filtre refuse.
|
||||||
|
|
||||||
|
Les routes sont **réconciliées comme les règles** par `appliquer_opnsense.py` : leur identité
|
||||||
|
est portée par leur description (`setopsroute:<tenant>:<réseau>-><saut>`), donc une route
|
||||||
|
retirée du devis est retirée de la frontière. Le nom de la passerelle OPNsense est résolu
|
||||||
|
depuis l'adresse du prochain saut — il n'y a pas d'intrant supplémentaire à tenir à jour.
|
||||||
|
|
||||||
Il se déclare dans `underlay.yml` par la clé **`passerelle_sortie`** — l'adresse du pare-feu
|
Il se déclare dans `underlay.yml` par la clé **`passerelle_sortie`** — l'adresse du pare-feu
|
||||||
sur ce lien :
|
sur ce lien :
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,40 @@ def cle_nat(n: dict) -> str:
|
||||||
return f"setopsnat:{n['tenant']}:{n['interface']}:{n['source']}->{n['destination']}:{n['cible']}"
|
return f"setopsnat:{n['tenant']}:{n['interface']}:{n['source']}->{n['destination']}:{n['cible']}"
|
||||||
|
|
||||||
|
|
||||||
|
def cle_route(r: dict) -> str:
|
||||||
|
"""Identite d'une route statique, meme principe que `cle_regle`.
|
||||||
|
|
||||||
|
Les routes ont longtemps ete posees A LA MAIN a cote de ce script. Elles
|
||||||
|
fonctionnaient, mais rien ne les reconciliait : leur disparition n'aurait ete vue
|
||||||
|
par personne, et un devis qui change les laissait derriere lui. C'est exactement le
|
||||||
|
defaut que ce script existe pour empecher — il ne pouvait pas s'appliquer a
|
||||||
|
lui-meme tant qu'il ignorait les routes.
|
||||||
|
"""
|
||||||
|
return f"setopsroute:{r['tenant']}:{r['reseau']}->{r['prochain_saut']}"
|
||||||
|
|
||||||
|
|
||||||
|
def _corps_route(r: dict, k: str, passerelle: str) -> dict:
|
||||||
|
return {
|
||||||
|
"network": r["reseau"],
|
||||||
|
"gateway": passerelle,
|
||||||
|
"disabled": "0",
|
||||||
|
"descr": k,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _passerelle_pour(api: Frontiere, saut: str) -> str | None:
|
||||||
|
"""Nom de la passerelle OPNsense portant cette adresse.
|
||||||
|
|
||||||
|
Une route se declare par NOM de passerelle, le devis raisonne en ADRESSE de
|
||||||
|
prochain saut. On resout ici plutot que d'exiger un intrant de plus : le nom est
|
||||||
|
deja sur le boitier, et le demander deux fois serait une occasion de divergence.
|
||||||
|
"""
|
||||||
|
for x in (api("/api/routes/gateway/status").get("items") or []):
|
||||||
|
if str(x.get("address") or "").strip() == str(saut).strip():
|
||||||
|
return str(x.get("name"))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _corps_nat(n: dict, k: str) -> dict:
|
def _corps_nat(n: dict, k: str) -> dict:
|
||||||
return {
|
return {
|
||||||
"enabled": "1",
|
"enabled": "1",
|
||||||
|
|
@ -164,7 +198,18 @@ def plan(api: Frontiere, devis: dict) -> dict:
|
||||||
if d.startswith("setopsnat:"):
|
if d.startswith("setopsnat:"):
|
||||||
nat_poses[d.split(" — ")[0]] = x
|
nat_poses[d.split(" — ")[0]] = x
|
||||||
|
|
||||||
|
rt_voulues = {cle_route(r): r for r in devis.get("routes") or []}
|
||||||
|
rt_posees = {}
|
||||||
|
for x in (api("/api/routes/routes/searchroute/",
|
||||||
|
{"current": 1, "rowCount": 1000}).get("rows") or []):
|
||||||
|
d = str(x.get("descr") or "")
|
||||||
|
if d.startswith("setopsroute:"):
|
||||||
|
rt_posees[d] = x
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"routes_creer": {k: r for k, r in rt_voulues.items() if k not in rt_posees},
|
||||||
|
"routes_garder": {k for k in rt_voulues if k in rt_posees},
|
||||||
|
"routes_retirer": {k: x for k, x in rt_posees.items() if k not in rt_voulues},
|
||||||
"nat_creer": {k: n for k, n in nat_voulus.items() if k not in nat_poses},
|
"nat_creer": {k: n for k, n in nat_voulus.items() if k not in nat_poses},
|
||||||
"nat_garder": {k for k in nat_voulus if k in nat_poses},
|
"nat_garder": {k for k in nat_voulus if k in nat_poses},
|
||||||
"nat_retirer": {k: x for k, x in nat_poses.items() if k not in nat_voulus},
|
"nat_retirer": {k: x for k, x in nat_poses.items() if k not in nat_voulus},
|
||||||
|
|
@ -192,6 +237,9 @@ def afficher(p: dict) -> bool:
|
||||||
for k, n in sorted(p["nat_creer"].items()):
|
for k, n in sorted(p["nat_creer"].items()):
|
||||||
print(f" + NAT sortant {n['interface']:<5} {n['source'][:30]:<30} "
|
print(f" + NAT sortant {n['interface']:<5} {n['source'][:30]:<30} "
|
||||||
f"-> {n['cible']}")
|
f"-> {n['cible']}")
|
||||||
|
for k, r in sorted(p["routes_creer"].items()):
|
||||||
|
print(f" + route {r['reseau']:<18} -> {r['prochain_saut']:<14} "
|
||||||
|
f"{r['tenant']}")
|
||||||
for k, x in sorted(p["regles_retirer"].items()):
|
for k, x in sorted(p["regles_retirer"].items()):
|
||||||
print(f" - regle PERIMEE {str(x.get('interface')):<5} "
|
print(f" - regle PERIMEE {str(x.get('interface')):<5} "
|
||||||
f"{str(x.get('protocol')):<4} {str(x.get('destination_port') or ''):<9} "
|
f"{str(x.get('protocol')):<4} {str(x.get('destination_port') or ''):<9} "
|
||||||
|
|
@ -199,15 +247,22 @@ def afficher(p: dict) -> bool:
|
||||||
for k, x in sorted(p["nat_retirer"].items()):
|
for k, x in sorted(p["nat_retirer"].items()):
|
||||||
print(f" - NAT PERIME {str(x.get('interface')):<5} "
|
print(f" - NAT PERIME {str(x.get('interface')):<5} "
|
||||||
f"{str(x.get('source_net'))[:30]:<30} -> {str(x.get('target'))}")
|
f"{str(x.get('source_net'))[:30]:<30} -> {str(x.get('target'))}")
|
||||||
|
for k, x in sorted(p["routes_retirer"].items()):
|
||||||
|
print(f" - route PERIMEE {str(x.get('network')):<18} "
|
||||||
|
f"-> {str(x.get('gateway'))}")
|
||||||
for n in sorted(p["alias_retirer"]):
|
for n in sorted(p["alias_retirer"]):
|
||||||
print(f" - alias ORPHELIN {n}")
|
print(f" - alias ORPHELIN {n}")
|
||||||
creer = len(p["alias_creer"]) + len(p["regles_creer"]) + len(p["nat_creer"])
|
creer = len(p["alias_creer"]) + len(p["regles_creer"]) + len(p["nat_creer"]) \
|
||||||
retirer = len(p["regles_retirer"]) + len(p["alias_retirer"]) + len(p["nat_retirer"])
|
+ len(p["routes_creer"])
|
||||||
|
retirer = len(p["regles_retirer"]) + len(p["alias_retirer"]) + len(p["nat_retirer"]) \
|
||||||
|
+ len(p["routes_retirer"])
|
||||||
print(f"\n a creer : {creer} | a retirer : {retirer}"
|
print(f"\n a creer : {creer} | a retirer : {retirer}"
|
||||||
f" | inchange : {len(p['regles_garder']) + len(p['nat_garder'])}")
|
f" | inchange : {len(p['regles_garder']) + len(p['nat_garder'])}"
|
||||||
|
f" + {len(p['routes_garder'])} routes")
|
||||||
return any(p[c] for c in ("alias_creer", "alias_majer", "alias_retirer",
|
return any(p[c] for c in ("alias_creer", "alias_majer", "alias_retirer",
|
||||||
"regles_creer", "regles_retirer",
|
"regles_creer", "regles_retirer",
|
||||||
"nat_creer", "nat_retirer"))
|
"nat_creer", "nat_retirer",
|
||||||
|
"routes_creer", "routes_retirer"))
|
||||||
|
|
||||||
|
|
||||||
def appliquer(api: Frontiere, p: dict) -> int:
|
def appliquer(api: Frontiere, p: dict) -> int:
|
||||||
|
|
@ -242,6 +297,20 @@ def appliquer(api: Frontiere, p: dict) -> int:
|
||||||
for k, n in sorted(p["nat_creer"].items()):
|
for k, n in sorted(p["nat_creer"].items()):
|
||||||
_fait(api("/api/firewall/source_nat/add_rule/", {"rule": _corps_nat(n, k)}), f"nat {k}")
|
_fait(api("/api/firewall/source_nat/add_rule/", {"rule": _corps_nat(n, k)}), f"nat {k}")
|
||||||
|
|
||||||
|
# 2bis. Routes, meme ordre et pour une raison plus forte encore : une route
|
||||||
|
# manquante coupe la flotte, une route en trop ne fait qu'acheminer vers un
|
||||||
|
# VRF qui la jettera. En cas de doute, on reste large.
|
||||||
|
for k, r in sorted(p["routes_creer"].items()):
|
||||||
|
gw = _passerelle_pour(api, r["prochain_saut"])
|
||||||
|
if gw is None:
|
||||||
|
echecs += 1
|
||||||
|
print(f" ! ECHEC route {k} : aucune passerelle ne porte {r['prochain_saut']}")
|
||||||
|
continue
|
||||||
|
_fait(api("/api/routes/routes/addroute/", {"route": _corps_route(r, k, gw)}),
|
||||||
|
f"route {k}")
|
||||||
|
for k, x in sorted(p["routes_retirer"].items()):
|
||||||
|
_fait(api(f"/api/routes/routes/delroute/{x['uuid']}", {}), f"retrait route {k}")
|
||||||
|
|
||||||
# 3. Retrait des perimees, puis des alias devenus orphelins.
|
# 3. Retrait des perimees, puis des alias devenus orphelins.
|
||||||
for k, x in sorted(p["regles_retirer"].items()):
|
for k, x in sorted(p["regles_retirer"].items()):
|
||||||
_fait(api(f"/api/firewall/filter/del_rule/{x['uuid']}", {}), f"retrait {k}")
|
_fait(api(f"/api/firewall/filter/del_rule/{x['uuid']}", {}), f"retrait {k}")
|
||||||
|
|
@ -253,6 +322,8 @@ def appliquer(api: Frontiere, p: dict) -> int:
|
||||||
if echecs:
|
if echecs:
|
||||||
print(f"\n {echecs} echec(s) — RIEN N'EST APPLIQUE, la config reste en attente.")
|
print(f"\n {echecs} echec(s) — RIEN N'EST APPLIQUE, la config reste en attente.")
|
||||||
return 1
|
return 1
|
||||||
|
if p["routes_creer"] or p["routes_retirer"]:
|
||||||
|
print(" routes :", api("/api/routes/routes/reconfigure/", {}).get("status", "?"))
|
||||||
print(" alias :", api("/api/firewall/alias/reconfigure/", {}).get("status", "?"))
|
print(" alias :", api("/api/firewall/alias/reconfigure/", {}).get("status", "?"))
|
||||||
print(" regles :", str(api("/api/firewall/filter/apply/", {}).get("status", "?")).strip())
|
print(" regles :", str(api("/api/firewall/filter/apply/", {}).get("status", "?")).strip())
|
||||||
print(" nat :", str(api("/api/firewall/source_nat/apply/", {}).get("status", "?")).strip())
|
print(" nat :", str(api("/api/firewall/source_nat/apply/", {}).get("status", "?")).strip())
|
||||||
|
|
|
||||||
|
|
@ -261,12 +261,28 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
if_gestion = _ou_marqueur(_intr, "opnsense_if_gestion", IF_GESTION)
|
if_gestion = _ou_marqueur(_intr, "opnsense_if_gestion", IF_GESTION)
|
||||||
nom_actif, nomenclature_active = _instance_active()
|
nom_actif, nomenclature_active = _instance_active()
|
||||||
|
|
||||||
|
# L'alias d'un tenant vaut ses SOUS-RESEAUX ATTRIBUES, pas son supernet.
|
||||||
|
#
|
||||||
|
# Tant qu'il valait `10.27.0.0/16`, nos propres regles autorisaient
|
||||||
|
# `admin -> 10.27.0.0/16:22` : le filtre laissait donc passer une connexion vers
|
||||||
|
# n'importe quelle adresse du /16, y compris celles ou aucune machine n'existe.
|
||||||
|
# Mesure du 2026-08-09, apres avoir retire les routes /16 : un `connect()` vers
|
||||||
|
# 10.27.99.99:22 aboutissait encore, et l'etat pf portait la description de NOTRE
|
||||||
|
# regle. Le symptome que nous avons attribue deux jours durant a une fonction
|
||||||
|
# d'anti-usurpation de la frontiere venait d'abord de nos declarations trop larges.
|
||||||
|
#
|
||||||
|
# Meme geste que pour les routes, et pour la meme raison : ne declarer que ce qui
|
||||||
|
# existe. L'alias sert a la fois de DESTINATION aux regles de filtrage et de SOURCE
|
||||||
|
# au NAT sortant ; le retrecir resserre correctement les deux.
|
||||||
alias: dict[str, dict] = {}
|
alias: dict[str, dict] = {}
|
||||||
for nom, pfx, n in tenants:
|
for nom, pfx, n in tenants:
|
||||||
alias[f"SETOPS_TENANT_{pfx}{n['index']}"] = {
|
alias[f"SETOPS_TENANT_{pfx}{n['index']}"] = {
|
||||||
"type": "network",
|
"type": "network",
|
||||||
"contenu": [supernet_de(n["index"])],
|
"contenu": [
|
||||||
"description": f"Supernet du tenant {nom} (index {n['index']})",
|
sous_reseau_de(n["index"], int(z))
|
||||||
|
for z in sorted((n.get("categories") or {}), key=int)
|
||||||
|
],
|
||||||
|
"description": f"Sous-reseaux attribues du tenant {nom} (index {n['index']})",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reseaux d'administration : UN ALIAS PAR TENANT, jamais une union. Chaque tenant
|
# Reseaux d'administration : UN ALIAS PAR TENANT, jamais une union. Chaque tenant
|
||||||
|
|
@ -405,14 +421,15 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
# deux jours a une fonction d'anti-usurpation de la frontiere, alors que c'etait un
|
# deux jours a une fonction d'anti-usurpation de la frontiere, alors que c'etait un
|
||||||
# routage trop large.
|
# routage trop large.
|
||||||
#
|
#
|
||||||
# La frontiere ne route desormais QUE ce qui existe. Le NAT, lui, reste sur le supernet
|
# La frontiere ne route desormais QUE ce qui existe. L'alias `SETOPS_TENANT_*`, qui
|
||||||
# (alias `SETOPS_TENANT_*`) : il porte sur la SOURCE, et le supernet contient tous les
|
# porte le NAT sortant et sert de destination aux regles, enumere exactement les memes
|
||||||
# sous-reseaux — la garde P24 reste donc satisfaite.
|
# sous-reseaux : la garde P24 compare ces deux listes et reste donc satisfaite.
|
||||||
_sdn = underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn"
|
_sdn = underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn"
|
||||||
routes = [
|
routes = [
|
||||||
{
|
{
|
||||||
"reseau": sous_reseau_de(n["index"], int(z)),
|
"reseau": sous_reseau_de(n["index"], int(z)),
|
||||||
"prochain_saut": saut,
|
"prochain_saut": saut,
|
||||||
|
"tenant": nom,
|
||||||
"description": f"{nom} — {cat.get('libelle', f'zone{z}')} (zone {z}, index "
|
"description": f"{nom} — {cat.get('libelle', f'zone{z}')} (zone {z}, index "
|
||||||
f"{n['index']}) — " + ("zone EVPN du tenant" if _sdn
|
f"{n['index']}) — " + ("zone EVPN du tenant" if _sdn
|
||||||
else "switches L3"),
|
else "switches L3"),
|
||||||
|
|
@ -669,6 +686,30 @@ def verifier(devis: dict) -> tuple[bool, list[str]]:
|
||||||
"avec une source privee. Le filtre laisserait passer et rien ne reviendrait."
|
"avec une source privee. Le filtre laisserait passer et rien ne reviendrait."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Ce qui est ROUTE et ce qui est AUTORISE doivent designer exactement les memes
|
||||||
|
# reseaux. Un alias plus large que les routes laisse le filtre approuver des
|
||||||
|
# destinations qui n'existent nulle part — defaut mesure le 2026-08-09 : l'alias valait
|
||||||
|
# le supernet /16, nos regles autorisaient donc `admin -> tout le /16:22`, et un
|
||||||
|
# `connect()` vers une adresse inexistante aboutissait toujours. Un alias plus etroit
|
||||||
|
# que les routes est la panne symetrique : la frontiere achemine vers un reseau que
|
||||||
|
# le filtre refuse. Les deux se voient ici, pas a l'usage.
|
||||||
|
routes_declarees = {str(r["reseau"]) for r in devis.get("routes") or []}
|
||||||
|
autorises: set[str] = set()
|
||||||
|
for nom, a in devis["alias"].items():
|
||||||
|
if nom.startswith("SETOPS_TENANT_"):
|
||||||
|
autorises |= {str(c) for c in a["contenu"]}
|
||||||
|
if routes_declarees != autorises:
|
||||||
|
for large in sorted(autorises - routes_declarees):
|
||||||
|
erreurs.append(
|
||||||
|
f"{large} est autorise par un alias de tenant mais n'est pas route : le "
|
||||||
|
"filtre approuverait une destination que la frontiere ne sait pas joindre."
|
||||||
|
)
|
||||||
|
for etroit in sorted(routes_declarees - autorises):
|
||||||
|
erreurs.append(
|
||||||
|
f"{etroit} est route mais absent des alias de tenant : la frontiere y "
|
||||||
|
"acheminerait des paquets que le filtre refuserait."
|
||||||
|
)
|
||||||
|
|
||||||
gestion = devis.get("reseaux_gestion") or []
|
gestion = devis.get("reseaux_gestion") or []
|
||||||
for r in devis["regles"]:
|
for r in devis["regles"]:
|
||||||
src = str(r.get("source") or "")
|
src = str(r.get("source") or "")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue