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:
root
2026-06-12 21:41:56 +00:00
parent 2dc9ba7b55
commit 852171cc59
18 changed files with 453 additions and 51 deletions
+38
View File
@@ -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})