SDN : un puits sur le supernet du tenant dans son propre VRF

« Je ne trouve pas ca normal » — l'exploitant avait raison. Pendant deux jours
nous avons attribue ce comportement a l'anti-usurpation de la frontiere, et je
l'avais consigne comme tel.

Mesure depuis DEUX points de vue : depuis le poste, quatre connexions sur
quatre s'etablissaient ; depuis l'interieur du tenant, le comportement etait
correct partout ou un VNet existe. L'anomalie ne touchait que les portions de
supernet sans VNet.

Cause : vrf_t17 portait les six /24 et un defaut, RIEN pour le reste du /16.
Une adresse non attribuee sortait du VRF, atteignait la frontiere, revenait a
l'hyperviseur dans la table PRINCIPALE — pas dans le VRF — et repartait vers
192.168.11.254, la passerelle du reseau d'ADMINISTRATION.

strophe_frr pose desormais un puits sur le supernet du tenant, moins specifique
que ses /24 donc invisible au trafic legitime. Derive du seed.

Verifie : depuis le tenant, 10.27.99.99 et 10.27.18.99 refusees, 10.27.18.21
etablie. Une machine du tenant ne peut plus atteindre le reseau de gestion par
une faute de frappe.

Reste, sans arbitrage : depuis le VLAN d'administration le connect() reussit
encore, ce trafic n'entrant jamais dans le VRF. Deux remedes possibles, dont
l'un touche la table qui porte l'administration des hyperviseurs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-09 16:01:24 -04:00
parent 91d9bd19f4
commit 78f2f7fd6f
2 changed files with 65 additions and 1 deletions

View file

@ -1,5 +1,53 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 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
avons tous les deux attribué ce comportement à une fonction d'anti-usurpation de la
frontière — moi le premier, et je l'avais même consigné comme tel.
**Mesuré, pas raconté.** Depuis le poste, quatre connexions sur quatre s'établissaient, y
compris vers une adresse où aucune machine n'existe. Mais vers des réseaux hors du tenant,
tout était refusé — donc rien n'interceptait globalement. Et **depuis l'intérieur du
tenant**, le comportement était correct partout où un VNet existe : hôte absent → refusé,
port fermé → refusé. L'anomalie ne touchait que les portions de supernet **non couvertes
par un VNet**.
**La cause était une route manquante, pas un pare-feu.**
```
vrf_t17 : les six /24 des VNets, puis default -> 10.0.4.1
RIEN pour le reste de 10.27.0.0/16
```
Une adresse non attribuée sortait donc du VRF par le défaut, atteignait la frontière, qui
la renvoyait à l'hyperviseur — où elle arrivait dans la table **principale**, pas dans le
VRF, et repartait vers `192.168.11.254`, la passerelle du réseau d'**administration**.
`ip route get 10.27.99.99` le disait en une ligne.
**Corrigé où le dépôt a la main** : `strophe_frr` pose désormais, dans chaque VRF, un puits
sur le supernet du tenant — **moins spécifique** que les `/24` de ses VNets, donc invisible
au trafic légitime. Dérivé du seed, comme tout le reste.
```
depuis le tenant, apres : 10.27.99.99 refusee 10.27.18.99 refusee 10.27.18.21 ETABLIE
```
**Une machine du tenant ne peut plus atteindre le réseau de gestion par une faute de
frappe.** C'était le vrai risque, et il est fermé.
**Ce qui reste, et que je ne corrige pas sans arbitrage.** Depuis le VLAN d'administration,
le `connect()` réussit encore : ce trafic n'entre jamais dans le VRF. OPNsense route tout
`10.27.0.0/16` vers l'hyperviseur, dont la table principale ne connaît que les six `/24`.
Deux remèdes possibles — un puits symétrique dans la table principale, ou n'annoncer à la
frontière que les `/24` réellement attribués. Le premier touche la table qui porte
l'administration des hyperviseurs ; ce n'est pas un geste à faire de sa propre initiative.
**La leçon dépasse la route.** Nous avons expliqué pendant deux jours un symptôme par une
cause plausible et fausse, et cette explication est entrée dans la documentation. Ce qui l'a
défaite n'est pas un raisonnement plus fin : c'est d'avoir mesuré depuis **deux points de
vue différents**. Un seul point de vue donne une histoire cohérente — souvent la mauvaise.
## 2026-08-09 — Le wiki rattrape ce que la reconstruction a appris ## 2026-08-09 — Le wiki rattrape ce que la reconstruction a appris
Le wiki datait du 3 août — six jours avant tout ce qui précède. Vérifié avant d'y toucher : Le wiki datait du 3 août — six jours avant tout ce qui précède. Vérifié avant d'y toucher :

View file

@ -63,7 +63,7 @@ import yaml
RACINE = Path(__file__).resolve().parents[1] RACINE = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(RACINE / "scripts")) sys.path.insert(0, str(RACINE / "scripts"))
from inventory_rules import passerelle_de, sous_reseau_de, vlan_de # noqa: E402 from inventory_rules import passerelle_de, sous_reseau_de, supernet_de, vlan_de # noqa: E402
from devis_reseau import decouvrir # noqa: E402 from devis_reseau import decouvrir # noqa: E402
import underlay as underlay_mod # noqa: E402 import underlay as underlay_mod # noqa: E402
@ -139,6 +139,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
blocs.append({ blocs.append({
"tenant": nom_instance, "tenant": nom_instance,
"index": index, "index": index,
"supernet": supernet_de(index),
"zone": zone_de(index), "zone": zone_de(index),
"vrf_vxlan": index, "vrf_vxlan": index,
"mtu": mtu, "mtu": mtu,
@ -185,6 +186,21 @@ def strophe_frr(devis: dict) -> str:
for b in devis["zones"]: for b in devis["zones"]:
lignes += [f"vrf vrf_{b['zone']}", lignes += [f"vrf vrf_{b['zone']}",
f" ip route 0.0.0.0/0 {devis['frontiere']} nexthop-vrf default", f" ip route 0.0.0.0/0 {devis['frontiere']} nexthop-vrf default",
# PUITS sur le supernet du tenant, MOINS SPECIFIQUE que les /24 de ses
# VNets : le trafic legitime ne le voit jamais, tout le reste s'y arrete.
#
# Sans lui, une adresse non attribuee du supernet ne trouve aucune route
# locale, sort par le defaut, atteint la frontiere qui la renvoie a
# l'hyperviseur — ou elle arrive dans la table PRINCIPALE, pas dans le
# VRF, et repart vers la passerelle du reseau d'ADMINISTRATION. Mesure du
# 2026-08-09 : `ip route get 10.27.99.99` rend `via 192.168.11.254`.
#
# Deux consequences. Le trafic d'un tenant pouvait atteindre le reseau de
# gestion par une simple faute de frappe. Et `connect()` reussissait vers
# 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",
"exit-vrf"] "exit-vrf"]
return "\n".join(lignes) + "\n" return "\n".join(lignes) + "\n"