Serveur DHCP intégré — remplacer dnsmasq par un binaire contrôlable #46

Closed
opened 2026-08-25 19:34:17 +00:00 by nicolas.boufideline · 11 comments

Contexte

Le DHCP est aujourd'hui assuré par dnsmasq, une instance par subnet, lancée dans le netns du VPC par run-dnsmasq-in-netns.sh via l'unit dnsmasq@<vpc>_<bridge>. Sa configuration est un fichier généré à la création du subnet, et jamais retouché ensuite.

Ce modèle atteint sa limite avec #33 (multi-subnet dans les VM).

Ce qui bloque #33 — une VM multi-interfaces ne doit recevoir la route par défaut que sur une de ses interfaces. Or la configuration DHCP est au niveau du subnet, pas de la VM. dnsmasq sait le faire par tags (set: dans dhcp-host, tag: dans dhcp-option), mais cela impose de toucher à sa configuration à chaque création de VM, donc d'ajouter une dépendance à un dnsmasq vivant et rechargeable sur le chemin critique du démarrage d'une VM.

Conception retenue

Un second binaire, sur le modèle de metadata : une instance par subnet, lancée dans le netns par une unit template dhcp@<vpc>_<subnet> et un script wrapper.

Contrôle par socket Unix — /run/two/dhcp/<vpc>_<subnet>.sock. L'agent y pousse l'état désiré de façon idempotente : configuration du subnet à la création, réservations et options à chaque création ou suppression de VM. Il peut aussi l'interroger : état courant, et surtout « qu'enverrais-tu à cette MAC ? ».

Le motif est déjà en production : l'agent parle à QEMU via la socket QMP sans netns.Call, alors que QEMU tourne dans le netns (internal/vm/delete.go). ip netns exec n'isole que le réseau, pas le système de fichiers.

État auto-persisté — le processus écrit son état dans /run/two/dhcp/<vpc>_<subnet>.state, le lit au démarrage ou le crée vide. Le fichier lui appartient : l'agent ne l'écrit jamais. Ce n'est pas un contrat entre deux composants mais un détail interne, donc il reflète toujours ce que le serveur a en mémoire. Il survit à un redémarrage du processus sans dépendre de l'agent ni de son API.

/run plutôt que /var/lib : cet état n'a aucun sens sans le netns, qui ne survit pas au redémarrage de l'host. Un fichier survivant ferait croire à un subnet fonctionnel dont le netns a disparu — piège que le .conf dnsmasq dans /etc a aujourd'hui.

Le processus ne supprime jamais son fichier, y compris à l'arrêt : il ne peut pas distinguer un arrêt définitif d'un simple redémarrage, et chaque déploiement effacerait l'état. Seul l'agent sait.

Responsabilités de l'agent :

Moment Action
Création du subnet supprimer un .state résiduel, démarrer l'unit, pousser l'état
Création / suppression de VM ordre sur la socket
Suppression du subnet arrêter l'unit, puis supprimer le .state

La suppression du résidu à la création est ce qui évite qu'un fichier orphelin, laissé par un arrêt sale, soit resservi à un subnet recréé du même nom.

Réconciliation par le watchdog — la base fait autorité, le fichier d'état est un cache. Un ordre perdu — envoyé par l'agent, processus mort avant de persister — les fait diverger. Le watchdog interroge la socket et compare à la base : c'est exactement son rôle, observer et notifier sans réparer, et c'est le seul moyen d'être sûr de ce qui tourne réellement.

Points à trancher à l'implémentation

  • Format du protocole de socket : JSON par ligne, ou binaire.
  • Encodage de l'option 121 en forme compacte RFC 3442 — le piège principal, à tester avec plusieurs clients.
  • Faut-il conserver des baux, ou les réservations statiques suffisent-elles ?
  • Comment le watchdog compare : état complet, ou empreinte.

Hors périmètre

Pas de DNS — dnsmasq tourne déjà avec -p0. Pas de pool dynamique, pas de PXE, pas de DHCPv6.

Préalable

Ne pas démarrer avant la sortie de 0.1.0. #33 sera livrée avec dhcp-hostsdir sur dnsmasq ; ce ticket est la cible, pas le chemin.

## Contexte Le DHCP est aujourd'hui assuré par dnsmasq, une instance par subnet, lancée dans le netns du VPC par `run-dnsmasq-in-netns.sh` via l'unit `dnsmasq@<vpc>_<bridge>`. Sa configuration est un fichier généré à la **création du subnet**, et jamais retouché ensuite. Ce modèle atteint sa limite avec #33 (multi-subnet dans les VM). **Ce qui bloque #33** — une VM multi-interfaces ne doit recevoir la route par défaut que sur **une** de ses interfaces. Or la configuration DHCP est au niveau du subnet, pas de la VM. dnsmasq sait le faire par tags (`set:` dans `dhcp-host`, `tag:` dans `dhcp-option`), mais cela impose de toucher à sa configuration à chaque création de VM, donc d'ajouter une dépendance à un dnsmasq vivant et rechargeable sur le chemin critique du démarrage d'une VM. ## Conception retenue Un second binaire, sur le modèle de `metadata` : une instance par subnet, lancée dans le netns par une unit template `dhcp@<vpc>_<subnet>` et un script wrapper. **Contrôle par socket Unix** — `/run/two/dhcp/<vpc>_<subnet>.sock`. L'agent y pousse l'état désiré de façon idempotente : configuration du subnet à la création, réservations et options à chaque création ou suppression de VM. Il peut aussi l'interroger : état courant, et surtout *« qu'enverrais-tu à cette MAC ? »*. Le motif est déjà en production : l'agent parle à QEMU via la socket QMP sans `netns.Call`, alors que QEMU tourne dans le netns (`internal/vm/delete.go`). `ip netns exec` n'isole que le réseau, pas le système de fichiers. **État auto-persisté** — le processus écrit son état dans `/run/two/dhcp/<vpc>_<subnet>.state`, le lit au démarrage ou le crée vide. Le fichier lui appartient : l'agent ne l'écrit jamais. Ce n'est pas un contrat entre deux composants mais un détail interne, donc il reflète toujours ce que le serveur a en mémoire. Il survit à un redémarrage du processus sans dépendre de l'agent ni de son API. `/run` plutôt que `/var/lib` : cet état n'a aucun sens sans le netns, qui ne survit pas au redémarrage de l'host. Un fichier survivant ferait croire à un subnet fonctionnel dont le netns a disparu — piège que le `.conf` dnsmasq dans `/etc` a aujourd'hui. **Le processus ne supprime jamais son fichier**, y compris à l'arrêt : il ne peut pas distinguer un arrêt définitif d'un simple redémarrage, et chaque déploiement effacerait l'état. Seul l'agent sait. Responsabilités de l'agent : | Moment | Action | |---|---| | Création du subnet | supprimer un `.state` résiduel, démarrer l'unit, pousser l'état | | Création / suppression de VM | ordre sur la socket | | Suppression du subnet | arrêter l'unit, puis supprimer le `.state` | La suppression du résidu **à la création** est ce qui évite qu'un fichier orphelin, laissé par un arrêt sale, soit resservi à un subnet recréé du même nom. **Réconciliation par le watchdog** — la base fait autorité, le fichier d'état est un cache. Un ordre perdu — envoyé par l'agent, processus mort avant de persister — les fait diverger. Le watchdog interroge la socket et compare à la base : c'est exactement son rôle, observer et notifier sans réparer, et c'est le seul moyen d'être sûr de ce qui tourne réellement. ## Points à trancher à l'implémentation - Format du protocole de socket : JSON par ligne, ou binaire. - Encodage de l'option 121 en forme compacte RFC 3442 — le piège principal, à tester avec plusieurs clients. - Faut-il conserver des baux, ou les réservations statiques suffisent-elles ? - Comment le watchdog compare : état complet, ou empreinte. ## Hors périmètre Pas de DNS — dnsmasq tourne déjà avec `-p0`. Pas de pool dynamique, pas de PXE, pas de DHCPv6. ## Préalable **Ne pas démarrer avant la sortie de 0.1.0.** #33 sera livrée avec `dhcp-hostsdir` sur dnsmasq ; ce ticket est la cible, pas le chemin.
Author
Owner

Prochaine étape

Prochaine étape
Author
Owner

on commence

on commence
Author
Owner

c'est en cour

c'est en cour
Author
Owner

ca avance

ca avance
Author
Owner

toujours pas fini

toujours pas fini
Author
Owner

toujours pas

toujours pas
Author
Owner

tag mis

tag mis
Author
Owner

soft deployer

soft deployer
Author
Owner

Éléments à vérifier avant de considérer le backend two éprouvé

Le code est livré sur feature-46 (12 commits, préversion 0.2.0rc002 et suivantes). Tout est
couvert par des tests, dont le dialogue agent ↔ serveur contre une vraie socket. Rien n'a encore
été validé sur un réseau réel
: les trois points ci-dessous ne sont pas testables hors Linux et
conditionnent la confiance qu'on peut accorder au backend.

1. Plusieurs instances sur UDP/67 dans un même netns — le plus important

C'est le seul mode de panne qui serait silencieux et grave.

Le netns est par VPC, le serveur par subnet : N processus écoutent donc UDP/67 dans le même netns,
un par bridge. server4.NewIPv4UDPConn pose SO_REUSEADDR, SO_REUSEPORT et le bind à
l'interface (SO_BINDTODEVICE) — le pendant du --interface=<bridge> --bind-interfaces de
dnsmasq — ce qui devrait suffire à ce que le noyau livre chaque datagramme au bon socket. Mais un
DISCOVER part en broadcast vers 255.255.255.255:67, et c'est exactement le cas où une erreur de
bind livrerait le paquet au serveur voisin.

Procédure : un VPC, deux subnets, une VM dans chacun, démarrées l'une après l'autre puis
simultanément.

Ce qui doit être vrai : chaque VM reçoit une adresse de son subnet. Une VM qui reçoit
l'adressage du subnet d'à côté est le scénario à écarter — et il ne lèverait aucune erreur.

Contrôle : probe sur chaque socket doit ne connaître que les MAC de son propre subnet.

echo '{"verb":"get-state"}' \
  | socat - UNIX-CONNECT:/run/two/dhcp/<netns>_<bridge>.sock | jq .state.hosts

2. Deux clients DHCP réels

Le serveur n'a été confronté qu'à des paquets fabriqués en test.

Procédure : provisionner une VM complète sur une image à systemd-networkd, puis sur une
image à dhclient.

Ce qui doit être vrai — l'adresse, et surtout les trois routes :

  • la route par défaut ;
  • la route vers le CIDR du VPC, avec interface_ip comme next-hop ;
  • la route /32 vers 169.254.169.254, également via interface_ip.

La troisième est celle qui conditionne le provisionnement cloud-init. Si cloud-init ne s'applique
pas alors que la VM a une adresse, c'est elle qu'il faut regarder en premier.

3. INIT-REBOOT d'un guest réclamant une ancienne adresse

Ce point tranche la décision « pas de NAK », prise volontairement.

Un client qui redémarre en réclamant une adresse qui n'est pas la sienne (image golden portant un
bail en cache, subnet recréé sur un autre CIDR) reçoit de notre part un ACK portant l'adresse
réservée — jamais un DHCPNAK. La RFC dit qu'il doit prendre le yiaddr de l'ACK.

Procédure : démarrer une VM depuis une image ayant un bail d'un autre réseau en cache.

Ce qui doit être vrai : le guest se configure avec l'adresse annoncée. S'il s'obstine, il doit
au pire retomber sur un DISCOVER et converger — plus lentement, mais converger.

Si l'un des deux clients boucle longtemps, le sujet du NAK se rouvre, sur ce cas réel et non
plus sur une hypothèse.

4. Bascule et retour arrière

La procédure est documentée dans docs/exploitation/configuration.rst. Elle suppose un
hyperviseur vide : l'option ne migre rien, un subnet déjà créé reste servi par le serveur qui
l'a créé.

À vérifier : que le retour arrière (two → dnsmasq, même procédure) fonctionne aussi. Il
n'a jamais été exécuté, et c'est la porte de sortie en cas de problème en production.


Sur le symptôme observé en lab (dnsmasq démarré malgré backend: two)

Cause probable identifiée et reproduite : LoadConfig ignorait l'erreur de ReadInConfig. Un YAML
invalide faisait ignorer tout le fichier en silence, chaque valeur retombant sur son défaut — et
le défaut de dhcp.backend est dnsmasq.

Corrigé : un fichier présent mais invalide fait désormais échouer le démarrage de l'agent. Un
fichier absent reste toléré, comportement délibéré et couvert par un test existant.

Le contrôle à passer sur l'hyperviseur si le symptôme réapparaît :

python3 -c 'import yaml; yaml.safe_load(open("/etc/two/agent.yml")); print("yaml ok")'

Seconde hypothèse à écarter si le YAML est valide : le subnet existait avant la bascule. Le test
qui départage est de le supprimer puis de le recréer, et de regarder si
dhcp@<netns>_<bridge>.service apparaît.


Hors périmètre de ce ticket, mais dans son sillage

  • Retrait de dnsmasq — à ouvrir quand tous les hyperviseurs seront passés en backend: two.
    Portera run-dnsmasq-in-netns.sh, dnsmasq@.service, dhcp.DefaultConfDir, les
    hostsdir/optsdir, la clé dhcp.backend elle-même et le paquet Debian.
  • #47 — course à l'arrêt dans le harnais de test de internal/dispatcher/agent, qui rend une
    passe complète de go test ./... sur deux instable. S'est manifestée pendant le développement de
    ce ticket.
  • #48 — la CI n'exécute aucun test et ne se déclenche que sur un tag de release, ce qui
    explique qu'aucun de ces deux défauts n'ait été signalé automatiquement.
  • Tests Linux only — ConfigureSubnet et TeardownSubnet des deux backends, Dnsmasq.DelVM
    et le main de cmd/dhcp appellent systemd ou exigent un bridge réel. Le reste, y compris tout
    le dialogue par socket, est couvert sur toute plateforme.
## Éléments à vérifier avant de considérer le backend `two` éprouvé Le code est livré sur `feature-46` (12 commits, préversion `0.2.0rc002` et suivantes). Tout est couvert par des tests, dont le dialogue agent ↔ serveur contre une vraie socket. **Rien n'a encore été validé sur un réseau réel** : les trois points ci-dessous ne sont pas testables hors Linux et conditionnent la confiance qu'on peut accorder au backend. ### 1. Plusieurs instances sur UDP/67 dans un même netns — le plus important C'est le seul mode de panne qui serait **silencieux et grave**. Le netns est par VPC, le serveur par subnet : N processus écoutent donc UDP/67 dans le même netns, un par bridge. `server4.NewIPv4UDPConn` pose `SO_REUSEADDR`, `SO_REUSEPORT` et le bind à l'interface (`SO_BINDTODEVICE`) — le pendant du `--interface=<bridge> --bind-interfaces` de dnsmasq — ce qui *devrait* suffire à ce que le noyau livre chaque datagramme au bon socket. Mais un DISCOVER part en broadcast vers `255.255.255.255:67`, et c'est exactement le cas où une erreur de bind livrerait le paquet au serveur voisin. **Procédure** : un VPC, deux subnets, une VM dans chacun, démarrées l'une après l'autre puis simultanément. **Ce qui doit être vrai** : chaque VM reçoit une adresse de *son* subnet. Une VM qui reçoit l'adressage du subnet d'à côté est le scénario à écarter — et il ne lèverait aucune erreur. **Contrôle** : `probe` sur chaque socket doit ne connaître que les MAC de son propre subnet. ```bash echo '{"verb":"get-state"}' \ | socat - UNIX-CONNECT:/run/two/dhcp/<netns>_<bridge>.sock | jq .state.hosts ``` ### 2. Deux clients DHCP réels Le serveur n'a été confronté qu'à des paquets fabriqués en test. **Procédure** : provisionner une VM complète sur une image à **systemd-networkd**, puis sur une image à **dhclient**. **Ce qui doit être vrai** — l'adresse, et surtout **les trois routes** : - la route par défaut ; - la route vers le CIDR du VPC, avec `interface_ip` comme next-hop ; - la route `/32` vers `169.254.169.254`, également via `interface_ip`. La troisième est celle qui conditionne le provisionnement cloud-init. Si cloud-init ne s'applique pas alors que la VM a une adresse, c'est elle qu'il faut regarder en premier. ### 3. INIT-REBOOT d'un guest réclamant une ancienne adresse Ce point tranche la décision **« pas de NAK »**, prise volontairement. Un client qui redémarre en réclamant une adresse qui n'est pas la sienne (image golden portant un bail en cache, subnet recréé sur un autre CIDR) reçoit de notre part un ACK portant l'adresse réservée — jamais un `DHCPNAK`. La RFC dit qu'il doit prendre le `yiaddr` de l'ACK. **Procédure** : démarrer une VM depuis une image ayant un bail d'un autre réseau en cache. **Ce qui doit être vrai** : le guest se configure avec l'adresse annoncée. S'il s'obstine, il doit au pire retomber sur un DISCOVER et converger — plus lentement, mais converger. **Si l'un des deux clients boucle longtemps**, le sujet du NAK se rouvre, sur ce cas réel et non plus sur une hypothèse. ### 4. Bascule et retour arrière La procédure est documentée dans `docs/exploitation/configuration.rst`. Elle suppose un **hyperviseur vide** : l'option ne migre rien, un subnet déjà créé reste servi par le serveur qui l'a créé. **À vérifier** : que le retour arrière (`two` → `dnsmasq`, même procédure) fonctionne aussi. Il n'a jamais été exécuté, et c'est la porte de sortie en cas de problème en production. --- ## Sur le symptôme observé en lab (dnsmasq démarré malgré `backend: two`) Cause probable identifiée et reproduite : `LoadConfig` ignorait l'erreur de `ReadInConfig`. Un YAML invalide faisait ignorer **tout le fichier** en silence, chaque valeur retombant sur son défaut — et le défaut de `dhcp.backend` est `dnsmasq`. Corrigé : un fichier présent mais invalide fait désormais échouer le démarrage de l'agent. Un fichier absent reste toléré, comportement délibéré et couvert par un test existant. Le contrôle à passer sur l'hyperviseur si le symptôme réapparaît : ```bash python3 -c 'import yaml; yaml.safe_load(open("/etc/two/agent.yml")); print("yaml ok")' ``` Seconde hypothèse à écarter si le YAML est valide : le subnet existait avant la bascule. Le test qui départage est de le supprimer puis de le recréer, et de regarder si `dhcp@<netns>_<bridge>.service` apparaît. --- ## Hors périmètre de ce ticket, mais dans son sillage - **Retrait de dnsmasq** — à ouvrir quand tous les hyperviseurs seront passés en `backend: two`. Portera `run-dnsmasq-in-netns.sh`, `dnsmasq@.service`, `dhcp.DefaultConfDir`, les `hostsdir`/`optsdir`, la clé `dhcp.backend` elle-même et le paquet Debian. - **#47** — course à l'arrêt dans le harnais de test de `internal/dispatcher/agent`, qui rend une passe complète de `go test ./...` sur deux instable. S'est manifestée pendant le développement de ce ticket. - **#48** — la CI n'exécute aucun test et ne se déclenche que sur un tag de release, ce qui explique qu'aucun de ces deux défauts n'ait été signalé automatiquement. - **Tests Linux only** — `ConfigureSubnet` et `TeardownSubnet` des deux backends, `Dnsmasq.DelVM` et le `main` de `cmd/dhcp` appellent systemd ou exigent un bridge réel. Le reste, y compris tout le dialogue par socket, est couvert sur toute plateforme.
Author
Owner

Validation en lab — 2026-09-10

Tests menés sur un hyperviseur de lab avec dhcp.backend: two.

Les adresses et noms de VM ci-dessous sont transposés sur l'adressage d'exemple de la
documentation
— le dépôt est public et le plan d'adressage du lab n'a pas à y figurer. La
structure des traces est celle observée : deux subnets d'un même VPC, une gateway distincte de
l'interface_ip, et la route /32 metadata pointant sur cette dernière.

1. Plusieurs instances sur UDP/67 dans un même netns — validé

C'était le risque principal, et le seul mode de panne qui aurait été silencieux. Deux serveurs dans
le netns vp-admin, chacun ne connaissant que son subnet :

br-000001 → 10.0.6.2   (i-db)
br-000000 → 10.0.5.10  (i-web)

i-web a bien reçu une adresse de son subnet : son DISCOVER en broadcast a donc atteint
br-000000 et non le serveur voisin. Le bind à l'interface posé par server4.NewIPv4UDPConn fait
son travail, et le pire scénario — un guest recevant l'adressage du subnet d'à côté, sans qu'aucune
erreur ne soit levée — est écarté.

2. Routes distribuées au guest — validé

default via 10.0.5.254 dev ens3 proto dhcp src 10.0.5.10 metric 100
169.254.169.254 via 10.0.5.1 dev ens3 proto dhcp src 10.0.5.10 metric 100
10.0.5.0/24 dev ens3 proto kernel scope link src 10.0.5.10 metric 100
  • route par défaut vers la gateway fournie ;
  • route /32 vers le serveur de métadonnées avec l'interface_ip du subnet en next-hop —
    c'est l'invariant critique : avec un autre next-hop la trame est commutée en L2 et ne traverse
    jamais PREROUTING, donc la DNAT ne la voit pas et cloud-init échoue ;
  • la troisième route est posée par le noyau depuis l'adresse, pas par le DHCP.

Pas de route vers le CIDR du VPC, et c'est correct : internal/subnet/routing.go ne l'émet pas
en mode bridge.

3. Chemin vers le serveur de métadonnées — validé

Depuis le guest, curl http://169.254.169.254/meta-data renvoie bien instance-id et
local-hostname. La chaîne complète est donc prouvée : route annoncée par le DHCP → next-hop
interface_ip → traversée de PREROUTING → DNAT → serveur de métadonnées.

4. Réservations poussées et retirées — validé

Après suppression d'une VM d'un subnet puis recréation sur l'autre :

br-000000 → 0 hôte
br-000001 → deux réservations, i-db et i-web

L'ordre del-host a bien été appliqué, set-host aussi sur l'autre subnet, et deux réservations
coexistent sur un même subnet avec les MAC attendues, dérivées de l'index de l'IP.

À noter au passage : les MAC ressortent en minuscules côté serveur alors que dhcp.Entries les
écrit en majuscules en base. La normalisation appliquée par le watchdog dans son diff n'est donc
pas théorique — sans elle chaque hôte serait signalé à la fois absent et obsolète.

5. Bail périmé au redémarrage — validé

La VM a été recréée sur son disque existant après changement de subnet : son guest portait en
cache un bail de l'autre réseau, avec un autre masque et une autre gateway. Il s'est configuré avec
l'adresse annoncée et répond au ping.

C'est le scénario qui conditionnait la décision « pas de NAK », et elle tient sur un cas réel :
un client à bail périmé converge sans blocage. Faute de capture on ne sait pas s'il est passé par
INIT-REBOOT ou s'il a repris en DISCOVER — le résultat est ce qui comptait.

6. Bascule et retour arrière — validé

Le passage dnsmasq → two et le retour fonctionnent tous deux, selon la procédure documentée
dans docs/exploitation/configuration.rst. Le retour arrière n'avait jamais été exécuté : c'est la
porte de sortie en cas de problème en production, elle est maintenant éprouvée.


Ce qui n'est pas validé, et qui est assumé

Un second client DHCP. Les tests ont porté sur un guest de la famille RHEL, dont le DHCP est le
client interne de NetworkManager (dhcp=internal, défaut depuis RHEL 9) — et non
systemd-networkd. Le client restant à éprouver est donc systemd-networkd, base de code distincte et
défaut de la famille Debian.

Reporté en P999, avec la procédure et le critère de réussite. ISC dhclient est écarté
volontairement : en fin de vie depuis 2022 et en voie de disparition des distributions. S'il devait
être testé, son mode de panne attendu est le hook rfc3442-classless-static-routes absent —
dhclient reçoit l'option 121 mais n'installe aucune route sans lui, ce qui se diagnostique dans le
guest et non côté serveur.

Écart au ticket, assumé

La réconciliation du watchdog porte sur les réservations, pas sur la configuration du subnet :
celle-ci dépend de la route par défaut de l'host, lue à chaud, et la reproduire produirait de faux
écarts au premier changement de route. Un serveur dépourvu de configuration est en revanche
signalé. C'est dans la liste des réservations que les ordres se perdent, donc c'est elle qui est
comparée.

Suites ouvertes

  • Retrait de dnsmasq — à ouvrir quand tous les hyperviseurs seront passés en backend: two.
    Portera run-dnsmasq-in-netns.sh, dnsmasq@.service, dhcp.DefaultConfDir, les
    hostsdir/optsdir, la clé dhcp.backend elle-même et le paquet Debian.
  • #47 — course dans le harnais de test de internal/dispatcher/agent.
  • #48 — la CI n'exécute aucun test et ne se déclenche que sur un tag de release.
## Validation en lab — 2026-09-10 Tests menés sur un hyperviseur de lab avec `dhcp.backend: two`. > Les adresses et noms de VM ci-dessous sont **transposés sur l'adressage d'exemple de la > documentation** — le dépôt est public et le plan d'adressage du lab n'a pas à y figurer. La > structure des traces est celle observée : deux subnets d'un même VPC, une gateway distincte de > l'`interface_ip`, et la route `/32` metadata pointant sur cette dernière. ### 1. Plusieurs instances sur UDP/67 dans un même netns — validé C'était le risque principal, et le seul mode de panne qui aurait été silencieux. Deux serveurs dans le netns `vp-admin`, chacun ne connaissant que son subnet : ``` br-000001 → 10.0.6.2 (i-db) br-000000 → 10.0.5.10 (i-web) ``` `i-web` a bien reçu une adresse de **son** subnet : son DISCOVER en broadcast a donc atteint `br-000000` et non le serveur voisin. Le bind à l'interface posé par `server4.NewIPv4UDPConn` fait son travail, et le pire scénario — un guest recevant l'adressage du subnet d'à côté, sans qu'aucune erreur ne soit levée — est écarté. ### 2. Routes distribuées au guest — validé ``` default via 10.0.5.254 dev ens3 proto dhcp src 10.0.5.10 metric 100 169.254.169.254 via 10.0.5.1 dev ens3 proto dhcp src 10.0.5.10 metric 100 10.0.5.0/24 dev ens3 proto kernel scope link src 10.0.5.10 metric 100 ``` - route par défaut vers la gateway fournie ; - route `/32` vers le serveur de métadonnées avec l'**`interface_ip` du subnet** en next-hop — c'est l'invariant critique : avec un autre next-hop la trame est commutée en L2 et ne traverse jamais `PREROUTING`, donc la DNAT ne la voit pas et cloud-init échoue ; - la troisième route est posée par le noyau depuis l'adresse, pas par le DHCP. **Pas de route vers le CIDR du VPC, et c'est correct** : `internal/subnet/routing.go` ne l'émet pas en mode `bridge`. ### 3. Chemin vers le serveur de métadonnées — validé Depuis le guest, `curl http://169.254.169.254/meta-data` renvoie bien `instance-id` et `local-hostname`. La chaîne complète est donc prouvée : route annoncée par le DHCP → next-hop `interface_ip` → traversée de `PREROUTING` → DNAT → serveur de métadonnées. ### 4. Réservations poussées et retirées — validé Après suppression d'une VM d'un subnet puis recréation sur l'autre : ``` br-000000 → 0 hôte br-000001 → deux réservations, i-db et i-web ``` L'ordre `del-host` a bien été appliqué, `set-host` aussi sur l'autre subnet, et deux réservations coexistent sur un même subnet avec les MAC attendues, dérivées de l'index de l'IP. À noter au passage : les MAC ressortent en minuscules côté serveur alors que `dhcp.Entries` les écrit en majuscules en base. La normalisation appliquée par le watchdog dans son diff n'est donc pas théorique — sans elle chaque hôte serait signalé à la fois absent et obsolète. ### 5. Bail périmé au redémarrage — validé La VM a été recréée **sur son disque existant** après changement de subnet : son guest portait en cache un bail de l'autre réseau, avec un autre masque et une autre gateway. Il s'est configuré avec l'adresse annoncée et répond au ping. C'est le scénario qui conditionnait la décision **« pas de NAK »**, et elle tient sur un cas réel : un client à bail périmé converge sans blocage. Faute de capture on ne sait pas s'il est passé par INIT-REBOOT ou s'il a repris en DISCOVER — le résultat est ce qui comptait. ### 6. Bascule et retour arrière — validé Le passage `dnsmasq` → `two` et le retour fonctionnent tous deux, selon la procédure documentée dans `docs/exploitation/configuration.rst`. Le retour arrière n'avait jamais été exécuté : c'est la porte de sortie en cas de problème en production, elle est maintenant éprouvée. --- ## Ce qui n'est pas validé, et qui est assumé **Un second client DHCP.** Les tests ont porté sur un guest de la famille RHEL, dont le DHCP est le **client interne de NetworkManager** (`dhcp=internal`, défaut depuis RHEL 9) — et non systemd-networkd. Le client restant à éprouver est donc systemd-networkd, base de code distincte et défaut de la famille Debian. Reporté en **P999**, avec la procédure et le critère de réussite. ISC dhclient est écarté volontairement : en fin de vie depuis 2022 et en voie de disparition des distributions. S'il devait être testé, son mode de panne attendu est le hook `rfc3442-classless-static-routes` absent — dhclient reçoit l'option 121 mais n'installe aucune route sans lui, ce qui se diagnostique dans le guest et non côté serveur. ## Écart au ticket, assumé La réconciliation du watchdog porte sur les **réservations**, pas sur la configuration du subnet : celle-ci dépend de la route par défaut de l'host, lue à chaud, et la reproduire produirait de faux écarts au premier changement de route. Un serveur dépourvu de configuration est en revanche signalé. C'est dans la liste des réservations que les ordres se perdent, donc c'est elle qui est comparée. ## Suites ouvertes - **Retrait de dnsmasq** — à ouvrir quand tous les hyperviseurs seront passés en `backend: two`. Portera `run-dnsmasq-in-netns.sh`, `dnsmasq@.service`, `dhcp.DefaultConfDir`, les `hostsdir`/`optsdir`, la clé `dhcp.backend` elle-même et le paquet Debian. - **#47** — course dans le harnais de test de `internal/dispatcher/agent`. - **#48** — la CI n'exécute aucun test et ne se déclenche que sur un tag de release.
Author
Owner

Validation en lab #50 — 2026-10-04

La validation restante (« un second client DHCP : systemd-networkd », et plusieurs subnets d'un même
VPC servis chacun par leur propre serveur) est faite sur le lab multi-nœud de #50, scénario
s1-dhcp-two, release 0.2.0rc003, hyperviseur en dhcp.backend: two, guests Debian 12
(genericcloud, netplan → systemd-networkd).

Vérification Résultat
une VPC, deux subnets, une VM dans chacun démarrée seule, puis une dans chacun simultanément 4 VM running
chaque VM a l'adresse de son subnet 4/4
bail DHCP tenu par systemd-networkd (/run/systemd/netif/leases/) 4/4
route par défaut via interface_ip 4/4
route vers le CIDR du VPC via interface_ip 4/4
/32 vers 169.254.169.254 via interface_ip 4/4
get-state de chaque serveur : uniquement les couples MAC/IP de son subnet 2/2 (vérifié par les couples MAC/IP : two dérivant la MAC du rang de l'IP, les MAC seules sont identiques d'un subnet à l'autre)

Détail et outillage : bilan E5 de #50.

## Validation en lab #50 — 2026-10-04 La validation restante (« un second client DHCP : systemd-networkd », et plusieurs subnets d'un même VPC servis chacun par leur propre serveur) est faite sur le lab multi-nœud de #50, scénario `s1-dhcp-two`, release `0.2.0rc003`, hyperviseur en `dhcp.backend: two`, guests Debian 12 (`genericcloud`, netplan → systemd-networkd). | Vérification | Résultat | |---|---| | une VPC, deux subnets, une VM dans chacun démarrée seule, puis une dans chacun simultanément | 4 VM `running` | | chaque VM a l'adresse de **son** subnet | 4/4 | | bail DHCP tenu par systemd-networkd (`/run/systemd/netif/leases/`) | 4/4 | | route par défaut via `interface_ip` | 4/4 | | route vers le CIDR du VPC via `interface_ip` | 4/4 | | `/32` vers `169.254.169.254` via `interface_ip` | 4/4 | | `get-state` de chaque serveur : uniquement les couples MAC/IP de son subnet | 2/2 (vérifié par les couples MAC/IP : two dérivant la MAC du rang de l'IP, les MAC seules sont identiques d'un subnet à l'autre) | Détail et outillage : bilan E5 de #50.
Sign in to join this conversation.
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
syonad/two#46
No description provided.