rmm2/agent/ws/tunnel.go
cynfo3000 fa68e191f9 Fix Tunnel: ID-Sync Backend<->Agent, bidirektionaler Proxy, API-Key via Query-Param
- Backend gibt tunnel_id vor, Agent uebernimmt sie
- Proxy-Verbindung bidirektional: Daten vom Agent zurueck an TCP-Client
- Middleware akzeptiert api_key als Query-Parameter (fuer WebSocket)
- Getestet: OPNsense WebUI erreichbar ueber Tunnel
2026-02-28 07:52:38 +01:00

212 lines
4.6 KiB
Go

package ws
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"log"
"net"
"sync"
"time"
)
type Tunnel struct {
ID string
TargetHost string
TargetPort int
Conn net.Conn
Active bool
CreatedAt time.Time
}
type TunnelManager struct {
client *Client
tunnels map[string]*Tunnel
mutex sync.RWMutex
}
func NewTunnelManager(client *Client) *TunnelManager {
return &TunnelManager{
client: client,
tunnels: make(map[string]*Tunnel),
}
}
func (tm *TunnelManager) OpenTunnel(targetHost string, targetPort int, tunnelID ...string) (string, error) {
// Tunnel-ID vom Backend uebernehmen oder selbst generieren
id := ""
if len(tunnelID) > 0 && tunnelID[0] != "" {
id = tunnelID[0]
} else {
id = tm.generateTunnelID()
}
// Verbindung zum Ziel aufbauen
target := fmt.Sprintf("%s:%d", targetHost, targetPort)
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
if err != nil {
return "", fmt.Errorf("Verbindung zu %s fehlgeschlagen: %w", target, err)
}
// Tunnel erstellen
tunnel := &Tunnel{
ID: id,
TargetHost: targetHost,
TargetPort: targetPort,
Conn: conn,
Active: true,
CreatedAt: time.Now(),
}
// Tunnel registrieren
tm.mutex.Lock()
tm.tunnels[id] = tunnel
tm.mutex.Unlock()
// Data-Forwarding starten
go tm.forwardData(tunnel)
return id, nil
}
func (tm *TunnelManager) CloseTunnel(tunnelID string) error {
tm.mutex.Lock()
tunnel, exists := tm.tunnels[tunnelID]
if !exists {
tm.mutex.Unlock()
return fmt.Errorf("Tunnel %s nicht gefunden", tunnelID)
}
tunnel.Active = false
delete(tm.tunnels, tunnelID)
tm.mutex.Unlock()
// Verbindung schließen
if tunnel.Conn != nil {
tunnel.Conn.Close()
}
return nil
}
func (tm *TunnelManager) GetActiveTunnels() []string {
tm.mutex.RLock()
defer tm.mutex.RUnlock()
tunnels := make([]string, 0, len(tm.tunnels))
for id := range tm.tunnels {
tunnels = append(tunnels, id)
}
return tunnels
}
func (tm *TunnelManager) SendDataToTunnel(tunnelID string, data []byte) error {
tm.mutex.RLock()
tunnel, exists := tm.tunnels[tunnelID]
tm.mutex.RUnlock()
if !exists || !tunnel.Active {
return fmt.Errorf("Tunnel %s nicht aktiv", tunnelID)
}
if tunnel.Conn == nil {
return fmt.Errorf("Tunnel %s hat keine Verbindung", tunnelID)
}
_, err := tunnel.Conn.Write(data)
if err != nil {
// Bei Fehler Tunnel schließen
tm.CloseTunnel(tunnelID)
return fmt.Errorf("Daten schreiben fehlgeschlagen: %w", err)
}
return nil
}
func (tm *TunnelManager) forwardData(tunnel *Tunnel) {
defer func() {
if tunnel.Active {
tm.CloseTunnel(tunnel.ID)
}
// Event senden: Tunnel geschlossen
event := Message{
Type: "event",
Event: "tunnel_closed",
Data: map[string]interface{}{
"tunnel_id": tunnel.ID,
},
}
tm.client.SendMessage(event)
log.Printf("Tunnel %s: Data-Forwarding beendet", tunnel.ID)
}()
buffer := make([]byte, 32768) // 32KB Buffer
for tunnel.Active {
// Read-Timeout setzen um regelmäßig Active-Status zu prüfen
tunnel.Conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := tunnel.Conn.Read(buffer)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
// Timeout ist OK, einfach weiter versuchen
continue
}
if err == io.EOF {
log.Printf("Tunnel %s: Ziel-Verbindung geschlossen", tunnel.ID)
} else {
log.Printf("Tunnel %s: Read-Fehler: %v", tunnel.ID, err)
}
break
}
if n > 0 {
// Daten über WebSocket an Backend weiterleiten
data := buffer[:n]
if err := tm.client.messageHandler.SendTunnelData(tunnel.ID, data); err != nil {
log.Printf("Tunnel %s: WebSocket senden fehlgeschlagen: %v", tunnel.ID, err)
break
}
}
}
}
func (tm *TunnelManager) generateTunnelID() string {
bytes := make([]byte, 8)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Backend
func (tm *TunnelManager) ProcessBinaryMessage(data []byte) error {
tunnelID, payload, err := ParseTunnelData(data)
if err != nil {
return fmt.Errorf("Binary-Message parsen: %w", err)
}
return tm.SendDataToTunnel(tunnelID, payload)
}
// CleanupInactiveTunnels schließt inaktive Tunnel (Cleanup-Task)
func (tm *TunnelManager) CleanupInactiveTunnels() {
tm.mutex.Lock()
defer tm.mutex.Unlock()
now := time.Now()
for id, tunnel := range tm.tunnels {
// Tunnel die länger als 1 Stunde inaktiv sind, schließen
if !tunnel.Active || now.Sub(tunnel.CreatedAt) > time.Hour {
tunnel.Active = false
if tunnel.Conn != nil {
tunnel.Conn.Close()
}
delete(tm.tunnels, id)
log.Printf("Inaktiver Tunnel %s bereinigt", id)
}
}
}