rust-app pour tester l'interco avec client / agents à travers un vpn pour accéder à des données serveurs cibles.
  • Rust 51%
  • Svelte 37.4%
  • Shell 4%
  • SCSS 2.8%
  • JavaScript 2.5%
  • Other 2.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
dwi e686138185
All checks were successful
CI / Rust (format, lint, tests) (push) Successful in 1m19s
CI / Frontend (tests, build, cohérence de dist) (push) Successful in 16s
CI / Agent Linux (push) Successful in 1m3s
CI / Console Windows (cross-compilation) (push) Successful in 3m42s
CI / Console Linux (push) Successful in 7m12s
CI / Publication sur la forge (push) Successful in 5s
forge : lire la release de l'agent, pas la plus récente du dépôt
Le bandeau rouge dans le panneau ne venait pas des droits d'accès : les
releases sont bien publiques et lisibles en anonyme, je l'ai vérifié. Il venait
de moi. `latest()` appelait `/releases/latest`, qui rend la release la plus
récente du dépôt toutes familles confondues. Depuis que la CI publie aussi
`console-latest`, c'est elle qui sortait une fois sur deux, et la console y
cherchait en vain un artefact nommé « agent ».

Ce défaut est né avec la seconde release : je l'ai ajoutée sans revenir sur
l'hypothèse « une seule release » que le code portait.

La lecture se fait donc par tag, `agent-latest` par défaut, réglable dans le
panneau pour viser une autre release sans reconstruire. Le repli ne prend plus
« la première publiée » mais « la première publiée qui contienne vraiment
l'artefact cherché », ce qui rattrape en prime un tag renommé.

Vérifié de bout en bout contre la vraie forge, en anonyme : release lue par tag,
empreinte publiée récupérée, 4 117 176 octets téléchargés, empreinte et en-tête
ELF contrôlés. Ce test reste dans le dépôt, ignoré par défaut pour que la CI ne
dépende pas d'un serveur tiers, et se lance par variables d'environnement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzJJYZKrk81Dmcnee8hv2T
2026-09-08 23:03:59 +02:00
.forgejo/workflows livrer la console en un zip par plateforme 2026-09-08 22:39:32 +02:00
agent tests : couverture des fonctions sensibles restées à découvert 2026-09-08 18:28:30 +02:00
console forge : lire la release de l'agent, pas la plus récente du dépôt 2026-09-08 23:03:59 +02:00
protocol tests : couverture des fonctions sensibles restées à découvert 2026-09-08 18:28:30 +02:00
screens docs: README remanié + captures de la console 2026-09-08 04:36:39 +02:00
scripts package : contrôler l'archive par noms, pas par nombre d'entrées 2026-09-08 22:50:06 +02:00
.gitignore client : build Linux, et publication des deux plateformes sur la forge 2026-09-08 22:18:40 +02:00
Cargo.lock onglet pare-feu : règles nftables / iptables en tableaux 2026-09-08 04:54:52 +02:00
Cargo.toml Initial: agent Rust + console Tauri pilotant un serveur via NetBird 2026-09-08 02:07:39 +02:00
Makefile livrer la console en un zip par plateforme 2026-09-08 22:39:32 +02:00
README.md forge : lire la release de l'agent, pas la plus récente du dépôt 2026-09-08 23:03:59 +02:00

🛰️ NetBird Ops Console

Piloter un serveur Linux à distance avec un agent Rust et une console GUI (Tauri + Svelte), le tout uniquement à travers le mesh NetBird. Pensé pour de l'opérationnel sysadmin : métriques, services, logs, terminal, fichiers, runbooks, comptes.

⚠️ Version de test, pas d'authentification applicative. La confidentialité repose entièrement sur le tunnel WireGuard de NetBird, et l'agent n'accepte que les connexions venant de 100.64.0.0/10. N'expose jamais le port de l'agent hors du mesh. L'agent exécute des commandes shell arbitraires : ne le lance que sur des hôtes de test.

📸 Aperçu

Tableau de bord

Tableau de bord : métriques poussées en continu, système, disques. Les panneaux Processus et Interfaces sont vides ici parce que l'agent de cet hôte date d'avant ces métriques ; la console reste compatible et affiche simplement le reste.

🧩 Services et Runbooks

Services

Les unités systemd, avec filtre et actions groupées (démarrer, arrêter, redémarrer, statut).

Runbooks

L'onglet Runbooks quand aucun catalogue n'est encore rempli : les quatre emplacements consultés sont affichés avec leur état.

Ce que ça fait

Onglet Ce qu'on y fait
📊 Tableau de bord Métriques poussées (~1 s) avec courbes, top processus, débit par interface, disques, anomalies
⚙️ Services Unités systemd : lister, filtrer, démarrer, arrêter, redémarrer, statut détaillé
📜 Logs Suivi temps réel du journal complet, d'une unité, ou d'un fichier
💻 Shell Terminaux interactifs en onglets, chacun sa session PTY indépendante
📁 Fichiers Gestionnaire façon FileZilla : navigation, envoi, téléchargement, permissions, renommage, suppression
📓 Runbooks Actions scriptées pré-déclarées, lancées en un clic, avec paramètres
🧱 Pare-feu Règles nftables ou iptables en tableaux, par chaîne et par interface
👥 Comptes Utilisateurs, groupes, et gestion des clés SSH autorisées

La barre latérale mémorise les hôtes en local et sait importer ceux de ~/.ssh/config pour éviter de les ressaisir.

🏗️ Architecture

flowchart LR
    subgraph WIN["Poste Windows"]
        C["console<br/>(Tauri + Svelte)"]
    end
    subgraph LNX["Serveur Linux"]
        A["agent<br/>(tokio)"]
        OS["systemd · journalctl · nftables<br/>PTY · fichiers · comptes"]
        A --> OS
    end
    C == "TCP · trames JSON préfixées longueur<br/>tunnel WireGuard NetBird (100.64.0.0/10)" ==> A
    A -. "réponses · streams" .-> C
Crate Rôle
protocol/ Contrat agent ↔ console : types serde, plus la logique pure partagée (catalogue de runbooks, analyse des règles de pare-feu et des clés SSH)
agent/ Binaire Linux. Écoute sur l'IP NetBird, exécute les requêtes. Mode probe inclus
console/ App Tauri. Backend Rust + frontend Svelte 5 / Vite / SCSS dans console/ui/

Le workspace racine ne contient que protocol + agent, pour un build Linux rapide. console/ est un crate autonome (son propre [workspace]) afin que cargo build à la racine ne tente pas de compiler la GUI.

🔌 Le protocole en détail

Chaque trame vaut [u32 big-endian longueur][JSON]. Le client envoie des Request avec un id croissant ; l'agent répond par une ou plusieurs Response du même id. Les opérations streamées se terminent par une trame terminale (Exit, End, FileEnd). Voir protocol/src/lib.rs.

sequenceDiagram
    participant C as console
    participant A as agent
    C->>A: Request { id, SysInfo }
    A-->>C: Response { id, SysInfo(...) }
    C->>A: Request { id, Exec("journalctl -f") }
    A-->>C: Response { id, Stdout(ligne) }
    A-->>C: Response { id, Stdout(ligne) }
    C->>A: Request { Cancel(target=id) }
    A-->>C: Response { id, End }

Les métriques sont poussées (MetricsSubscribe) plutôt que sondées. L'agent garde un échantillonneur d'un relevé à l'autre : l'usage CPU par processus et les débits réseau sont des écarts entre deux mesures, ils vaudraient zéro avec un System recréé à chaque appel.

Les champs ajoutés à SysInfo après coup sont en serde(default), donc une console récente reste capable de lire un agent plus ancien. C'est exactement ce qu'on voit sur la capture du tableau de bord.

🔄 Compatibilité entre versions

Compatibilité agent / console

L'agent et la console évoluent séparément : le binaire déployé sur un hôte est souvent plus ancien que la console qu'on vient de rebuilder. Le protocole doit donc tolérer qu'un pair reçoive quelque chose qu'il ne connaît pas.

Versions agent / console

L'agent et la console évoluent séparément, et un agent déployé il y a une semaine ne sait pas répondre à une console d'aujourd'hui. La console lui demande donc sa carte d'identité au moment de la connexion :

✔ Agent v0.1.0 build c48bdd5 (2026-09-08 17:57) · protocole v3
  capacités : metrics, shell, files, runbooks, accounts, firewall

Elle affiche la version dans la barre du haut, en gris quand tout va bien, en rouge quand l'agent est en retard. Et surtout, elle n'envoie pas à un agent une opération qu'il n'annonce pas : c'est ce qui évite qu'un onglet récent coupe la session.

L'agent annonce une liste de capacités plutôt qu'un simple numéro : une table version → fonctionnalités côté console dupliquerait la connaissance et finirait par diverger.

🔌 La question part sur une connexion jetable. Un agent antérieur à AgentInfo ne sait pas la désérialiser et y répond en fermant la session : sur une connexion dédiée, cette fermeture ne coûte rien. La session de travail n'est jamais exposée.

PROTOCOL_VERSION (dans protocol/src/lib.rs) est à incrémenter à chaque ajout d'opération, et la capacité correspondante à déclarer dans agent_info(), juste à côté du dispatch qui l'implémente.

  • Une opération inconnue n'est pas une trame corrompue. L'agent la décode via protocol::decode_request, qui récupère l'id et le nom de l'opération, et répond une erreur ciblée. Sans cette tolérance, une seule requête récente suffisait à faire raccrocher l'agent, et la console perdait la session entière au moment précis où on ouvrait un onglet récent.
  • Une réponse inconnue est terminale. Une console plus ancienne que son agent la range dans RespBody::Unknown au lieu de faire tomber son thread lecteur, et rend la main tout de suite plutôt qu'au bout du délai.
  • Les champs ajoutés à une structure sont en serde(default), donc un agent plus ancien reste lisible : c'est ce qui fait que le tableau de bord affiche les métriques de base même quand l'hôte ne renvoie ni processus ni interfaces.

En revanche une trame réellement illisible reste une erreur fatale pour la session : à ce moment-là le flux est désynchronisé.

🔄 Après avoir ajouté une opération au protocole, redéploie l'agent sur les hôtes concernés. La console affiche sinon « l'agent a fermé la connexion sans répondre » ou, une fois l'agent à jour, un message nommant l'opération manquante.

🚀 Démarrage

Agent (Linux)

cargo build --release -p agent      # -> target/release/agent

./agent serve                       # bind auto sur l'IP NetBird détectée, port 7878
./agent serve --bind 100.120.45.50 --port 7878

Options : --bind <ip>, --port <n>, --allow-any (accepte les connexions hors NetBird, tests locaux uniquement).

🔑 En lecture (métriques, liste des services, logs) un utilisateur normal suffit souvent. Pour start/stop/restart, les clés SSH d'un autre compte ou l'écriture de fichiers système, l'agent doit tourner en root ou avec une policy polkit adaptée.

Mettre à jour un agent depuis la console

La barre du haut propose Mettre à jour dès qu'un hôte est connecté, et pas seulement quand un retard est détecté : la console ne compare que les versions de contrat, un agent « à jour » de ce point de vue peut très bien tourner sur un vieux binaire. Le panneau offre trois sources.

Source Ce que c'est Quand
🌐 Dernier paquet de la forge La release roulante agent-latest, publiée par la CI à chaque poussée sur main Le cas courant : remettre un serveur à niveau sans rien reconstruire
📦 Agent embarqué L'agent du même commit que la console Juste après un make win
📁 Binaire du poste Un fichier choisi à la main Test d'une version locale

Dans les trois cas, la console envoie le binaire, l'agent se remplace et se relance ; la session tombe quelques secondes puis est reprise automatiquement.

🌐 Ce que la console va chercher sur la forge

Elle interroge l'API Forgejo/Gitea du dépôt, par tag : /api/v1/repos/…/releases/tags/agent-latest. Demander « la release la plus récente » serait plus court et faux, puisque ce dépôt publie aussi une console-latest, plus récente une fois sur deux. Si le tag est introuvable, elle parcourt la liste et retient la première release publiée qui contienne vraiment l'artefact cherché, ce qui rattrape un tag renommé.

Elle y choisit ensuite l'artefact nommé agent et le télécharge.

La CI dépose à côté du binaire un fichier agent.sha256 : l'empreinte vient donc de la publication, pas du téléchargement. Un artefact tronqué côté forge est détecté ici, avant d'atteindre le serveur, ce que TLS seul ne garantit pas. Le panneau affiche ✓ empreinte publiée vérifiée quand ce fichier existe.

Le tag et le nom de l'artefact sont réglables dans le panneau (Release et Artefact), pour viser une autre release sans reconstruire la console.

Le dépôt visé est déduit du remote git au moment du build : rien à saisir quand la forge se sert sous le même nom d'hôte que le remote. Ce n'est pas toujours le cas : un remote en git.example.org peut très bien correspondre à une interface web en forge.example.org, et le certificat TLS ne couvrir que le second. C'est alors le panneau qui tranche, en une fois.

Pour un dépôt privé, il faut de plus un jeton : la lecture de la release comme le téléchargement de l'artefact passent par lui. Configurer la forge dans le panneau écrit %APPDATA%\netbird-ops\forge.json (~/.config/netbird-ops/forge.json sous Linux). Les variables NBOPS_FORGE_URL, _OWNER, _REPO, _ASSET, _TAG et _TOKEN priment sur le fichier, le temps d'un lancement. Elles servent aussi à lancer le test de bout en bout, ignoré par défaut pour que la CI ne dépende pas d'un serveur tiers :

cd console && NBOPS_FORGE_URL=https://forge.example.org NBOPS_FORGE_OWNER=dwi \
  NBOPS_FORGE_REPO=app cargo test -- --ignored --nocapture
🕰️ Un agent trop ancien pour être mis à jour à distance

AgentInfo et AgentUpgrade sont arrivés ensemble. Un agent antérieur ne sait donc ni se présenter, ni recevoir un binaire : il ferme la connexion au lieu de répondre. Lui envoyer une mise à jour donne une déconnexion, une reconnexion, et rien de changé sur l'hôte. La console refuse maintenant l'envoi plutôt que de laisser recommencer.

Elle sait quand même dire qui il est. Un tel agent gère Exec, alors on lui demande de décrire son propre binaire :

e=$(readlink -f /proc/$PPID/exe); echo "path=$e"; stat -c "mtime=%Y" "$e";

Exec lance sh -c directement depuis le processus de l'agent, donc le $PPID de ce shell est l'agent : pas de pgrep qui se tromperait de processus, pas de chemin codé en dur. On en tire la version, la date du binaire, son empreinte courte, et surtout son chemin, qui est l'argument du redéploiement. L'entête affiche alors retard · v0.1.0 · 08/09/2026 au lieu d'un simple « en retard », et le panneau donne la commande à copier :

make deploy HOST=vps.example.fr REMOTE_PATH=/tmp/agent

Une fois cette bascule faite, l'agent se met à jour depuis la console pour toujours. Comme les deux sondes passent par une connexion jetable, un agent qui raccroche ne coûte jamais la session de travail.

La console embarque aussi l'agent de son propre build. make win construit l'agent Linux puis l'inclut dans l'exécutable Windows : elle peut donc pousser exactement l'agent qui lui correspond, sans réseau ni jeton : la correspondance des versions est garantie par construction plutôt que par convention. Si la console a été construite sans (EMBED_AGENT=0), cette source est grisée.

🔧 Comment le remplacement se passe, et ce qui le protège

Trois contraintes dictent la marche à suivre :

  • on ne peut pas écrire sur un exécutable en cours (ETXTBSY), mais on peut le remplacer par un rename : l'ancien inode reste mappé par le processus vivant, le nouveau prend sa place dans le dossier ;
  • le fichier temporaire doit être dans le même dossier que la cible, sinon le rename traverse un point de montage et échoue ;
  • l'agent répond avant de se relancer, sinon la console voit une coupure sans savoir si la mise à jour a abouti.

La relance est un exec : le PID est conservé, seule l'image change. Pour vérifier qu'elle a bien eu lieu, comparer l'inode de /proc/<pid>/exe à celui du fichier ; une image non remplacée apparaîtrait comme supprimée.

Le contenu reçu est vérifié avant toute écriture : taille plausible, en-tête ELF, et empreinte SHA-256. Un transfert tronqué, une archive ou un exécutable Windows sont refusés sans que l'agent en place soit touché, et la session survit au refus.

⚠️ Remplacer l'agent à distance revient à exécuter du code arbitraire sur l'hôte. Cette version n'a pas d'authentification et expose déjà Exec : la mise à jour n'ouvre donc pas une catégorie de risque nouvelle. Les contrôles ci-dessus visent l'accident, pas l'attaque.

Déployer l'agent sur un hôte

make deploy HOST=ep1.example.fr           # /tmp/agent par défaut
make deploy HOST=ep1.example.fr REMOTE_PATH=/usr/local/bin/agent

Le script construit le binaire, l'envoie, reprend les arguments du processus en place (bind explicite, port non standard) et relance, puis vérifie avec un probe. Il n'y a pas d'hôte par défaut : make deploy tout court ne pousse rien nulle part.

⚠️ Ne jamais arrêter l'agent distant avec pkill -f "agent serve" : la ligne de commande du shell qui exécute la commande contient elle aussi le motif, et se fait tuer au passage. Le script cible le processus par son nom (pgrep -x agent).

Smoke test sans la GUI, pratique pour valider la connectivité mesh :

./agent probe 100.120.45.50:7878 --exec 'uptime; systemctl is-system-running'

Console (Windows et Linux)

Le frontend est prébuildé en statique dans console/ui/dist/, que Tauri embarque. Conséquence : le build de l'exécutable n'a besoin que de Rust, pas de Node.

Le plus simple est de prendre l'archive déjà construite : la CI publie à chaque poussée sur main une release roulante console-latest avec un zip par plateforme, tout ce qui va ensemble dedans. Sinon, depuis les sources :

make win      # -> dist-win64/netbird-ops-console-windows-x86_64.zip  (cross-compilé)
make linux    # -> dist-linux/netbird-ops-console-linux-x86_64.zip

Un zip plutôt que des fichiers en vrac parce que l'exécutable Windows ne démarre pas sans WebView2Loader.dll à côté de lui : livrer les deux séparément, c'est livrer une question. L'archive règle aussi un défaut du transport par artefacts de CI, qui fait perdre son bit exécutable à un binaire Linux ; à l'intérieur d'un zip, il le garde.

Moteur de rendu Prérequis à l'exécution
Windows WebView2 préinstallé sur Windows 11, rien à faire
Linux WebKitGTK libwebkit2gtk-4.1-0, libgtk-3-0, libsoup-3.0-0

C'est la différence de fond entre les deux : Windows fournit son moteur, Linux attend qu'il soit dans le système. Les deux binaires embarquent en revanche la même chose, le frontend et l'agent du même commit.

Sous Windows, cargo tauri build reste possible pour produire en plus un installeur NSIS (cargo install tauri-cli --version "^2").

Au premier lancement : + dans la barre latérale pour ajouter un hôte (nom et adresse, par exemple 100.120.45.50:7878), ou l'icône d'import pour reprendre ceux de ~/.ssh/config, puis pour se connecter à son agent. Les hôtes sont mémorisés localement.

⚠️ Le piège du build Windows : la feature custom-protocol

generate_context! décide d'embarquer ou non le frontend avec :

dev: cfg!(not(feature = "custom-protocol"))

Sans cette feature, Tauri embarque un jeu d'assets vide et la fenêtre charge devUrl (localhost:5173). L'exécutable compile, se lie et se lance : rien ne signale que le frontend n'est pas dedans. cargo tauri build passe le flag tout seul, mais la cross-compilation utilise un cargo build nu, qui doit le passer explicitement. C'est fait dans make win.

Deux garde-fous, dans scripts/check-frontend.sh, appelés automatiquement par make win (ou seuls via make verify) :

  • avant le build : index.html référence-t-il des assets qui existent vraiment, et le dist est-il plus récent que les sources ?
  • après le build : les clés de la table d'assets (/index.html, /assets/index-*.js) sont-elles présentes dans le binaire ? Elles y sont écrites en clair, donc leur absence prouve un build sans frontend.

Symptôme associé, si ça arrive quand même : le webview affiche une ancienne version en cache au lieu d'une page blanche, parce que WebView2 garde http://tauri.localhost/index.html d'un lancement précédent. D'où additionalBrowserArgs avec --disable-http-cache dans tauri.conf.json, et la cible make clean-webview pour vider ce cache sans toucher aux hôtes mémorisés.

🤖 Intégration continue (Forgejo)

.forgejo/workflows/ci.yml, syntaxe compatible GitHub Actions. Quatre jobs :

Job Ce qu'il fait
rust cargo fmt --check, clippy -D warnings et tests sur les deux workspaces
frontend tests vitest, build, et vérification que console/ui/dist committé correspond aux sources
agent build release, publié en artefact agent-linux-x86_64
linux build natif de la console (GTK/WebKit), artefact console-linux-x86_64
publish sur main seulement : dépose l'agent dans agent-latest, celle que la console interroge, et les deux zips du client dans console-latest
windows cross-compilation mingw, contrôles d'embarquement, artefact console-windows-x86_64

Les runners sont Linux uniquement, ce qui n'empêche pas de produire l'exécutable Windows : il est de toute façon cross-compilé, exactement comme avec make win en local.

Les étapes appellent les cibles du Makefile plutôt que de recopier les commandes, pour que CI et poste de travail ne puissent pas diverger. make ci enchaîne localement la même séquence. Même raisonnement pour l'installation de la toolchain, dans scripts/ci-rust.sh : quatre jobs en ont besoin, et la recopier quatre fois avait déjà produit un rustup-init mal appelé qui cassait la CI.

Le job publish ne reconstruit rien : il récupère les artefacts des autres jobs et publie exactement ces octets-là, ce qui évite qu'un binaire contrôlé et un binaire publié puissent différer. Deux releases plutôt qu'une, parce que agent-latest est un contrat lu par la console, qui y cherche un artefact nommé agent : y déposer aussi les clients rendrait ce contrat moins lisible. Chaque fichier est publié avec son .sha256. Il réécrit tag et release à chaque fois, pour que agent-latest pointe toujours le dernier commit de main : supprimer la release sans supprimer le tag laisserait la suivante s'ouvrir sur l'ancien commit. Il utilise le jeton éphémère du runner, ou un secret FORGE_TOKEN s'il existe. Beaucoup d'instances donnent au runner un jeton en lecture seule, avec lequel la publication échouerait en 403.

Le contrôle « dist committé à jour » mérite un mot : console/ui/dist est versionné exprès, et un dist périmé produit un exécutable qui embarque une vieille interface sans que rien ne le signale. Le job rebuild le frontend et échoue si le résultat diffère de ce qui est committé. Il suppose la même version majeure de Node qu'en local (24), sinon les empreintes de fichiers peuvent varier sans que le contenu change.

Raccourcis (Makefile)
make full-rebuild   table rase puis reconstruction complète
make ui        rebuild du frontend -> console/ui/dist   (nécessite Node)
make win       livrable Windows (zip) -> dist-win64/
make linux     livrable Linux   (zip) -> dist-linux/
make client    les deux : le cas courant après une modif d'UI
make verify    contrôle seul : dist cohérent + frontend bien embarqué
make agent     binaire Linux de l'agent
make deploy HOST=ep1.example.fr    envoie l'agent sur un hôte et le relance

make check     fmt + clippy -D warnings + tests, sur les deux workspaces
make ci        la séquence complète de la forge, en local
make test-ui   tests du frontend (vitest)
make clean     supprime target/, console/target/, console/target-win/, dist-win64/
make clean-webview  vide le cache HTTP du webview Windows
make mrproper  clean + node_modules

make win cross-compile depuis Linux (x86_64-pc-windows-gnu), ce qui permet de tout faire depuis WSL sans repasser par Windows. Prérequis, une fois :

rustup target add x86_64-pc-windows-gnu
sudo apt install mingw-w64

Tous les artefacts de compilation vont dans ~/.cache/netbird-ops/, donc sur ext4 et pas sous /mnt/c. Trois raisons, toutes vécues sur ce dépôt :

  • le pont 9p divise la vitesse par cinq (1 min contre 5 min pour l'exe Windows) ;
  • Windows peut poser un verrou sur un artefact, qui devient alors ni réinscriptible ni supprimable, et le build échoue sur un Permission denied sans rapport avec le code ;
  • sous compilation parallèle, cargo se prend des No such file or directory sur des fichiers qu'il vient lui-même de créer.

Seuls les artefacts bougent : le dépôt reste sous Documents, accessible depuis Windows. Les emplacements se changent avec LINUX_DIR=, CONSOLE_DIR= et WIN_DIR=.

Il consomme console/ui/dist/ tel quel, sans toucher à Node : c'est tout l'intérêt d'avoir ce dossier versionné. Pour la même raison, make clean ne le supprime pas.

🔧 Rebuild du frontend et mode dev

À faire après toute modification de console/ui/ :

cd console/ui
npm install          # une fois
npm run build        # -> console/ui/dist/

Peut se faire sous WSL avec Node natif ; dist/ est ensuite consommé tel quel par le build Windows. C'est pour ça que dist/ est versionné volontairement.

Run de dev avec rechargement à chaud (nécessite Node) :

cd console
cargo tauri dev
🐧 Développement sous WSL

L'agent se build et se teste nativement. La GUI peut aussi tourner sous WSLg pour du debug, à condition d'installer les libs Tauri :

sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev \
                 libsoup-3.0-dev libjavascriptcoregtk-4.1-dev \
                 libayatana-appindicator3-dev
cd console && cargo build

En build debug lancé hors cargo tauri dev, la fenêtre est blanche : Tauri charge alors devUrl (localhost:5173). Lance Vite (npm --prefix ui run dev) ou build en release, qui embarque dist/.

📓 Runbooks

Des actions scriptées déclarées à l'avance, lancées en un clic sur l'hôte sélectionné. Deux catalogues indépendants, cumulables ou consultables séparément via le sélecteur Les deux / Console / Hôte :

Catalogue Fichier système (root) Fichier utilisateur
💼 Console, portable, vaut pour tous les hôtes %ProgramData%\NetBirdOps\runbooks.toml %APPDATA%\NetBirdOps\runbooks.toml
🖥️ Hôte, propre à la machine /etc/netbird-ops/runbooks.toml ~/.config/netbird-ops/runbooks.toml

Dans chaque catalogue, le fichier utilisateur surcharge le fichier système : un même id y est redéfini. Les boutons Créer un fichier d'exemple et Ouvrir dans l'éditeur évitent d'aller chercher le chemin à la main.

[[runbook]]
id          = "journal-purge"
name        = "Purger le journal"
group       = "Maintenance"
description = "Réduit /var/log/journal à la taille demandée."
danger      = true                  # rouge + confirmation obligatoire
script      = """
journalctl --vacuum-size="$taille"
"""

  [[runbook.params]]
  name    = "taille"
  label   = "Taille cible"
  default = "200M"
  choices = ["100M", "200M", "500M", "1G"]   # liste fermée au lieu d'un champ libre
🔒 Comment un runbook est réellement exécuté

Le script part en Exec : sortie streamée et annulable comme n'importe quelle commande.

Les paramètres ne sont jamais interpolés dans le texte du script. Ils sont posés en variables shell échappées côté Rust, au-dessus du corps :

set -u
taille='200M'

journalctl --vacuum-size="$taille"

Une valeur hostile du genre a'; rm -rf / # reste donc une chaîne, et un test le vérifie. Le set -u fait échouer franchement une variable non déclarée au lieu de la réduire silencieusement à du vide.

Un runbook marqué confirm ou danger affiche le script réellement composé avant de partir, pas seulement son libellé.

🧱 Pare-feu

Les règles de l'hôte, en tableaux, une carte par chaîne. L'ordre des règles dans une chaîne est précisément ce qui décide du sort d'un paquet : un tableau global le noierait, donc chaque chaîne garde le sien.

Chaque chaîne affiche son point d'accroche (hook input, postrouting...), sa priorité et sa policy. Une chaîne sans hook est signalée comme chaîne utilisateur : elle n'est atteinte que par un jump, elle ne filtre rien d'elle même. Les colonnes couvrent interface d'entrée et de sortie, source, destination, protocole, port, état de connexion, verdict et compteurs. Filtres par famille, par interface et par texte ; les règles qui ont déjà compté des paquets sont marquées.

Lecture seule pour l'instant. L'édition est prévue pour une v2, et chaque règle porte déjà son handle, qui est exactement ce dont nft a besoin pour insérer ou supprimer au bon endroit.

🔍 D'où viennent les règles

Deux sources, dans cet ordre :

  1. nftables via nft --json list ruleset. C'est la source de vérité sur Debian récent, y compris quand les règles ont été posées avec iptables, qui est en réalité iptables-nft.
  2. iptables legacy via iptables-save et ip6tables-save, pour les hôtes qui n'ont pas migré.

Le JSON donne la structure, mais rendre une expression nftables en texte lisible reviendrait à réimplémenter le formateur de nft. La console récupère donc aussi la sortie de nft -a list ruleset et apparie les deux par handle : chaque règle porte à la fois ses champs exploitables et sa ligne exacte, dépliable d'un clic. Un appariement par position se décalerait au premier ruleset réordonné.

🔑 Lire un ruleset demande CAP_NET_ADMIN. Si l'agent ne tourne pas en root, il retente via sudo -n et le signale dans un bandeau. En cas d'échec, la raison est affichée plutôt qu'une liste vide sans explication.

👥 Comptes et clés SSH

Utilisateurs et groupes locaux lus dans /etc/passwd et /etc/group, sans aucun appel shell. Fiche par utilisateur (uid/gid, home, shell, groupe primaire, groupes secondaires, connexion possible ou non), vue groupes séparée, et gestion complète des authorized_keys : algorithme, taille, empreinte SHA256:... identique à ssh-keygen -l, commentaire, options, avec ajout, modification et retrait.

🛡️ Les garde-fous sur authorized_keys

Une clé est identifiée par son matériel, pas par sa position. Un authorized_keys réordonné ou édité à la main entre deux appels ne fait pas modifier ou supprimer la mauvaise ligne. Les lignes que l'agent ne sait pas analyser (commentaires de l'admin, en-têtes) sont conservées telles quelles.

L'écriture est atomique : temporaire voisin, chmod 600, chown, puis rename. sshd ne voit jamais un fichier tronqué ou trop permissif.

La saisie est validée par le même analyseur que celui qui écrira. Une clé privée collée par erreur, ou une ligne qui annonce ssh-rsa avec un blob ed25519, sont refusées avant même l'aller-retour réseau.

🧭 Pistes d'amélioration (hors scope test)

  • 🔐 Authentification : token pré-partagé HMAC sur challenge, ou mTLS.
  • 📦 Transfert de fichiers chunké (actuellement en un bloc base64).
  • 🔁 Reconnexion automatique, multi-sélection d'hôtes, exécution en parallèle.
  • 📈 Historique de métriques persistant, latence entre hôtes du mesh.