sdn : le puits avalait le plan d administration — une route, et le chemin existe

CE QUE LA MESURE A ETABLI, saut par saut. Le poste de l exploitant atteint la
frontiere, qui LAISSE PASSER (rule 31/0 match, pass out vlan040). Asgard RECOIT le
paquet sur vlan40. La VM ne le voit jamais. Et le noyau dit pourquoi :

    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 SOURCE EST LE DISCRIMINANT, pas l interface. Le chemin de RETOUR, vu du VRF :

    vers 10.0.31.11  ->  via 10.0.4.1 dev vlan40
    vers 10.17.0.17  ->  Invalid argument          <- le puits

LE PUITS A SES RAISONS et on n y touche pas : sans lui, une adresse non attribuee
du supernet sort par le defaut, revient par la frontiere dans la table PRINCIPALE
et repart vers le reseau de gestion (mesure du 2026-08-09). Le defaut n est pas
qu il existe, c est qu il est TROP LARGE : il couvre la bande basse ou D-77 place
justement l underlay d un site. Deux regles justes separement, contradictoires
ensemble.

LA CORRECTION EST DERIVEE, PAS ECRITE : pour chaque tenant, les reseaux
d administration qu il DECLARE (nftables_admin_ssh) et qui tombent dans son propre
supernet recoivent une route vers la frontiere, plus specifique que le puits. Ceux
qui vivent dehors n en ont pas besoin — la route par defaut les joint deja.

    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 ops-01

CE QUE CA REPARE AU-DELA DE L ACCES : les regles administration -> tenant de la
frontiere etaient VRAIES et INAPPLICABLES a 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.

make verifier : vert. make prouver : CONFORME, 54 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
This commit is contained in:
Daniel Allaire 2026-08-28 17:46:40 -04:00
parent 90ea474745
commit 732aff9c70
2 changed files with 112 additions and 1 deletions

View file

@ -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 —

View file

@ -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.<index>.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"