feat(vpn): VPN on-demand Tailscale + agent studioE5 standalone

- Agent studioE5 standalone en Go (console + systray)
- VPN on-demand via tailscaled + tailscale up (authkey Headscale)
- Resolver/serveur dans le tailnet studioe5
- Caddy on-demand TLS pour les instances
- Nouveaux endpoints serveur /api/internal/send-to-node
- Suppression des anciens binaires edubox-agent
- Suivi dans SUIVI_VPN_ONDEMAND.md
This commit is contained in:
EduBox Dev
2026-06-23 09:48:00 +00:00
parent dd49993157
commit 124543d658
40 changed files with 1303 additions and 485 deletions
+47 -13
View File
@@ -5,21 +5,22 @@ import (
"log"
"os"
"path/filepath"
"runtime"
"time"
)
// version is injected at build time via -ldflags "-X main.version=X.Y.Z"
var version = "dev"
const AGENT_VERSION = "0.3.0"
const (
AGENT_VERSION = "0.3.0"
APP_NAME = "studioE5"
)
var (
serverAddr = flag.String("server", "ws://localhost:3001", "Adresse WebSocket du serveur")
nodeID = flag.String("node-id", defaultNodeID(), "ID du nœud (défaut: hostname)")
dataDir = flag.String("data-dir", "./edubox-data", "Répertoire de données")
uiEnabled = flag.Bool("ui", true, "Activer l'interface locale HTMX")
headscaleURL = flag.String("headscale-url", "", "URL du serveur Headscale (ex: http://151.80.60.98:8080)")
headscaleAuthKey = flag.String("headscale-auth-key", "", "Clé d'authentification Headscale")
dataDir = flag.String("data-dir", "./studioE5-data", "Répertoire de données")
uiEnabled = flag.Bool("ui", true, "Activer l'interface locale HTMX")
noTray = flag.Bool("no-tray", false, "Désactiver l'icône dans la barre système (mode console)")
)
func defaultNodeID() string {
@@ -49,19 +50,52 @@ func main() {
log.Fatalf("Cannot create data-dir: %v", err)
}
log.Printf("[EduBox Agent] version=%s node=%s data-dir=%s", AGENT_VERSION, *nodeID, *dataDir)
cfg, created, err := loadOrCreateConfig(*dataDir)
if err != nil {
log.Fatalf("Cannot load config: %v", err)
}
if cfg.Server == "" {
cfg.Server = "ws://localhost:3001"
}
if cfg.NodeID == "" {
cfg.NodeID = defaultNodeID()
}
if cfg.DataDir == "" {
cfg.DataDir = *dataDir
}
if err := saveConfig(*dataDir, cfg); err != nil {
log.Fatalf("Cannot save config: %v", err)
}
log.Printf("[%s Agent] version=%s node=%s data-dir=%s server=%s", APP_NAME, AGENT_VERSION, cfg.NodeID, *dataDir, cfg.Server)
if *uiEnabled {
go startUI(*dataDir, *nodeID, *serverAddr)
go startUI(*dataDir, cfg.NodeID, cfg.Server)
if created {
go openBrowser(uiURL + "#settings")
}
}
go startWebSocket(*serverAddr, *nodeID, *dataDir)
go startWebSocket(cfg.Server, cfg.NodeID, *dataDir, cfg.HeadscaleURL, cfg.HeadscaleAuthKey)
if *headscaleURL != "" && *headscaleAuthKey != "" {
go startTailscaleAndReport(*dataDir, *nodeID, *headscaleURL, *headscaleAuthKey)
shutdownCh := make(chan struct{})
if *noTray {
log.Printf("[%s Agent] running in console mode (no tray)", APP_NAME)
<-shutdownCh
return
}
select {}
// Run tray on its own locked OS thread; keep main blocked so the process
// does not exit when systray is not available (e.g. headless Linux).
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
runTray(APP_NAME, shutdownCh)
}()
<-shutdownCh
}
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {