feat(vpn): intégration Tailscale/Headscale + URLs publiques par sous-domaine
- Ajout d'un conteneur Tailscale côté serveur pour joindre les agents via IPs Tailscale - Configuration Headscale exposé en HTTPS via Caddy (headscale.alfrednobel.edudeploy.com) - Caddy configuré pour les sous-domaines avec TLS on-demand - Middleware et route proxy Next.js pour router les sous-domaines vers les agents - Ajout du champ domain sur Establishment et affichage de l'URL publique dans le dashboard - Agent Windows v0.2.3 avec proxy Tailscale par instance pour contourner Docker Desktop - Templates WordPress/PrestaShop bindés sur 0.0.0.0 pour être accessibles via Tailscale
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
VERSION="0.2.1"
|
||||
VERSION="0.2.3"
|
||||
LDFLAGS="-X main.version=${VERSION}"
|
||||
|
||||
echo "Building EduBox Agent v${VERSION}..."
|
||||
|
||||
+34
-5
@@ -6,16 +6,19 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// version is injected at build time via -ldflags "-X main.version=X.Y.Z"
|
||||
var version = "dev"
|
||||
|
||||
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")
|
||||
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")
|
||||
)
|
||||
|
||||
func defaultNodeID() string {
|
||||
@@ -51,5 +54,31 @@ func main() {
|
||||
go startUI(*dataDir, *nodeID, *serverAddr)
|
||||
}
|
||||
|
||||
startWebSocket(*serverAddr, *nodeID, *dataDir)
|
||||
go startWebSocket(*serverAddr, *nodeID, *dataDir)
|
||||
|
||||
if *headscaleURL != "" && *headscaleAuthKey != "" {
|
||||
go startTailscaleAndReport(*dataDir, *nodeID, *headscaleURL, *headscaleAuthKey)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) {
|
||||
tsDir := filepath.Join(dataDir, "tailscale")
|
||||
ip, err := startTailscale(tsDir, nodeID, headscaleURL, authKey)
|
||||
if err != nil {
|
||||
log.Printf("Tailscale error: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Tailscale IP obtained: %s", ip)
|
||||
|
||||
for {
|
||||
if err := sendMessage(WSMessage{Action: "tailscale_ip", NodeID: nodeID, TailscaleIP: ip}); err != nil {
|
||||
log.Printf("Waiting for WebSocket to send tailscale_ip...")
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Printf("Sent tailscale_ip to server: %s", ip)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
+81
-4
@@ -1,14 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
func startTailscale(dataDir string, nodeID string) (net.Listener, error) {
|
||||
var globalTSServer *tsnet.Server
|
||||
|
||||
func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, error) {
|
||||
// Configure tsnet to use our Headscale server
|
||||
os.Setenv("TS_AUTHKEY", authKey)
|
||||
os.Setenv("TS_CONTROL_URL", headscaleURL)
|
||||
|
||||
s := &tsnet.Server{
|
||||
Hostname: nodeID,
|
||||
Dir: dataDir,
|
||||
@@ -16,13 +26,80 @@ func startTailscale(dataDir string, nodeID string) (net.Listener, error) {
|
||||
}
|
||||
|
||||
if err := s.Start(); err != nil {
|
||||
return nil, fmt.Errorf("tailscale start: %w", err)
|
||||
return "", fmt.Errorf("tailscale start: %w", err)
|
||||
}
|
||||
|
||||
ln, err := s.Listen("tcp", ":0")
|
||||
globalTSServer = s
|
||||
|
||||
// Wait for Tailscale to come up and retrieve IP
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tailscale listen: %w", err)
|
||||
return "", fmt.Errorf("tailscale local client: %w", err)
|
||||
}
|
||||
|
||||
var tailscaleIP string
|
||||
for {
|
||||
status, err := lc.Status(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tailscale status: %w", err)
|
||||
}
|
||||
if status.Self != nil && len(status.Self.TailscaleIPs) > 0 {
|
||||
tailscaleIP = status.Self.TailscaleIPs[0].String()
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", fmt.Errorf("tailscale IP timeout")
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Tailscale started with IP: %s", tailscaleIP)
|
||||
return tailscaleIP, nil
|
||||
}
|
||||
|
||||
func startTailscaleProxy(port int) (net.Listener, error) {
|
||||
if globalTSServer == nil {
|
||||
return nil, fmt.Errorf("tailscale server not started")
|
||||
}
|
||||
ln, err := globalTSServer.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tailscale listen on port %d: %w", port, err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Printf("tailscale proxy accept error on port %d: %v", port, err)
|
||||
return
|
||||
}
|
||||
go handleProxyConn(conn, port)
|
||||
}
|
||||
}()
|
||||
log.Printf("Tailscale proxy started on port %d", port)
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
func handleProxyConn(src net.Conn, port int) {
|
||||
defer src.Close()
|
||||
dst, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
|
||||
if err != nil {
|
||||
log.Printf("tailscale proxy dial localhost:%d error: %v", port, err)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
done := make(chan struct{}, 2)
|
||||
go func() {
|
||||
_, _ = io.Copy(dst, src)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
_, _ = io.Copy(src, dst)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
<-done
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -20,6 +21,7 @@ type WSMessage struct {
|
||||
StudentId string `json:"studentId,omitempty"`
|
||||
StudentName string `json:"studentName,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
TailscaleIP string `json:"tailscaleIp,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -27,6 +29,11 @@ var (
|
||||
mainConnMu sync.Mutex
|
||||
)
|
||||
|
||||
var (
|
||||
tsProxies = make(map[int]net.Listener)
|
||||
tsProxiesMu sync.Mutex
|
||||
)
|
||||
|
||||
func sendMessage(msg WSMessage) error {
|
||||
mainConnMu.Lock()
|
||||
defer mainConnMu.Unlock()
|
||||
@@ -197,12 +204,32 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
// Start Tailscale proxy so the server can reach localhost via Tailscale IP
|
||||
tsProxiesMu.Lock()
|
||||
if _, exists := tsProxies[msg.Port]; !exists {
|
||||
if ln, err := startTailscaleProxy(msg.Port); err == nil {
|
||||
tsProxies[msg.Port] = ln
|
||||
} else {
|
||||
log.Printf("startTailscaleProxy error: %v", err)
|
||||
}
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
|
||||
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||
sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
||||
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||
case "stop":
|
||||
log.Printf("Stop instance %s", msg.InstanceID)
|
||||
// Stop Tailscale proxy for this instance port
|
||||
if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil {
|
||||
tsProxiesMu.Lock()
|
||||
if ln, exists := tsProxies[inst[msg.InstanceID].Port]; exists {
|
||||
_ = ln.Close()
|
||||
delete(tsProxies, inst[msg.InstanceID].Port)
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
}
|
||||
if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil {
|
||||
log.Printf("dockerComposeDown error: %v", err)
|
||||
}
|
||||
@@ -226,6 +253,17 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
// Start Tailscale proxy so the server can reach localhost via Tailscale IP
|
||||
tsProxiesMu.Lock()
|
||||
if _, exists := tsProxies[msg.Port]; !exists {
|
||||
if ln, err := startTailscaleProxy(msg.Port); err == nil {
|
||||
tsProxies[msg.Port] = ln
|
||||
} else {
|
||||
log.Printf("startTailscaleProxy error: %v", err)
|
||||
}
|
||||
}
|
||||
tsProxiesMu.Unlock()
|
||||
|
||||
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||
sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
||||
|
||||
Reference in New Issue
Block a user