- PHP 87.9%
- Shell 12.1%
| client | ||
| debian | ||
| server | ||
| .gitignore | ||
| build.sh | ||
| MIGRATION.md | ||
| README.md | ||
infratom v2
Gestion centralisée d'un mesh WireGuard full IPv6, multi-environnements (intégration / prod), piloté par un serveur de configuration.
Principes
- Plus de tunnel d'amorçage. Le client parle au serveur de config en HTTPS public (IPv6), avant tout tunnel. Fini le problème d'œuf et de poule de la v1 (où il fallait le tunnel pour être identifié par son IP interne).
- Identité = token secret stable. L'hôte s'authentifie avec un token (Bearer). Le token est l'identifiant ; il ne change pas quand on déplace une machine d'un environnement à l'autre.
- Clé
(token, réseau). La configuration servie dépend du couple (token, réseau). Cloner une machine de prod vers l'intégration = changer uniquement le paramètreNETWORK, pas le token. - Découverte de l'IP publique. Le serveur lit l'adresse source de la requête
HTTPS (
REMOTE_ADDR, IPv6 publique) et la mémorise comme endpoint du peer. En full IPv6 sans NAT, l'endpoint est directement exploitable et se rafraîchit à chaque appel. - Application du diff. Le client récupère l'état désiré et l'applique avec
wg syncconf(ajout/retrait/modif des peers sans couper l'interface) + une réconciliation des routes. Il ne fait que « appliquer la différence ». - Mise à jour par polling. Un agent (timer/daemon systemd) interroge
/v2/syncpériodiquement : il récupère les changements de topologie et rafraîchit son IP publique côté serveur. Un numéro de version évite d'agir quand rien n'a changé. - Clés WireGuard régénérables par environnement. Pour isoler le matériel
cryptographique entre prod et intégration, l'agent peut (re)générer sa paire de
clés ; sa pubkey est (ré)enregistrée à chaque
/v2/sync.
Contrat d'API
POST /v2/sync
Requête :
POST /v2/sync HTTP/1.1
Authorization: Bearer <HOST_TOKEN>
Content-Type: application/json
{
"network": "integration",
"pubkey": "<clé publique WireGuard base64>"
}
Le serveur :
- résout l'hôte par
(token, network); 404 si inconnu, 401 si token absent ; - met à jour
pubkey,public_endpoint = [REMOTE_ADDR]:port,last_seen, oùportest le port d'écoute défini côté serveur (réseau, ou override d'hôte) — l'agent ne l'envoie pas ; - calcule l'état désiré et renvoie :
{
"version": "sha256:...",
"interface": {
"address": "fd42:1::10/64",
"fqdn": "mon-hote.integration.exemple.org",
"listen_port": 51820,
"dns": ["2a01:db8::53"],
"dns_domains": ["integration.exemple.org", "...ip6.arpa"]
},
"peers": [
{
"name": "node-1",
"fqdn": "node-1.integration.exemple.org",
"public_key": "...",
"endpoint": "[2001:db8::5]:51820",
"allowed_ips": ["fd42:1::1/128", "fd42:1:beef::/64"],
"persistent_keepalive": 25
}
]
}
fqdn (= nom.domaine) est informatif : il n'est pas utilisé fonctionnellement
pour le moment (juste reporté en commentaire dans la config WireGuard générée).
version est un hash de la liste des peers servie : si elle est identique à la
dernière appliquée, l'agent ne touche à rien.
GET /v2/healthz
Sonde de vie, sans authentification. Retourne {"status":"ok"}.
Frontend d'administration (/admin)
UI web server-rendered (PHP) pour gérer réseaux, hôtes (création + token),
peerings et routes, et visualiser l'état (endpoint, last_seen). Équivalent
web de infractl.
- Authentification OIDC (Authorization Code) via
jumbojett/openid-connect-php— flux et validation JWT/JWKS délégués à la librairie. Routes :/admin/login(=redirect_uri),/admin/logout. - Autorisation : appartenance au groupe
oidc_admin_grouprenvoyé par l'IdP (claimoidc_groups_claim). Sinon → 403. - CSRF : jeton de session vérifié sur chaque POST.
- L'API agents
/v2/*reste en auth par token, indépendante de l'OIDC.
Configuration : section OIDC de server/etc/server.ini.sample. Dépendances PHP
installées par composer install (fait par build.sh deb).
Serveur DNS du mesh (Unbound + génération)
Un Unbound co-localisé sur le serveur web résout les noms du mesh et relaie le DNS public, le tout réservé aux membres :
- ACL par IP source :
access-controln'autorise que lespublic_endpointconnus ; le reste est refusé. - Split-horizon : une vue par réseau (
view+access-control-view) ; chaque IP membre est rattachée à la vue de son réseau, qui contient seslocal-data. Indispensable car deux environnements (clone prod ↔ intégration) peuvent partager domaine et adressage interne. - Forward
nom.domaine→AAAA(IP interne) ; reverseip6.arpa→PTR(FQDN) — vialocal-data/local-data-ptr. - Relais public : pour tout nom hors mesh, Unbound recurse/relaie nativement (avec cache) pour les clients autorisés.
Génération & rechargement (découplés)
Unbound n'interroge pas la base en direct : sa config est générée depuis SQLite puis rechargée. Le déclenchement est découplé de PHP :
- PHP (
/v2/syncsi lepublic_endpointchange, ou l'admin lors d'un changement réseau/hôte) pose un drapeau :touch /run/infratom2/dns.dirty(infratom_dns_mark_dirty(), non privilégié). - Un
systemd.path(infratom2-dns.path) surveille ce drapeau et déclenche le service oneshotinfratom2-dns-reload→server/bin/dns-reload. dns-reloadexécuteinfractl dns-generate(→ fragment de config Unbound), valide (unbound-checkconf) et recharge (unbound-control reload).
Le path unit coalesce naturellement les rafales (un seul reload). Le
drapeau n'est levé que sur changement pertinent pour le DNS (jamais sur le
simple last_seen d'un sync).
DNS côté client (auto-configuration)
Le serveur pousse, dans la réponse /v2/sync, le résolveur du mesh
(interface.dns = networks.dns) et les domaines à router
(interface.dns_domains = domaine du réseau + zone reverse). L'agent applique
automatiquement à chaque sync (apply_dns) :
- split-DNS (
INFRATOM_DNS_DEFAULT=no, défaut) : per-link via systemd-resolved — seuls le domaine du réseau et la zone reverse partent au résolveur du mesh, le reste garde le résolveur local. Prérequis hôte :systemd-resolvedactif +/etc/resolv.conf→ stub. - tout via le relais (
INFRATOM_DNS_DEFAULT=yes) : toutes les requêtes vont au résolveur du mesh (repli en réécrivant/etc/resolv.confsi pas de systemd-resolved).
Le résolveur doit être l'IPv6 publique du serveur Unbound (pour que la
source des requêtes = public_endpoint → ACL satisfaite ; on n'interroge pas
le DNS via le tunnel). Le régler sur un réseau :
infractl net-set prod --dns=<IPv6_publique_du_serveur_unbound>
Modèle de données (SQLite)
networks— un réseau = un mesh = un environnement (prod,integration), avec son préfixe ULA, sondomain(non unique, informatif), ses DNS et sonlisten_port(port d'écoute du réseau).hosts— appartenance d'un hôte à un réseau :(token, network_id)unique,nameobligatoire et unique par réseau,internal_ipgénérée depuis le préfixe du réseau (override possible),listen_portoptionnel (override du port du réseau), et les champs dynamiques (pubkey,public_endpoint,last_seen).peerings— liens explicites entre hôtes d'un même réseau. Un hôte ne voit que les hôtes avec lesquels un lien est déclaré (topologie sélective ; peering WireGuard symétrique).routes— sous-réseaux IPv6 routés exposés par un hôte (ajoutés à sesallowed_ips).
Schéma complet : server/schema.sql.
Arborescence
server/
schema.sql # schéma SQLite
composer.json # dépendances PHP (OIDC) -> vendor/
public/index.php # front controller (routeur : /v2/* agents, /admin* UI)
src/bootstrap.php # config, PDO, helpers HTTP
src/sync.php # logique agents + allocation IP + état désiré
src/auth.php # OIDC (login/logout/groupe) + CSRF
src/admin.php # frontend d'administration (CRUD)
src/dns.php # génération config Unbound (vues split-horizon + ACL)
bin/infractl # CLI d'admin (réseaux, hôtes, liens, routes, dns-generate)
bin/dns-reload # régénère la config Unbound + reload (service oneshot)
systemd/ # infratom2-dns.path + infratom2-dns-reload.service
etc/ # vhost Apache, base Unbound, config exemple (DB + OIDC)
client/
bin/agent # agent Bash (sync + wg syncconf + routes)
etc/agent.conf # configuration exemple de l'hôte
systemd/ # units systemd
debian/ # packaging .deb (infratom2-server, infratom2-client)
Statut
Squelette initial de la v2. Voir les TODO en fin de fichiers et l'historique git.