feat(dns_rec) : le résolveur descend en conteneur, la traversée VRF disparaît #35

Merged
thystips merged 2 commits from feat/dns-rec-container into main 2026-08-12 02:11:48 +02:00
Owner

Étape 5 du plan conteneurs, et la dernière qui retire un mécanisme au lieu d'en déplacer un.

La traversée xvrf-dn42/xvrf-glb existait parce qu'un processus ne peut pas être dans deux tables de routage : la VRF pour joindre les serveurs de délégation DN42, la globale pour joindre internet. Un conteneur a nativement les deux pattes — eth0 sur br-svc, eth1 sur le pont NATé.

Ce qui disparaît

Avant Après
Instances Unbound deux, unbound-dn42 sous ip vrf exec + unbound-global une
Le lien entre elles paire veth 10.42.255.0/31, hors DN42
Marque netfilter la 3
Règles de pare-feu 2 en input (lignes 9a et 9b)
resolver_crossing dans as.yml déclaré
Validation DNSSEC racine mondiale d'un côté, dn42. de l'autre les deux, par la même instance

La dernière ligne est un gain : le nœud ne recevait pas le bit ad sur les réponses DN42 et « faisait confiance au veth ».

⚠ Le point qui commandait ce chantier, et il était silencieux

« DN42 au sens large n'obtient pas le clearnet » était appliqué par le routage, pas par Unbound. La configuration donnait allow à tout DN42 ; un pair extérieur qui demandait google.com échouait seulement parce que la récursion ne pouvait pas sortir de la VRF.

Un conteneur atteint internet. Reconduire la configuration telle quelle en aurait fait un résolveur ouvert pour tout DN42, sur une adresse anycast que le réseau entier connaît — et rien ne l'aurait signalé, le service se mettant seulement à mieux répondre.

D'où la vue dn42-only : local-zone "." refuse plus un transparent par zone DN42, les deux tirés de la même liste que les stub-zone.

Le test négatif passe sur les quatre, depuis fd42:d0d0:1::1 posée sur le lo du conteneur (jamais annoncée — le filtre d'export de Bird ne laisse passer que la VIP) :

ETRANGER  example.com=REFUSED   thystips.dn42=NOERROR
NOUS      example.com=NOERROR

C'est un geste et non un contrôle du rôle : unbound-checkconf accepte la version ouverte comme la fermée, et un contrôle qui interrogerait depuis la boucle locale tomberait dans l'ACL du nœud — il aurait l'air d'en être un.

⚠ Le seul mécanisme ajouté, contre cinq retirés

L'adresse eth1 du conteneur est épinglée à 10.42.0.53. Un résolveur est le seul service dont son propre hôte est client, et un processus en table globale ne joint aucun conteneur par DN42 — pas même celui de sa machine. La même adresse sur les quatre nœuds, chacun ayant son propre incusbr0 dans le même /24 : locale par construction, ce que la traversée garantissait aussi.

Le DNS= du nœud est une unité systemd et non un fichier, et les deux impasses sont mesurées :

  • resolved.conf n'a qu'une liste DNS= globale — y mettre le résolveur lui confierait aussi thystips.xyz et rendrait publique une zone privée ;
  • un .network demanderait que networkd gère incusbr0, que Incus possède (unmanaged, vérifié) — c'est la panne que network_anycast_owners documente déjà.

Ce que le déroulé a appris (second commit)

  1. Incus refuse un nom d'instance avec un _ → le conteneur s'appelle resolver, d'après son service anycast. Validation ajoutée au plugin.
  2. L'ordre est le NŒUD d'abord, contrairement à la bascule de la VIP d'ingress : sans la règle 179 ni la session côté nœud, le Bird du conteneur reste en Connect.
  3. networkctl reload ne supprime pas une interface dont le .netdev a disparu. Le veth survivait à son propre démontage, avec ses adresses, hors de tout fichier. C'est le contrôle du playbook qui l'a dit.
  4. L'image cloud n'a aucun client DNS → le test négatif documenté n'était pas exécutable.

Et une mesure écrite plutôt que corrigée : un Unbound qui démarre SERVFAIL sur le clearnet 4 à 6 s, DN42 répondant normalement. La VIP est annoncée pendant cette fenêtre.

Deux défauts de render-check, trouvés en rendant

Tous deux des contrôles qui ne contrôlaient pas :

  • les defaults du rôle rendu n'étaient pas résolus — un {{ … }} littéral atterrissait dans un ExecStart que systemd aurait refusé, lint au vert ;
  • les éléments d'une liste non plus : access-control: {{ dn42_prefix_v4 }} allow, soit la ligne même qui décide qui peut résoudre le clearnet à travers nous.

Le reste

Le gabarit Bird du conteneur est factorisé dans ansible/templates/, paramétré par le service anycast — deux copies de 140 lignes portant cinq défauts mesurés en production auraient divergé. Il ne nomme aucune variable de rôle, leçon de gai.conf.j2.

La boucle input sur dn42_anycast ne parcourt plus que ce que le nœud sert lui-même : un paquet vers la VIP d'un conteneur est forwardé et ne touche jamais cette chaîne. Les règles d'ingress y étaient mortes depuis la PR #25.

Et le pare-feu du résolveur n'a demandé aucune ligne écrite : déclarer anycast: resolver a suffi à rendre les quatre règles de forward et la session iBGP. C'est ce que « les ports viennent du service » achète, et ça ne se voit qu'au second service.

Déploiement

Les 4 nœuds et les 4 nouveaux conteneurs, un nœud à la fois. Sur chacun : plus de xvrf, plus de marque 3, Unbound inactif sur l'hôte, VIP absente de lo-dn42, route de la VIP via cnt_resolver, sonde anycast répondant avec le bon NSID en v4 et v6, whois.dn42 résolu par le conteneur, thystips.xyz toujours par le plan privé. NetBox synchronisé, --check à zéro.

Reste un geste, à décider : 8 assignations périmées de la VIP sur les lo-dn42 des nœuds. Six passent le garde-fou (tag infra-dn42 seul), mais les ids 421 et 422 portent un dns_name que le modèle ne pose jamais — donc annotés par quelqu'un d'autre. Non supprimées.

Étape 5 du plan conteneurs, et la dernière qui **retire** un mécanisme au lieu d'en déplacer un. La traversée `xvrf-dn42`/`xvrf-glb` existait parce qu'un processus ne peut pas être dans deux tables de routage : la VRF pour joindre les serveurs de délégation DN42, la globale pour joindre internet. Un conteneur a nativement les deux pattes — `eth0` sur `br-svc`, `eth1` sur le pont NATé. ## Ce qui disparaît | | Avant | Après | |---|---|---| | Instances Unbound | **deux**, `unbound-dn42` sous `ip vrf exec` + `unbound-global` | **une** | | Le lien entre elles | paire veth `10.42.255.0/31`, hors DN42 | — | | Marque netfilter | la **3** | — | | Règles de pare-feu | 2 en `input` (lignes 9a et 9b) | — | | `resolver_crossing` dans `as.yml` | déclaré | — | | Validation DNSSEC | racine mondiale d'un côté, `dn42.` de l'autre | **les deux**, par la même instance | La dernière ligne est un gain : le nœud ne recevait pas le bit `ad` sur les réponses DN42 et « faisait confiance au veth ». ## ⚠ Le point qui commandait ce chantier, et il était silencieux « DN42 au sens large n'obtient pas le clearnet » était appliqué par le **routage**, pas par Unbound. La configuration donnait `allow` à tout DN42 ; un pair extérieur qui demandait `google.com` échouait seulement parce que la récursion ne pouvait pas sortir de la VRF. Un conteneur atteint internet. Reconduire la configuration telle quelle en aurait fait un **résolveur ouvert pour tout DN42**, sur une adresse anycast que le réseau entier connaît — et rien ne l'aurait signalé, le service se mettant seulement à *mieux* répondre. D'où la vue `dn42-only` : `local-zone "." refuse` plus un `transparent` par zone DN42, les deux tirés de la **même** liste que les `stub-zone`. **Le test négatif passe sur les quatre**, depuis `fd42:d0d0:1::1` posée sur le `lo` du conteneur (jamais annoncée — le filtre d'export de Bird ne laisse passer que la VIP) : ``` ETRANGER example.com=REFUSED thystips.dn42=NOERROR NOUS example.com=NOERROR ``` C'est un **geste** et non un contrôle du rôle : `unbound-checkconf` accepte la version ouverte comme la fermée, et un contrôle qui interrogerait depuis la boucle locale tomberait dans l'ACL du nœud — il aurait l'air d'en être un. ## ⚠ Le seul mécanisme ajouté, contre cinq retirés L'adresse `eth1` du conteneur est **épinglée à `10.42.0.53`**. Un résolveur est le seul service dont son propre hôte est client, et un processus en table globale ne joint aucun conteneur par DN42 — pas même celui de sa machine. La même adresse sur les quatre nœuds, chacun ayant son propre `incusbr0` dans le même /24 : **locale par construction**, ce que la traversée garantissait aussi. Le `DNS=` du nœud est une unité systemd et non un fichier, et les deux impasses sont mesurées : - `resolved.conf` n'a qu'une liste `DNS=` **globale** — y mettre le résolveur lui confierait aussi `thystips.xyz` et rendrait publique une zone privée ; - un `.network` demanderait que networkd gère `incusbr0`, que **Incus possède** (`unmanaged`, vérifié) — c'est la panne que `network_anycast_owners` documente déjà. ## Ce que le déroulé a appris (second commit) 1. **Incus refuse un nom d'instance avec un `_`** → le conteneur s'appelle `resolver`, d'après son service anycast. Validation ajoutée au plugin. 2. **L'ordre est le NŒUD d'abord**, contrairement à la bascule de la VIP d'ingress : sans la règle 179 ni la session côté nœud, le Bird du conteneur reste en `Connect`. 3. ⚠ **`networkctl reload` ne supprime pas une interface dont le `.netdev` a disparu.** Le veth survivait à son propre démontage, avec ses adresses, hors de tout fichier. C'est le contrôle du playbook qui l'a dit. 4. **L'image `cloud` n'a aucun client DNS** → le test négatif documenté n'était pas exécutable. Et une mesure écrite plutôt que corrigée : un Unbound qui démarre **SERVFAIL sur le clearnet 4 à 6 s**, DN42 répondant normalement. La VIP est annoncée pendant cette fenêtre. ## Deux défauts de `render-check`, trouvés en rendant Tous deux des contrôles qui ne contrôlaient pas : - les defaults du **rôle rendu** n'étaient pas résolus — un `{{ … }}` littéral atterrissait dans un `ExecStart` que systemd aurait refusé, lint au vert ; - les **éléments d'une liste** non plus : `access-control: {{ dn42_prefix_v4 }} allow`, soit la ligne même qui décide qui peut résoudre le clearnet à travers nous. ## Le reste Le gabarit Bird du conteneur est factorisé dans `ansible/templates/`, paramétré par le service anycast — deux copies de 140 lignes portant cinq défauts mesurés en production auraient divergé. Il ne nomme aucune variable de rôle, leçon de `gai.conf.j2`. La boucle `input` sur `dn42_anycast` ne parcourt plus que ce que le nœud sert **lui-même** : un paquet vers la VIP d'un conteneur est forwardé et ne touche jamais cette chaîne. Les règles d'`ingress` y étaient mortes depuis la PR #25. Et le pare-feu du résolveur n'a demandé **aucune ligne écrite** : déclarer `anycast: resolver` a suffi à rendre les quatre règles de `forward` et la session iBGP. C'est ce que « les ports viennent du service » achète, et ça ne se voit qu'au second service. ## Déploiement Les 4 nœuds et les 4 nouveaux conteneurs, un nœud à la fois. Sur chacun : plus de `xvrf`, plus de marque 3, Unbound inactif sur l'hôte, VIP absente de `lo-dn42`, route de la VIP via `cnt_resolver`, sonde anycast répondant avec le bon NSID en v4 et v6, `whois.dn42` résolu par le conteneur, `thystips.xyz` toujours par le plan privé. NetBox synchronisé, `--check` à zéro. ⚠ **Reste un geste, à décider** : 8 assignations périmées de la VIP sur les `lo-dn42` des nœuds. Six passent le garde-fou (tag `infra-dn42` seul), mais **les ids 421 et 422 portent un `dns_name` que le modèle ne pose jamais** — donc annotés par quelqu'un d'autre. Non supprimées.
Étape 5 du plan conteneurs, et la dernière qui RETIRE un mécanisme au lieu d'en
déplacer un. La traversée `xvrf-dn42`/`xvrf-glb` existait parce qu'un processus ne
peut pas être dans deux tables de routage : la VRF pour joindre les serveurs de
délégation DN42, la globale pour joindre internet. Un conteneur a nativement les
deux pattes.

Ce qui part avec elle : la SECONDE instance Unbound, la paire veth, la marque
netfilter 3, deux règles d'`input` (lignes 9a et 9b de la matrice), et
`resolver_crossing` dans as.yml.

⚠ LE POINT QUI COMMANDE CE CHANTIER, et il est silencieux. « DN42 au sens large
n'obtient pas le clearnet » était appliqué par le ROUTAGE et non par Unbound : la
configuration donnait `allow` à tout DN42, et un pair extérieur échouait seulement
parce que la récursion ne pouvait pas sortir de la VRF. Un conteneur, lui, atteint
internet — reconduire la configuration telle quelle en aurait fait un RÉSOLVEUR
OUVERT sur une adresse anycast que le réseau entier connaît, sans que rien ne le
signale : le service se serait mis à MIEUX répondre.

D'où la vue `dn42-only` : `local-zone "." refuse` plus un `transparent` par zone
DN42, les deux tirés de la même liste que les `stub-zone`. Le test négatif est un
GESTE, écrit dans le README — `unbound-checkconf` accepte la version ouverte comme
la fermée, et un contrôle qui interrogerait depuis la boucle locale aurait l'air
d'en être un.

⚠ LE SEUL MÉCANISME AJOUTÉ, contre cinq retirés : l'adresse `eth1` du conteneur est
épinglée à 10.42.0.53. Un résolveur est le seul service dont son propre hôte est
client, et un processus en table globale ne joint aucun conteneur par DN42, pas même
celui de sa machine. La même adresse sur les quatre nœuds — chacun a son propre
`incusbr0` dans le même /24 — donc LOCALE PAR CONSTRUCTION, ce que la traversée
garantissait aussi.

Le `DNS=` du nœud est une unité systemd et non un fichier, et les deux impasses sont
mesurées : `resolved.conf` n'a qu'une liste GLOBALE (y mettre le résolveur lui
confierait `thystips.xyz` et rendrait publique une zone privée), et un `.network`
demanderait que networkd gère `incusbr0`, que Incus possède — `unmanaged`, vérifié.

DEUX DÉFAUTS DE render-check TROUVÉS EN RENDANT, tous deux des contrôles qui ne
contrôlaient pas :

  - les defaults du rôle RENDU n'étaient pas résolus, donc un `{{ … }}` atterrissait
    littéralement dans le fichier vérifié. Trouvé sur un `ExecStart` que systemd
    aurait refusé, et que le lint validait ;
  - les ÉLÉMENTS d'une liste ne l'étaient pas non plus — `access-control:
    {{ dn42_prefix_v4 }} allow`, soit la ligne même qui décide qui peut résoudre le
    clearnet à travers nous.

Le gabarit Bird du conteneur est factorisé dans `ansible/templates/`, paramétré par
le service anycast : deux copies de 140 lignes portant cinq défauts mesurés en
production auraient fini par diverger. Il ne nomme aucune variable de rôle, leçon de
`gai.conf.j2`.

Et la boucle `input` sur `dn42_anycast` ne parcourt plus que ce que le nœud sert
LUI-MÊME : un paquet vers la VIP d'un conteneur est forwardé et ne touche jamais
cette chaîne. Les règles d'`ingress` y étaient mortes depuis la PR #25.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
Corrections trouvées en basculant les quatre nœuds, aucune visible au lint.

1. INCUS REFUSE UN NOM D'INSTANCE AVEC UN `_`. Le conteneur s'appelait `dns_rec`,
   d'après le rôle qui le place ; il s'appelle `resolver`, d'après le service anycast
   qu'il porte — ce que `containers.yml` énonçait déjà comme convention et qui est
   aussi une contrainte. Le plugin d'inventaire le vérifie désormais : l'oubli
   échouait au DÉPLOIEMENT, sur « Invalid instance name » que rien ne rattachait au
   fichier fautif.

2. L'ORDRE DU DÉROULÉ EST LE NŒUD D'ABORD, contrairement à ce que la bascule de la
   VIP d'ingress laissait attendre et à ce que le README disait. Sans la règle 179 et
   sans la session côté nœud, le Bird du conteneur reste en `Connect` et le rôle
   échoue sur son propre contrôle. Le prix est que le nœud ne résout plus `.dn42`
   pendant deux à quatre minutes — deux consommateurs non critiques, contre un trou
   dans un service anycast servi à tout le réseau.

3. ⚠ `networkctl reload` NE SUPPRIME PAS UNE INTERFACE dont le `.netdev` a disparu.
   Le veth de la traversée survivait à son propre démontage — UP, avec ses adresses,
   et hors de tout fichier de configuration, donc invisible à la prochaine relecture
   du dépôt. networkd crée ce qu'un `.netdev` décrit, il ne détruit pas ce dont un
   `.netdev` cesse de parler. C'est le contrôle en bas du playbook qui l'a dit.

4. L'IMAGE `cloud` N'EMBARQUE AUCUN CLIENT DNS, donc le test négatif que le README
   décrit n'était pas exécutable. Un contrôle qu'on ne peut pas lancer là où il compte
   n'est pas un contrôle : `knot-dnsutils` rejoint le rôle, pour la même raison que
   `kdig` est sur les nœuds.

Et une mesure, écrite au README plutôt que corrigée : un Unbound qui vient de démarrer
SERVFAIL sur le clearnet pendant 4 à 6 secondes, le temps d'amorcer l'ancre de la
racine, DN42 répondant normalement. La VIP étant posée par `ExecStartPost`, elle est
annoncée pendant cette fenêtre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGwvXLVXJ2PrRFwJTB3Xp9
thystips deleted branch feat/dns-rec-container 2026-08-12 02:11:48 +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!35
No description provided.