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
+81 -4
View File
@@ -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
}