- Rust 51%
- Svelte 37.4%
- Shell 4%
- SCSS 2.8%
- JavaScript 2.5%
- Other 2.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
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
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 |
||
| .forgejo/workflows | ||
| agent | ||
| console | ||
| protocol | ||
| screens | ||
| scripts | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Makefile | ||
| README.md | ||
🛰️ 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 : 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
Les unités systemd, avec filtre et actions groupées (démarrer, arrêter, redémarrer, statut).
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 à
AgentInfone 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'idet 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::Unknownau 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 unrename: 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
renametraverse 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.htmlréférence-t-il des assets qui existent vraiment, et ledistest-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 deniedsans rapport avec le code ; - sous compilation parallèle, cargo se prend des
No such file or directorysur 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 alorsdevUrl(localhost:5173). Lance Vite (npm --prefix ui run dev) ou build en release, qui embarquedist/.
📓 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 :
- 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 aveciptables, qui est en réalitéiptables-nft. - iptables legacy via
iptables-saveetip6tables-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 viasudo -net 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.


