diff --git a/CHANGELOG.md b/CHANGELOG.md index eab7af9..4298f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # CHANGELOG — Set-OPS +## 2026-08-28 — Le puits avalait le plan d'administration + +**54 preuves.** Le poste de l'exploitant ne joignait aucune machine de tenant. La chaîne, +mesurée saut par saut : + +``` +1. le poste 10.17.0.17 -> 10.17.19.41:22 route et source correctes +2. la frontiere rule 31/0(match): pass out vlan040 ELLE LAISSE PASSER +3. asgard vlan40 In ... 10.17.19.41.22 IL RECOIT +4. la VM (rien) IL N'ARRIVE JAMAIS +``` + +Et le noyau dit pourquoi — **la source est le discriminant, pas l'interface** : + +``` +ip route get 10.17.19.41 from 10.0.31.11 iif vlan40 -> dev vrf_t17 +ip route get 10.17.19.41 from 10.17.0.17 iif vlan40 -> Invalid cross-device link +``` + +Parce que le **retour** est impossible : depuis le VRF du tenant, `10.17.0.17` tombe dans +`blackhole 10.17.0.0/16`. + +### Le puits a ses raisons — c'est sa largeur qui était fausse + +Sans lui, une adresse non attribuée du supernet sort par le défaut, revient par la +frontière **dans la table principale** et repart vers le réseau de gestion (mesuré le +2026-08-09). On n'y touche pas. + +Mais il couvre la **bande basse**, là où D-77 place justement l'underlay d'un site. *Deux +règles justes séparément, contradictoires ensemble.* + +### La correction est dérivée, pas écrite + +Pour chaque tenant, les réseaux d'administration **qu'il déclare** (`nftables_admin_ssh`) +et qui tombent dans son propre supernet reçoivent une route vers la frontière, plus +spécifique que le puits. Ceux qui vivent dehors n'en ont pas besoin. + +``` +vrf_t17 ip route 10.17.0.0/24 ... l'admin de Chezlepro +vrf_t23 (rien) le sien est hors de son supernet +vrf_t29 ip route 10.29.19.41/32 ... l'admin de patient 0 : son propre ops-01 +``` + +### Ce que ça répare au-delà de l'accès + +Les règles `administration → tenant` de la frontière étaient **vraies et inapplicables à +la fois**. Elles correspondaient, elles laissaient passer, et le paquet mourait un saut +plus loin. *Un devis vert sur un chemin qui ne pouvait pas aboutir* — exactement le +« périmètre vide » que ce dépôt traque partout ailleurs. + +*Trois instruments m'ont menti avant d'y arriver : une capture qui ne tournait pas, un +`ping` vers un ICMP que rien n'autorise, et un test TCP depuis la frontière dont la source +n'est pas dans l'IPSet de la VM. La mesure qui a tranché est `ip route get ... iif`, +comparée entre une source qui marche et une qui échoue.* + +make verifier : vert. make prouver : CONFORME, 54 OK, 0 echec, 0 saute. + ## 2026-08-28 — `make inseminer` : le geste sort de mes mains et entre dans le depot **54 preuves.** L'insemination a d'abord ete conduite A LA MAIN depuis le runner du site — diff --git a/scripts/devis_sdn.py b/scripts/devis_sdn.py index 1c85f04..02b2f43 100644 --- a/scripts/devis_sdn.py +++ b/scripts/devis_sdn.py @@ -52,6 +52,7 @@ Usage : from __future__ import annotations import argparse +import ipaddress import json import os import sys @@ -118,6 +119,36 @@ def sdn_hebergeur() -> dict: return data.get("proxmox_sdn") or {} +def _admin_dans_le_supernet(nom_instance: str, supernet: str) -> list[str]: + """Les reseaux d'administration de ce tenant qui tombent DANS son propre supernet. + + CEUX-LA SEULEMENT ONT BESOIN D'UNE ROUTE, et c'est tout le point. Un plan + d'administration pose hors du supernet (10.0.0.0/24 du site, une adresse de VPN) est + joint par la route par defaut du VRF : il n'a jamais rencontre le puits. Celui qui vit + DANS le supernet, lui, tombe dedans. + + D-77 place justement l'underlay d'un site dans la BANDE BASSE de son propre supernet — + `10..0-15.x`. Chez un hebergeur qui est aussi son propre tenant, le plan + d'administration est donc a l'interieur du /16 que le puits couvre. Les deux regles + sont justes separement et se contredisent ensemble ; cette fonction nomme + l'intersection. + """ + try: + from devis_reseau import admin_de + reseau_super = ipaddress.ip_network(supernet, strict=False) + dedans = [] + for cidr in admin_de(nom_instance): + try: + n = ipaddress.ip_network(str(cidr).strip(), strict=False) + except ValueError: + continue + if n.subnet_of(reseau_super) and n != reseau_super: + dedans.append(str(n)) + return sorted(set(dedans)) + except Exception: + return [] + + def construire(tenants: list[tuple[str, str, dict]]) -> dict: sdn = sdn_hebergeur() u = underlay_mod.charger() @@ -140,6 +171,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: "tenant": nom_instance, "index": index, "supernet": supernet_de(index), + "admin_interne": _admin_dans_le_supernet(nom_instance, supernet_de(index)), "zone": zone_de(index), "vrf_vxlan": index, "mtu": mtu, @@ -200,7 +232,29 @@ def strophe_frr(devis: dict) -> str: # n'importe quelle adresse inexistante du supernet — ce qui a longtemps # passe pour de l'anti-usurpation de la frontiere, alors que c'etait une # route manquante ici. - f" ip route {b['supernet']} blackhole", + ] + # LE PUITS AVALAIT LE PLAN D'ADMINISTRATION (mesure du 2026-08-28). + # + # Il couvre tout le supernet du tenant. Les VNets s'en sortent : leurs /24 sont + # connectes, donc plus specifiques. Mais D-77 place l'underlay d'un site dans la + # BANDE BASSE de ce meme supernet — et rien ne l'y rendait plus specifique. + # + # Consequence, invisible parce qu'elle porte sur le RETOUR : un paquet venu du plan + # d'administration arrive bien sur l'hyperviseur, et le noyau refuse de l'acheminer + # parce que sa source est injoignable depuis le VRF. + # + # ip route get 10.17.19.41 from 10.0.31.11 iif vlan40 -> dev vrf_t17 + # ip route get 10.17.19.41 from 10.17.0.17 iif vlan40 -> Invalid cross-device link + # + # La frontiere, elle, laissait passer : ses regles `administration -> tenant` + # etaient donc VRAIES et INAPPLICABLES a la fois — un devis vert sur un chemin + # qui ne pouvait pas aboutir. + # + # On ne touche pas au puits, qui a ses raisons (voir plus haut). On rend l'admin + # plus specifique que lui, vers la frontiere — la ou ce trafic doit aller. + for _adm in b.get("admin_interne") or []: + lignes.append(f" ip route {_adm} {devis['frontiere']} nexthop-vrf default") + lignes += [f" ip route {b['supernet']} blackhole", "exit-vrf"] return "\n".join(lignes) + "\n"