Serveur DHCP intégré — remplacer dnsmasq par un binaire contrôlable #46
Labels
No labels
bug fix
feature implementation
new feature
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
syonad/two#46
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.shvia l'unitdnsmasq@<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:dansdhcp-host,tag:dansdhcp-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 templatedhcp@<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 execn'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./runplutô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.confdnsmasq dans/etca 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 :
.staterésiduel, démarrer l'unit, pousser l'état.stateLa 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
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-hostsdirsur dnsmasq ; ce ticket est la cible, pas le chemin.Prochaine étape
on commence
c'est en cour
ca avance
toujours pas fini
toujours pas
tag mis
soft deployer
Éléments à vérifier avant de considérer le backend
twoéprouvéLe code est livré sur
feature-46(12 commits, préversion0.2.0rc002et suivantes). Tout estcouvert 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.NewIPv4UDPConnposeSO_REUSEADDR,SO_REUSEPORTet le bind àl'interface (
SO_BINDTODEVICE) — le pendant du--interface=<bridge> --bind-interfacesdednsmasq — 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 debind 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 :
probesur chaque socket doit ne connaître que les MAC de son propre subnet.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 :
interface_ipcomme next-hop ;/32vers169.254.169.254, également viainterface_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 leyiaddrde 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 unhyperviseur 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. Iln'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 :
LoadConfigignorait l'erreur deReadInConfig. Un YAMLinvalide faisait ignorer tout le fichier en silence, chaque valeur retombant sur son défaut — et
le défaut de
dhcp.backendestdnsmasq.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 :
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>.serviceapparaît.Hors périmètre de ce ticket, mais dans son sillage
backend: two.Portera
run-dnsmasq-in-netns.sh,dnsmasq@.service,dhcp.DefaultConfDir, leshostsdir/optsdir, la clédhcp.backendelle-même et le paquet Debian.internal/dispatcher/agent, qui rend unepasse complète de
go test ./...sur deux instable. S'est manifestée pendant le développement dece ticket.
explique qu'aucun de ces deux défauts n'ait été signalé automatiquement.
ConfigureSubnetetTeardownSubnetdes deux backends,Dnsmasq.DelVMet le
maindecmd/dhcpappellent systemd ou exigent un bridge réel. Le reste, y compris toutle dialogue par socket, est couvert sur toute plateforme.
Validation en lab — 2026-09-10
Tests menés sur un hyperviseur de lab avec
dhcp.backend: two.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 :i-weba bien reçu une adresse de son subnet : son DISCOVER en broadcast a donc atteintbr-000000et non le serveur voisin. Le bind à l'interface posé parserver4.NewIPv4UDPConnfaitson 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é
/32vers le serveur de métadonnées avec l'interface_ipdu 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 ;Pas de route vers le CIDR du VPC, et c'est correct :
internal/subnet/routing.gone l'émet pasen mode
bridge.3. Chemin vers le serveur de métadonnées — validé
Depuis le guest,
curl http://169.254.169.254/meta-datarenvoie bieninstance-idetlocal-hostname. La chaîne complète est donc prouvée : route annoncée par le DHCP → next-hopinterface_ip→ traversée dePREROUTING→ 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 :
L'ordre
del-hosta bien été appliqué,set-hostaussi sur l'autre subnet, et deux réservationscoexistent 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.Entrieslesé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→twoet le retour fonctionnent tous deux, selon la procédure documentéedans
docs/exploitation/configuration.rst. Le retour arrière n'avait jamais été exécuté : c'est laporte 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 nonsystemd-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-routesabsent —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
backend: two.Portera
run-dnsmasq-in-netns.sh,dnsmasq@.service,dhcp.DefaultConfDir, leshostsdir/optsdir, la clédhcp.backendelle-même et le paquet Debian.internal/dispatcher/agent.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, release0.2.0rc003, hyperviseur endhcp.backend: two, guests Debian 12(
genericcloud, netplan → systemd-networkd).running/run/systemd/netif/leases/)interface_ipinterface_ip/32vers169.254.169.254viainterface_ipget-statede chaque serveur : uniquement les couples MAC/IP de son subnetDétail et outillage : bilan E5 de #50.