No description
  • PHP 87.9%
  • Shell 12.1%
Find a file
2026-06-25 03:54:01 +02:00
client first commit 2026-06-25 03:54:01 +02:00
debian first commit 2026-06-25 03:54:01 +02:00
server first commit 2026-06-25 03:54:01 +02:00
.gitignore Initial commit : infratom v2 (mesh WireGuard full IPv6 piloté par serveur) 2026-06-24 20:59:30 +02:00
build.sh Initial commit : infratom v2 (mesh WireGuard full IPv6 piloté par serveur) 2026-06-24 20:59:30 +02:00
MIGRATION.md Initial commit : infratom v2 (mesh WireGuard full IPv6 piloté par serveur) 2026-06-24 20:59:30 +02:00
README.md Initial commit : infratom v2 (mesh WireGuard full IPv6 piloté par serveur) 2026-06-24 20:59:30 +02:00

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ètre NETWORK, 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/sync pé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 :

  1. résout l'hôte par (token, network) ; 404 si inconnu, 401 si token absent ;
  2. met à jour pubkey, public_endpoint = [REMOTE_ADDR]:port, last_seen, où port est le port d'écoute défini côté serveur (réseau, ou override d'hôte) — l'agent ne l'envoie pas ;
  3. 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_group renvoyé par l'IdP (claim oidc_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-control n'autorise que les public_endpoint connus ; 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 ses local-data. Indispensable car deux environnements (clone prod ↔ intégration) peuvent partager domaine et adressage interne.
  • Forward nom.domaineAAAA (IP interne) ; reverse ip6.arpaPTR (FQDN) — via local-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 :

  1. PHP (/v2/sync si le public_endpoint change, 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é).
  2. Un systemd.path (infratom2-dns.path) surveille ce drapeau et déclenche le service oneshot infratom2-dns-reloadserver/bin/dns-reload.
  3. dns-reload exécute infractl 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-resolved actif + /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.conf si 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, son domain (non unique, informatif), ses DNS et son listen_port (port d'écoute du réseau).
  • hosts — appartenance d'un hôte à un réseau : (token, network_id) unique, name obligatoire et unique par réseau, internal_ip générée depuis le préfixe du réseau (override possible), listen_port optionnel (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 à ses allowed_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.