diff --git a/README.md b/README.md index 9439539..20627f4 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Options utiles : | `-d` | dry-run : affiche les commandes sans les exécuter | | `-V` | désactiver la vérification des sommes de contrôle | -Un déploiement relève les instances `dnsmasq@` et `metadata@` actives **avant** l'arrêt des +Un déploiement relève les instances `dnsmasq@`, `dhcp@` et `metadata@` actives **avant** l'arrêt des services, et les redémarre ensuite — c'est la seule façon de savoir lesquelles relancer. ## Configuration diff --git a/docs/architecture/vue-densemble.rst b/docs/architecture/vue-densemble.rst index b8fb8e8..a0f5c3f 100644 --- a/docs/architecture/vue-densemble.rst +++ b/docs/architecture/vue-densemble.rst @@ -50,7 +50,7 @@ Paquets * - ``internal/vm`` - cycle de vie d'une VM : tap, iptables, metadata, qemu * - ``internal/dhcp`` - - génération des configurations dnsmasq et entrées ip → mac + - plan d'adressage ip → mac, et configurations dnsmasq du backend historique * - ``internal/metadata`` - serveur de metadata cloud-init et ses templates * - ``internal/watchdog`` diff --git a/docs/concepts/vpc-subnet-vm.rst b/docs/concepts/vpc-subnet-vm.rst index ac1e673..6337ea9 100644 --- a/docs/concepts/vpc-subnet-vm.rst +++ b/docs/concepts/vpc-subnet-vm.rst @@ -24,7 +24,8 @@ Subnet ------ Un subnet appartient à un VPC et pose, dans son netns, un bridge qui porte ``interface_ip`` — la -gateway vue par les VM. Il fournit aussi le DHCP (dnsmasq) et les routes annoncées aux guests. +gateway vue par les VM. Il fournit aussi le DHCP — dnsmasq ou le serveur intégré selon +``dhcp.backend`` — et les routes annoncées aux guests. ``iface_type`` est une clé **logique** (``vms``, ``internet``, ``admin``…), traduite en nom de bridge physique par la configuration de l'agent. Une clé absente ou inconnue retombe sur diff --git a/docs/demarrage/installation.rst b/docs/demarrage/installation.rst index 56c4cda..ed4217a 100644 --- a/docs/demarrage/installation.rst +++ b/docs/demarrage/installation.rst @@ -73,6 +73,10 @@ Ce que fait ``-i`` **masqué** : il prendrait le port 53 en concurrence des instances ``dnsmasq@`` que l'agent lance dans les netns. +``dnsmasq`` reste installé même avec ``dhcp.backend: two`` : le backend intégré ne le remplace que +pour les subnets créés après la bascule, et le paquet est nécessaire tant qu'un hyperviseur peut +revenir en arrière. Voir :doc:`/exploitation/configuration`. + **Noyau** — chargement de ``br_netfilter``, puis ``net.ipv4.ip_forward = 1`` et ``net.bridge.bridge-nf-call-iptables = 1``. Cette dernière clé est **requise** par la DNAT vers le serveur de metadata : sans elle, iptables ne voit pas le trafic bridgé des VM et cloud-init @@ -118,16 +122,21 @@ Binaires installés * - ``db`` - inspection de la base clé-valeur en ligne de commande - ``-conf`` + * - ``dhcp`` + - serveur DHCP intégré, une instance par subnet dans le netns du VPC ; démarré uniquement + avec ``dhcp.backend: two`` + - ``-conf`` -Les trois partagent le même fichier, ``/etc/two/agent.yml`` — voir -:doc:`/exploitation/configuration`. +Les quatre partagent le même fichier, ``/etc/two/agent.yml`` — voir +:doc:`/exploitation/configuration`. ``dhcp`` reçoit en plus son bridge et ses deux chemins de +fichiers en paramètres, posés par son script d'enrobage. Mise à jour ----------- -``deploy.sh`` relève les instances ``dnsmasq@`` et ``metadata@`` actives **avant** d'arrêter les -services, et les redémarre ensuite : c'est la seule façon de savoir lesquelles relancer. Arrêter -les services à la main avant de lancer le script fait perdre cette liste. +``deploy.sh`` relève les instances ``dnsmasq@``, ``dhcp@`` et ``metadata@`` actives **avant** +d'arrêter les services, et les redémarre ensuite : c'est la seule façon de savoir lesquelles +relancer. Arrêter les services à la main avant de lancer le script fait perdre cette liste. Vérifier l'installation ----------------------- diff --git a/docs/exploitation/api-agent/asynchronisme.rst b/docs/exploitation/api-agent/asynchronisme.rst index 96b5172..a506d55 100644 --- a/docs/exploitation/api-agent/asynchronisme.rst +++ b/docs/exploitation/api-agent/asynchronisme.rst @@ -18,7 +18,7 @@ Les deux temps d'une requête A->>A: Prepare — valide, écrit "creating" A-->>C: 202 + ressource en creating A->>W: Dispatch (file d'attente) - W->>W: Execute — netns, netif, dnsmasq + W->>W: Execute — netns, netif, dhcp W->>W: état → running (ou error) C->>A: GET /subnets/ A-->>C: 200 + state diff --git a/docs/exploitation/configuration.rst b/docs/exploitation/configuration.rst index 63b29a9..7024527 100644 --- a/docs/exploitation/configuration.rst +++ b/docs/exploitation/configuration.rst @@ -1,12 +1,19 @@ Configuration ============= -Un seul fichier, ``/etc/two/agent.yml``, partagé par les trois binaires : ``agent -config``, -``metadata -conf`` et ``db -conf``. Le fichier de référence commenté est +Un seul fichier, ``/etc/two/agent.yml``, partagé par les quatre binaires : ``agent -config``, +``metadata -conf``, ``db -conf`` et ``dhcp -conf``. Le fichier de référence commenté est ``conf/agent/config.exemple.yml`` dans le dépôt. Le chargement se fait par **viper** : les clés sont celles ci-dessous, en YAML. +.. warning:: + + Un fichier **absent** est toléré : toutes les valeurs par défaut s'appliquent. Un fichier + **présent mais invalide** fait en revanche échouer le démarrage, volontairement — jusqu'à + la version 0.1.0 il était ignoré en silence, et l'agent tournait alors entièrement sur les + défauts sans le dire. Une tabulation d'indentation ou un ``--`` égaré suffisent. + .. danger:: **L'API de l'agent n'a aucune authentification.** L'exemple livré écoute sur @@ -117,6 +124,71 @@ Les chemins OVMF sont nécessaires aux VM démarrées avec ``uefi: true`` (paque Debian et Ubuntu). ``uefi_vars_dir`` reçoit une copie inscriptible des variables UEFI par VM, créée au démarrage et supprimée à l'arrêt. +Backend DHCP +------------ + +.. code-block:: yaml + + dhcp: + backend: dnsmasq # ou two + +Choisit qui sert le DHCP des subnets **créés par cet agent** : + +.. list-table:: + :header-rows: 1 + :widths: 14 44 42 + + * - Valeur + - Serveur + - Unit + * - ``dnsmasq`` + - dnsmasq, configuré par fichiers dans ``/etc/dnsmasq.d`` + - ``dnsmasq@_`` + * - ``two`` + - le binaire ``dhcp``, piloté par socket Unix + - ``dhcp@_`` + +Le défaut est ``dnsmasq`` : un fichier de configuration de la 0.1.0, non modifié, se comporte +exactement comme avant. Toute autre valeur que ``dnsmasq`` ou ``two`` fait échouer le démarrage. + +Le répertoire d'exécution du backend ``two`` — ``/run/two/dhcp`` — **n'est pas configurable** : +le script d'enrobage le code en dur, une clé que lui ignorerait serait un mensonge. + +Ce que le backend ``two`` apporte : la configuration DHCP devient modifiable par VM et non plus +seulement par subnet, ce qui permet de n'annoncer la route par défaut que sur **une** interface +d'une VM multi-réseaux. Le watchdog peut en outre interroger le serveur et comparer ce qu'il sert +à ce que la base dit — voir :doc:`diagnostic`. + +Bascule d'un backend à l'autre +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + + L'option ne décide que du backend des **nouveaux** subnets. Elle ne migre rien : un subnet + déjà créé continue d'être servi par le serveur qui l'a été. Changer la valeur sans vider + l'hyperviseur laisse l'agent parler à un serveur qui ne tourne pas — les VM existantes + continuent, les nouvelles n'obtiennent pas d'adresse. + +La bascule est **manuelle** et suppose un hyperviseur vide : + +1. Supprimer toutes les VM, puis tous les subnets, puis les VPC. +2. Vérifier qu'il ne reste aucune unit DHCP active et aucun résidu : + + .. code-block:: bash + + systemctl list-units 'dnsmasq@*' 'dhcp@*' + ls /etc/dnsmasq.d/ /run/two/dhcp/ + +3. Modifier ``dhcp.backend`` dans ``/etc/two/agent.yml``. +4. ``systemctl restart agent`` — la valeur est lue au démarrage, pas à chaque commande. +5. Recréer VPC, subnets et VM. +6. Sur la première VM, vérifier l'adresse **et les trois routes** : la route par défaut, la + route vers le CIDR du VPC, et la route ``/32`` vers ``169.254.169.254``. C'est cette + dernière qui conditionne le provisionnement cloud-init. + +Le retour arrière suit la même procédure. Il n'y a pas de bascule à chaud, dans un sens ni dans +l'autre. + Watchdog -------- diff --git a/docs/exploitation/diagnostic.rst b/docs/exploitation/diagnostic.rst index 89f7b81..b60200c 100644 --- a/docs/exploitation/diagnostic.rst +++ b/docs/exploitation/diagnostic.rst @@ -25,7 +25,9 @@ partiellement créés subsistent. La VM démarre mais n'a pas d'adresse ------------------------------------ -Le DHCP est servi par l'instance ``dnsmasq@`` du subnet. +Le DHCP est servi par une instance dédiée au subnet. Quelle unit selon ``dhcp.backend`` : + +**Backend ``dnsmasq``** .. code-block:: bash @@ -34,7 +36,31 @@ Le DHCP est servi par l'instance ``dnsmasq@`` du subnet. cat /run/dnsmasq-_.leases cat /etc/dnsmasq.d/_.conf -Si dnsmasq ne voit passer aucune requête, le problème est en amont : tap absent, bridge non +**Backend ``two``** + +.. code-block:: bash + + systemctl status 'dhcp@_' + journalctl -u 'dhcp@_' -n 50 + + # Ce que le serveur a réellement en mémoire + echo '{"verb":"get-state"}' \ + | socat - UNIX-CONNECT:/run/two/dhcp/_.sock | jq . + + # Ce qu'il enverrait à une MAC donnée, sans effet de bord + echo '{"verb":"probe","mac":"00:22:33:00:00:0A"}' \ + | socat - UNIX-CONNECT:/run/two/dhcp/_.sock | jq .lease + +``probe`` est le point de départ le plus rapide : il montre l'adresse, le masque, le routeur, les +DNS et les routes classless tels qu'ils partiraient. Une réponse ``"served": false`` signifie que +la MAC n'est pas réservée — l'ordre ``set-host`` n'a jamais atteint le serveur, ou la VM n'a pas +été créée par cet agent. + +Le watchdog signale ces écarts de lui-même, à chaque tick, en comparant l'état servi à la base : +``dhcp reservation missing on the server``, ``stale dhcp reservation``, ``dhcp reservation +diverges``. Regarder ses notifications avant de sonder à la main. + +Si le serveur ne voit passer aucune requête, le problème est en amont : tap absent, bridge non raccordé, VM dans le mauvais netns. La VM a une adresse mais cloud-init n'applique rien diff --git a/docs/exploitation/observabilite.rst b/docs/exploitation/observabilite.rst index 05392d1..b8f7a4c 100644 --- a/docs/exploitation/observabilite.rst +++ b/docs/exploitation/observabilite.rst @@ -57,7 +57,8 @@ Journaux journalctl -u agent -f journalctl -u 'metadata@i-web' -n 50 - tail -f /var/log/dnsmasq-vp-admin_br-sn000001.log + tail -f /var/log/dnsmasq-vp-admin_br-sn000001.log # backend dnsmasq + journalctl -fu 'dhcp@vp-admin_br-sn000001' # backend two Inspection de la base --------------------- diff --git a/docs/exploitation/services.rst b/docs/exploitation/services.rst index 4e096cc..146f98c 100644 --- a/docs/exploitation/services.rst +++ b/docs/exploitation/services.rst @@ -1,7 +1,8 @@ Services systemd ================ -Trois units, installées sous ``/opt/two/bin`` par ``deploy.sh``. +Quatre units, installées sous ``/opt/two/bin`` par ``deploy.sh``. Les deux units DHCP +s'excluent : celle qui tourne dépend de ``dhcp.backend`` (voir :doc:`configuration`). .. list-table:: :header-rows: 1 @@ -15,7 +16,10 @@ Trois units, installées sous ``/opt/two/bin`` par ``deploy.sh``. - processus principal : API, dispatcher, exécution, watchdog * - ``dnsmasq@.service`` - ``_`` - - dnsmasq lancé dans le netns du VPC, un par subnet + - dnsmasq lancé dans le netns du VPC, un par subnet — backend ``dnsmasq`` + * - ``dhcp@.service`` + - ``_`` + - serveur DHCP intégré, un par subnet — backend ``two`` * - ``metadata@.service`` - ```` - serveur de metadata cloud-init, un par VM @@ -26,14 +30,15 @@ n'y a pas à les démarrer à la main en fonctionnement normal. .. code-block:: bash systemctl status agent - systemctl status 'dnsmasq@vp-admin_br-sn000001' + systemctl status 'dnsmasq@vp-admin_br-sn000001' # backend dnsmasq + systemctl status 'dhcp@vp-admin_br-sn000001' # backend two systemctl status 'metadata@i-web' dnsmasq ------- -Le script ``run-dnsmasq-in-netns.sh`` entre dans le netns puis exécute dnsmasq avec un fichier -de configuration par subnet, généré par l'agent : +Backend historique. Le script ``run-dnsmasq-in-netns.sh`` entre dans le netns puis exécute dnsmasq +avec un fichier de configuration par subnet, généré par l'agent : .. list-table:: :widths: 40 60 @@ -50,6 +55,42 @@ de configuration par subnet, généré par l'agent : Le fichier de baux et le journal sont les deux premiers endroits à regarder quand une VM n'obtient pas d'adresse. +Serveur DHCP intégré +-------------------- + +Backend ``two``. Le script ``run-dhcp-in-netns.sh`` entre dans le netns puis exécute le binaire +``dhcp``, à qui il passe le bridge à servir et ses deux chemins de fichiers — il ne déduit rien et +ignore le netns dans lequel il tourne : + +.. code-block:: bash + + /opt/two/bin/dhcp -conf /etc/two/agent.yml \ + -interface br-sn000001 \ + -state /run/two/dhcp/vp-admin_br-sn000001.state \ + -socket /run/two/dhcp/vp-admin_br-sn000001.sock + +.. list-table:: + :widths: 40 60 + + * - Socket de contrôle + - ``/run/two/dhcp/_.sock`` + * - État + - ``/run/two/dhcp/_.state`` + * - Journal + - ``journalctl -u 'dhcp@_'`` + +Il n'y a **ni fichier de configuration ni fichier de baux**. L'agent pousse l'état désiré sur la +socket de contrôle : la configuration du subnet à sa création, une réservation par interface à +chaque création ou suppression de VM. Les réservations sont statiques — une MAC inconnue n'obtient +rien, et le serveur reste silencieux plutôt que de répondre par un refus. + +Le fichier d'état **appartient au processus**, qui l'écrit et le relit à son démarrage. L'agent ne +l'écrit jamais ; il le supprime seulement, à la création du subnet pour écarter un résidu et à sa +suppression après avoir arrêté l'unit. Il vit dans ``/run`` parce qu'il n'a aucun sens sans le +netns, qui ne survit pas au redémarrage de l'host. + +Diagnostic : voir :doc:`diagnostic`, qui montre comment interroger la socket. + QEMU n'est pas une unit ----------------------- @@ -86,6 +127,6 @@ journal au moment d'un ``stop`` n'est donc pas une anomalie. Mise à jour ----------- -``deploy.sh`` relève les instances ``dnsmasq@`` et ``metadata@`` actives **avant** d'arrêter les +``deploy.sh`` relève les instances ``dnsmasq@``, ``dhcp@`` et ``metadata@`` actives **avant** d'arrêter les services, et les redémarre ensuite : c'est la seule façon de savoir lesquelles relancer. Arrêter les services à la main avant de lancer le script fait perdre cette liste. diff --git a/docs/versions/0.2.0.md b/docs/versions/0.2.0.md new file mode 100644 index 0000000..a8f52a5 --- /dev/null +++ b/docs/versions/0.2.0.md @@ -0,0 +1,2 @@ +```{include} ../../release_notes/0.2.0.md +``` diff --git a/docs/versions/index.rst b/docs/versions/index.rst index 4333ae4..09f40fa 100644 --- a/docs/versions/index.rst +++ b/docs/versions/index.rst @@ -6,6 +6,7 @@ Chaque version porte un nom de code dérivé du rang de sa publication : anges e .. toctree:: :maxdepth: 1 + 0.2.0 0.1.0 .. include:: ../../release_notes/codenames.md diff --git a/release_notes/0.2.0.md b/release_notes/0.2.0.md new file mode 100644 index 0000000..5f42489 --- /dev/null +++ b/release_notes/0.2.0.md @@ -0,0 +1,87 @@ +# Bael + +Serveur DHCP intégré, en coexistence avec dnsmasq, et documentation du projet. + +## Fonctionnalités + +**Serveur DHCP intégré** + +- Nouveau binaire `dhcp`, une instance par subnet lancée dans le netns du VPC par l'unit + `dhcp@_`. Il reçoit son bridge et ses deux chemins de fichiers en paramètres : + il ne compose aucun chemin et ignore le netns dans lequel il tourne +- Piloté par l'agent sur une **socket Unix** `/run/two/dhcp/_.sock`, en JSON par + ligne. Ordres idempotents en remplacement intégral : configuration du subnet à sa création, + une réservation par interface à chaque création ou suppression de VM +- **Réservations statiques uniquement**, pas de baux : une MAC inconnue n'obtient rien, et le + serveur reste silencieux plutôt que de répondre par un refus. Aucun `DHCPNAK` n'est émis +- État auto-persisté dans `/run/two/dhcp/_.state`, en écriture atomique et lisible + par le seul `root`. Le fichier appartient au processus, qui le relit à son démarrage ; l'agent + ne l'écrit jamais et se borne à le supprimer — à la création du subnet pour écarter un résidu, + à sa suppression après avoir arrêté l'unit +- La route par défaut est décidée **par interface** et non plus par subnet, ce qui permet de ne + l'annoncer que sur une interface d'une VM multi-réseaux +- L'encodage RFC 3442 de l'option 121 est délégué à `github.com/insomniacslk/dhcp` + +**Coexistence avec dnsmasq** + +- Nouvelle clé `dhcp.backend`, `dnsmasq` ou `two`, qui choisit le serveur des subnets **créés par + cet agent**. Le défaut est `dnsmasq` : un fichier de configuration de la 0.1.0, non modifié, se + comporte exactement comme avant +- La bascule est une **opération manuelle** sur un hyperviseur vide — l'option ne migre rien, un + subnet déjà créé reste servi par le serveur qui l'a été. La procédure est documentée +- Toute valeur autre que `dnsmasq` ou `two` fait échouer le démarrage de l'agent + +**Exploitation** + +- Le watchdog interroge le serveur DHCP intégré et compare les réservations servies à celles que + la base implique. Il nomme ce qui diverge — ordre perdu à la création, ordre perdu à la + suppression, adresse ou route par défaut divergente — et reste en lecture seule +- La socket de contrôle répond à `get-state` et à `probe`, ce dernier montrant sans effet de bord + ce qui serait envoyé à une MAC donnée : adresse, masque, routeur, DNS et routes +- Documentation Sphinx du projet : concepts, architecture, déploiement et exploitation + +## Correctifs + +- **Un fichier de configuration présent mais invalide fait désormais échouer le démarrage.** + Jusqu'en 0.1.0 l'erreur de lecture était ignorée : l'agent tournait alors entièrement sur ses + valeurs par défaut sans le dire, ce qui rendait indétectable une simple tabulation d'indentation +- `probe` annonce explicitement `"served": false` pour une MAC non réservée, au lieu d'omettre le + champ et de le rendre indistinguable d'une réponse tronquée + +## Changements internes + +- Le module passe à **Go 1.25**, exigé par la bibliothèque DHCP retenue. La version de Go du + workflow de build, restée à 1.21 alors que le module en demandait davantage, est alignée +- Nouveau paquet `pkg/db/statefile` : persistance générique d'un état de composant dans un + fichier, en écriture atomique. Badger a été écarté pour cet usage — une instance par subnet + coûterait une memtable de 64 Mio et quatre goroutines de compaction pour environ un kilo-octet + d'état, dans un `tmpfs`, et laisserait un verrou résiduel après un arrêt brutal +- `internal/subnet` et `internal/vm` ne parlent plus à dnsmasq en direct mais à une interface + `Backend` à deux implémentations + +## Périmètre et limites connues + +Celles de la 0.1.0 restent valables, sauf mention contraire ci-dessus. S'y ajoutent : + +- **dnsmasq n'est pas retiré** et reste un paquet requis : le backend intégré ne sert que les + subnets créés après la bascule, et le retour arrière suppose dnsmasq installé +- Pas de DNS, pas de pool dynamique, pas de PXE, pas de DHCPv6 dans le serveur intégré +- La comparaison faite par le watchdog porte sur les **réservations** et non sur la configuration + du subnet : celle-ci dépend de la route par défaut de l'host, dont la lecture au moment du + contrôle produirait de faux écarts. Un serveur dépourvu de configuration est en revanche signalé +- L'option 249 (routes classless de Microsoft) n'est pas émise, comme dnsmasq ne l'émet pas +- Le serveur intégré écoute UDP/67 sans authentification, comme tout serveur DHCP : l'isolation + entre locataires d'un même subnet repose sur les règles ebtables anti-usurpation, inchangées +- `internal/dhcpbackend` n'est testé sous Linux que pour ses appels systemd ; le reste, y compris + le dialogue avec le serveur intégré, est couvert sur toute plateforme + +## Mise à jour depuis la 0.1.0 + +```bash +curl -O https://git.g3e.fr/syonad/two/raw/branch/main/scripts/deploy.sh +bash ./deploy.sh -t 0.2.0 +``` + +Aucune action n'est requise : sans `dhcp.backend` dans `/etc/two/agent.yml`, le comportement est +celui de la 0.1.0. Pour passer au serveur intégré, suivre la procédure de bascule dans la +documentation d'exploitation — elle suppose un hyperviseur vidé. diff --git a/release_notes/codenames.md b/release_notes/codenames.md index 17161c2..fe77557 100644 --- a/release_notes/codenames.md +++ b/release_notes/codenames.md @@ -21,7 +21,7 @@ prochain nom disponible sans tenir de compteur ailleurs : c'est la première lig | # | Nom | Nature | Version | Date | |---|---|---|---|---| | 1 | Michael | ange | [0.1.0](0.1.0.md) | 2026-08-26 | -| 2 | Bael | démon | 0.2.0 | | +| 2 | Bael | démon | [0.2.0](0.2.0.md) | | | 3 | Gabriel | ange | | | | 4 | Agares | démon | | | | 5 | Raphael | ange | | |