fix(network) : un /64 de conteneurs par nœud, pour que la réponse revienne au bon #38

Merged
thystips merged 5 commits from feat/per-node-container-prefix into main 2026-08-14 02:16:28 +02:00
Owner

Le défaut

Les deux nœuds d'un site partageaient un seul /64 de services et l'annonçaient tous
les deux
. Un conteneur qui interrogeait une destination DN42 externe voyait donc sa réponse
revenir par le pair que le lointain avait choisi — une fois sur deux le nœud voisin,
qui n'a aucune entrée de conntrack pour un flux parti d'à côté et la jetait sur
drop "DN42 does not reach containers".

La règle de sortie portait l'hypothèse fausse dans son propre commentaire : « le retour
revient en established, accepté tout en haut de la chaîne »
. Vrai seulement si le retour
revient sur le même nœud
. Une hypothèse d'état écrite au singulier là où il y a deux
machines.

En production : deux résolveurs anycast sur quatre ne résolvaient plus le moindre nom
DN42 externe, tout en annonçant leur VIP. Par destination et intermittent, donc invisible.

Le correctif

Un /64 de conteneurs par nœud, que le nœud est seul à annoncer — une seconde adresse
sur br-svc suffit à rendre le préfixe connecté. Le voisin ne délivre donc plus localement :
il forwarde vers le propriétaire, qui a l'entrée de conntrack.

⚠ La règle de transit n'affaiblit pas la politique : le nœud de destination applique la
sienne — established s'il a l'entrée, drop sinon. La décision est simplement prise une
fois, sur la machine seule à pouvoir en juger.

⚠ Le /64 du segment ne bouge pas : ghost y vit à Bordeaux, avec une session BGP vers
les deux bod.

La mesure, avant / après

kdig +retry=0 +timeout=3 @<stub-addr> dn42. NS depuis chaque conteneur résolveur, sur les
trois serveurs de délégation v6 d'Unbound :

ac53::1 2189::1 avant après
resolver-fr-rbx1 FAIL → OK OK 1/3 2/3
resolver-fr-rbx2 OK OK 2/3 2/3
resolver-fr-bod1 OK OK 2/3 2/3
resolver-fr-bod2 OK FAIL → OK 1/3 2/3

Le signe qui compte n'est pas le total, c'est que les quatre donnent désormais le même
résultat.
Avant, deux jumeaux d'un même site — même configuration, même segment —
divergeaient : c'était toute la signature du défaut. (fd00:913e:130::400 échouait déjà
partout avant : serveur hors service côté DN42, pas notre chemin.)

Trois défauts trouvés en déroulant, corrigés ici

  1. cloud-init n'écrit l'adresse d'un conteneur qu'au premier boot. La branche ne pouvait
    donc renuméroter aucun conteneur existant — l'adresse était un état en écriture unique,
    jamais confronté à sa source de vérité. Le rôle containers pose désormais
    10-dn42-eth{0,1}.network et désactive la gestion réseau de cloud-init : deux écrivains
    sur une interface, c'est un redémarrage qui défait un déploiement.
  2. La session iBGP nœud → conteneur partait de la mauvaise des deux adresses du nœud.
    Bird refuse une connexion entrante d'une adresse qui n'est pas celle de son voisin —
    « Unexpected connect from unknown address » — session coincée en Active des deux côtés
    avec TCP/179 ouvert et joignable.
  3. La matrice des flux n'avait pas suivi le gabarit, règle du dépôt.

Déployé et vérifié

  • 12 conteneurs renumérotés, chacun portant exactement une adresse (l'ancienne retirée)
  • 8/8 sessions iBGP conteneur Established
  • les 4 VIP résolveur répondent avec le bon NSID ; lg.thystips.dn42 répond 302 depuis les 4
  • raft OpenBao : leader élu, 4 membres au même Raft Applied Index
  • dn42_dns_recursion_success à 1 partout
  • NetBox : 0 to create, 0 to update

Le raft ne s'est pas remis seul : retry_join ne sert qu'au join initial et ne corrige
pas la configuration interne du raft, donc plus aucun membre ne se joignait et aucun leader ne
pouvait être élu. Dénoué par la récupération officielle peers.json. Un membre migré affiche
Sealed false / standby — donc l'air d'aller bien — alors que le raft ne l'a jamais
réenregistré : le seul contrôle qui voit la panne est l'index identique sur les quatre.

Les deux conteneurs lg (PR #37) n'étaient pas dans l'inventaire de cette branche et
restent sur le /64 du segment. Sans régression — le segment reste dans ct_sources — mais
ils gardent le défaut jusqu'au rebase de la #37, qui leur donnera leur adresse tout seul.

La v4 depuis les conteneurs restait hétérogène après ce correctif — pour une raison
différente, instruite et corrigée plus bas.


Un SECOND retour asymétrique, de cause différente (14/08/2026)

Trouvé en instruisant la v4 restée hétérogène après le correctif ci-dessus.

Un conteneur n'a pas d'adresse v4 propre dans DN42 : sa seule v4 est la VIP anycast,
sur sa loopback. Quand Unbound récurse en v4, il sort donc avec une source anycast, et le
lointain répond à l'instance la plus proche de lui :

fr-rbx2  wgp-yuyuko     Out  172.20.187.253 > 172.20.129.1   NS? dn42.
fr-bod1  wgp-routedbits In   172.20.129.1 > 172.20.187.253   NS ...   ← AUTRE SITE

Ce n'est pas le défaut du /64 : celui-ci tient à l'anycast lui-même et serait resté
entier. Une adresse anycast comme source est fausse par construction. En v6 la question ne
se pose pas — le conteneur a une unicast pour sortir et la VIP pour servir ; en v4 il n'a que
la seconde.

Le correctif : un SNAT vers la loopback v4 du nœud, qui est unicast. C'est le premier NAT
que ce dépôt écrit lui-même — choisi contre une v4 unicast par conteneur parce que le /27 du
site est une ressource rare (32 adresses, 7 prises).

Il ne voit pas le servant : la chaîne nat n'est consultée que pour le premier paquet d'un
flux, jamais pour la réponse à une requête entrante. Et il exclut br-svc, pour que le raft et
les sondes gardent l'identité de qui parle.

Mesuré après déploiement : les quatre résolveurs atteignent les quatre serveurs testés
en v4 comme en v6 (16/16), et les deux VIP v4 répondent toujours depuis les quatre nœuds
avec le bon NSID.

## Le défaut Les deux nœuds d'un site partageaient **un seul `/64` de services** et l'**annonçaient tous les deux**. Un conteneur qui interrogeait une destination DN42 externe voyait donc sa réponse revenir par le pair que le **lointain** avait choisi — une fois sur deux le **nœud voisin**, qui n'a aucune entrée de conntrack pour un flux parti d'à côté et la jetait sur `drop "DN42 does not reach containers"`. La règle de sortie portait l'hypothèse fausse dans son propre commentaire : *« le retour revient en `established`, accepté tout en haut de la chaîne »*. Vrai **seulement si le retour revient sur le même nœud**. Une hypothèse d'état écrite au singulier là où il y a deux machines. En production : **deux résolveurs anycast sur quatre** ne résolvaient plus le moindre nom DN42 externe, **tout en annonçant leur VIP**. Par destination et intermittent, donc invisible. ## Le correctif Un **`/64` de conteneurs par nœud**, que le nœud est seul à annoncer — une seconde adresse sur `br-svc` suffit à rendre le préfixe connecté. Le voisin ne délivre donc plus localement : il **forwarde** vers le propriétaire, qui a l'entrée de conntrack. ⚠ La règle de transit **n'affaiblit pas la politique** : le nœud de destination applique la sienne — `established` s'il a l'entrée, `drop` sinon. La décision est simplement prise une fois, sur la machine seule à pouvoir en juger. ⚠ Le `/64` du **segment** ne bouge pas : `ghost` y vit à Bordeaux, avec une session BGP vers les deux `bod`. ## La mesure, avant / après `kdig +retry=0 +timeout=3 @<stub-addr> dn42. NS` depuis chaque conteneur résolveur, sur les trois serveurs de délégation **v6** d'Unbound : | | `ac53::1` | `2189::1` | avant | après | |---|---|---|---|---| | resolver-fr-rbx1 | FAIL → **OK** | OK | 1/3 | **2/3** | | resolver-fr-rbx2 | OK | OK | 2/3 | 2/3 | | resolver-fr-bod1 | OK | OK | 2/3 | 2/3 | | resolver-fr-bod2 | OK | FAIL → **OK** | 1/3 | **2/3** | **Le signe qui compte n'est pas le total, c'est que les quatre donnent désormais le même résultat.** Avant, deux jumeaux d'un même site — même configuration, même segment — divergeaient : c'était toute la signature du défaut. (`fd00:913e:130::400` échouait déjà partout avant : serveur hors service côté DN42, pas notre chemin.) ## Trois défauts trouvés en déroulant, corrigés ici 1. **`cloud-init` n'écrit l'adresse d'un conteneur qu'au premier boot.** La branche ne pouvait donc renuméroter **aucun conteneur existant** — l'adresse était un état en écriture unique, jamais confronté à sa source de vérité. Le rôle `containers` pose désormais `10-dn42-eth{0,1}.network` et désactive la gestion réseau de cloud-init : deux écrivains sur une interface, c'est un redémarrage qui défait un déploiement. 2. **La session iBGP nœud → conteneur partait de la mauvaise des deux adresses du nœud.** Bird refuse une connexion entrante d'une adresse qui n'est pas celle de son voisin — *« Unexpected connect from unknown address »* — session coincée en `Active` des deux côtés **avec TCP/179 ouvert et joignable**. 3. **La matrice des flux n'avait pas suivi le gabarit**, règle du dépôt. ## Déployé et vérifié - 12 conteneurs renumérotés, chacun portant **exactement une** adresse (l'ancienne retirée) - 8/8 sessions iBGP conteneur `Established` - les 4 VIP résolveur répondent avec le bon NSID ; `lg.thystips.dn42` répond 302 depuis les 4 - raft OpenBao : leader élu, **4 membres au même `Raft Applied Index`** - `dn42_dns_recursion_success` à 1 partout - NetBox : `0 to create, 0 to update` ⚠ **Le raft ne s'est pas remis seul** : `retry_join` ne sert qu'au join initial et ne corrige pas la configuration interne du raft, donc plus aucun membre ne se joignait et aucun leader ne pouvait être élu. Dénoué par la récupération officielle `peers.json`. Un membre migré affiche `Sealed false` / `standby` — donc l'air d'aller bien — alors que le raft ne l'a jamais réenregistré : le seul contrôle qui voit la panne est l'index identique sur les quatre. ⚠ **Les deux conteneurs `lg`** (PR #37) n'étaient pas dans l'inventaire de cette branche et restent sur le `/64` du segment. Sans régression — le segment reste dans `ct_sources` — mais ils gardent le défaut jusqu'au rebase de la #37, qui leur donnera leur adresse tout seul. ⚠ **La v4 depuis les conteneurs restait hétérogène après ce correctif** — pour une raison différente, instruite et corrigée plus bas. --- ## Un SECOND retour asymétrique, de cause différente (14/08/2026) Trouvé en instruisant la v4 restée hétérogène après le correctif ci-dessus. Un conteneur n'a **pas d'adresse v4 propre** dans DN42 : sa seule v4 est la **VIP anycast**, sur sa loopback. Quand Unbound récurse en v4, il sort donc avec une source **anycast**, et le lointain répond à l'instance la plus proche de **lui** : ``` fr-rbx2 wgp-yuyuko Out 172.20.187.253 > 172.20.129.1 NS? dn42. fr-bod1 wgp-routedbits In 172.20.129.1 > 172.20.187.253 NS ... ← AUTRE SITE ``` ⚠ **Ce n'est pas le défaut du `/64`** : celui-ci tient à l'anycast lui-même et serait resté entier. Une adresse anycast comme **source** est fausse par construction. En v6 la question ne se pose pas — le conteneur a une unicast pour sortir et la VIP pour servir ; en v4 il n'a que la seconde. **Le correctif** : un SNAT vers la loopback v4 du nœud, qui est unicast. C'est le premier NAT que ce dépôt écrit lui-même — choisi contre une v4 unicast par conteneur parce que le `/27` du site est une ressource rare (32 adresses, 7 prises). Il ne voit pas le servant : la chaîne `nat` n'est consultée que pour le **premier paquet** d'un flux, jamais pour la réponse à une requête entrante. Et il exclut `br-svc`, pour que le raft et les sondes gardent l'identité de qui parle. **Mesuré après déploiement** : les quatre résolveurs atteignent les quatre serveurs testés **en v4 comme en v6** (16/16), et les deux VIP v4 répondent toujours depuis les quatre nœuds avec le bon NSID.
Le /64 de services est partagé par les deux nœuds d'un site et annoncé par les
DEUX. La réponse à un flux SORTANT d'un conteneur revient donc par le pair que le
LOINTAIN a choisi — une fois sur deux par le voisin, qui n'a aucune entrée de
conntrack pour un flux parti d'ailleurs et la jette sur la règle « DN42 does not
reach containers ».

Le commentaire de la règle de sortie énonçait l'hypothèse, et c'est elle qui
était fausse : « le retour revient en established, accepté tout en haut de la
chaîne ». Vrai SEULEMENT si le retour revient sur le même nœud.

MESURÉ le 12/08/2026 : deux des quatre résolveurs ne joignaient plus les serveurs
de délégation DN42 et SERVFAILaient sur tout nom DN42 hors de notre zone, EN
ANNONÇANT leur VIP anycast. Capture à l'appui — requête sortie par `wgp-yuyuko`
sur fr-rbx2, réponse entrée par `wgp-kioubit` sur fr-rbx1, et rien derrière.

LE CORRECTIF. Chaque nœud reçoit un /64 à lui, annoncé par lui seul. Le voisin ne
délivre plus localement : il FORWARDE vers le nœud propriétaire — transit dn42
vers dn42, déjà accepté — et c'est celui-là qui a l'entrée de conntrack. La
politique n'est pas affaiblie : elle est appliquée UNE FOIS, sur la machine qui
possède le préfixe et qui est la seule à pouvoir en juger.

⚠ LE SEGMENT NE BOUGE PAS, et ce n'est pas de la prudence : à Bordeaux le routeur
de la maison est dessus (`::ffff`, session BGP avec les deux `bod`). Le
renuméroter voudrait dire toucher au MikroTik pour un défaut qui ne le concerne
pas. Le nœud porte donc DEUX adresses sur `br-svc`.

LA CONVENTION `<chiffre du site>c<id du nœud>` met ces /64 hors d'atteinte du
schéma de la maison, qui écrit `2` suivi du VLAN en DÉCIMAL — un `2c..` ne peut
pas en naître. Le plugin d'inventaire vérifie l'appartenance au services_prefix
et l'absence de recouvrement avec tout ce que topology/ dépense déjà.

Le rendu comparé sur fr-rbx1 ne montre que trois choses : les adresses des
sessions iBGP vers les conteneurs, `ct_sources` qui gagne les quatre préfixes, et
la règle de transit neuve. Rien d'autre ne bouge.

⚠ NON DÉPLOYÉ. Tous les conteneurs sont renumérotés, dont les quatre membres du
raft OpenBao, joints par leurs adresses DN42.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Le commit précédent ajoute `ct_siblings` et la règle qui achemine vers les
conteneurs d'un autre nœud, mais laisse `flow-matrix.md` intact — or les deux
décrivent la même politique et se modifient ensemble.

La ligne de transit entre dans le tableau, et une section dit ce qu'aucune des
quatre lignes ne pouvait dire : le `drop` était juste et cassait quand même le
sortant, parce que la réponse revenait chez le voisin.

Le commentaire de la règle de sortie portait l'hypothèse fausse mot pour mot
(« le retour revient en established ») et restait intact au-dessus de sa propre
correction. Il porte maintenant la condition qui manquait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
`cloud-init.network-config` est lu au PREMIER BOOT et jamais ensuite. L'adresse
d'un conteneur n'existait donc que là, et changer `topology/` ne renumérotait
rien sur un conteneur déjà en marche — un état en écriture unique, qui dérivait
en silence de sa source de vérité parce que personne ne l'avait jamais confronté.

Le passage au /64 par nœud le confronte : les douze conteneurs doivent bouger.

Ansible prend donc la patte. cloud-init amorce, `10-dn42-eth{0,1}.network`
maintient, et la gestion réseau de cloud-init est désactivée derrière — deux
écrivains sur une interface, c'est un redémarrage qui défait un déploiement.

`networkctl reconfigure` et pas seulement `reload` : c'est lui qui RETIRE ce
qui n'est plus déclaré. La vérification cherche l'ancienne adresse autant que la
nouvelle, un conteneur répondant sur deux préfixes gardant exactement le défaut
qu'on corrige.

Le render-check validait tout template du rôle en YAML, ce qui était vrai tant
qu'ils étaient tous des documents cloud-init. Il valide maintenant ceux qui en
sont.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Le nœud porte désormais DEUX adresses sur `br-svc` — celle du segment et celle
de son /64 de conteneurs — et la session iBGP déclarait la première en `local`.
Or le conteneur nomme la seconde en `neighbor`, parce que c'est sa passerelle.

Bird refuse une connexion entrante venant d'une adresse qui n'est pas celle de
son voisin : « Unexpected connect from unknown address ». Session coincée en
`Active` des deux côtés, avec TCP/179 ouvert et joignable — le genre de panne
qui se diagnostique partout sauf là où elle est.

`ansible_managed` n'existe que dans un `template`, pas dans le `content` d'un
`copy` ; le fichier qui désactive la gestion réseau de cloud-init en devient un.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Un conteneur n'a pas d'adresse v4 propre dans DN42 : sa seule v4 est la VIP
anycast, sur sa loopback. Quand Unbound récurse vers un serveur de délégation en
v4, il sort donc avec une source anycast — et le lointain répond à l'instance la
plus proche de LUI.

    fr-rbx2  wgp-yuyuko     Out  172.20.187.253 > 172.20.129.1   NS? dn42.
    fr-bod1  wgp-routedbits In   172.20.129.1 > 172.20.187.253   NS ...

Question partie de Roubaix, réponse arrivée à Bordeaux, livrée au conteneur qui
n'avait rien demandé.

⚠ Ce n'est PAS le défaut du /64 corrigé la veille, dont la cause était un préfixe
annoncé depuis deux endroits. Celui-ci tient à l'anycast lui-même et serait resté
entier. En v6 la question ne se pose pas — le conteneur a une unicast pour sortir
et la VIP pour servir ; en v4 il n'a que la seconde.

Le premier NAT que ce dépôt écrit lui-même, choisi contre une v4 unicast par
conteneur parce que le /27 du site est rare. Il ne voit pas le servant : la
chaîne nat n'est consultée que pour le premier paquet d'un flux, jamais pour la
réponse à une requête entrante. Et il exclut `br-svc`, pour que le raft garde
l'identité de qui parle.

Mesuré après : les quatre résolveurs atteignent les quatre serveurs testés en v4
comme en v6, et les deux VIP v4 répondent toujours depuis les quatre nœuds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
thystips deleted branch feat/per-node-container-prefix 2026-08-14 02:16:28 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ThysTips_dn42/infra-dn42!38
No description provided.