- 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
357 lines
8.4 KiB
Go
357 lines
8.4 KiB
Go
package ws
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type BackendTunnel struct {
|
|
ID string
|
|
AgentID string
|
|
TargetHost string
|
|
TargetPort int
|
|
ProxyPort int
|
|
Listener net.Listener
|
|
Active bool
|
|
CreatedAt time.Time
|
|
// Aktive Proxy-Verbindungen (fuer Rueckkanal)
|
|
proxyConn net.Conn
|
|
proxyMutex sync.Mutex
|
|
}
|
|
|
|
type TunnelManager struct {
|
|
hub *Hub
|
|
tunnels map[string]*BackendTunnel
|
|
mutex sync.RWMutex
|
|
|
|
// Port-Range für dynamische Tunnel-Proxies
|
|
portStart int
|
|
portEnd int
|
|
usedPorts map[int]bool
|
|
}
|
|
|
|
func NewTunnelManager(hub *Hub) *TunnelManager {
|
|
return &TunnelManager{
|
|
hub: hub,
|
|
tunnels: make(map[string]*BackendTunnel),
|
|
portStart: 10000,
|
|
portEnd: 20000,
|
|
usedPorts: make(map[int]bool),
|
|
}
|
|
}
|
|
|
|
func (tm *TunnelManager) OpenTunnel(agentID, targetHost string, targetPort int) (*BackendTunnel, error) {
|
|
// Prüfen ob Agent verbunden ist
|
|
if !tm.hub.IsAgentConnected(agentID) {
|
|
return nil, ErrAgentNotConnected
|
|
}
|
|
|
|
// Tunnel-ID generieren
|
|
tunnelID := tm.generateTunnelID()
|
|
|
|
// Freien Port finden
|
|
proxyPort, err := tm.allocatePort()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Proxy-Port allokieren fehlgeschlagen: %w", err)
|
|
}
|
|
|
|
// TCP-Listener für Proxy erstellen
|
|
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", proxyPort))
|
|
if err != nil {
|
|
tm.releasePort(proxyPort)
|
|
return nil, fmt.Errorf("Proxy-Listener erstellen fehlgeschlagen: %w", err)
|
|
}
|
|
|
|
// Tunnel-Objekt erstellen
|
|
tunnel := &BackendTunnel{
|
|
ID: tunnelID,
|
|
AgentID: agentID,
|
|
TargetHost: targetHost,
|
|
TargetPort: targetPort,
|
|
ProxyPort: proxyPort,
|
|
Listener: listener,
|
|
Active: true,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// Tunnel registrieren
|
|
tm.mutex.Lock()
|
|
tm.tunnels[tunnelID] = tunnel
|
|
tm.mutex.Unlock()
|
|
|
|
// Proxy-Server starten
|
|
go tm.runTunnelProxy(tunnel)
|
|
|
|
// Command an Agent senden — tunnel_id vorgeben damit Backend und Agent dieselbe ID nutzen
|
|
cmd := map[string]interface{}{
|
|
"type": "command",
|
|
"id": tm.generateTunnelID(),
|
|
"command": "tunnel_open",
|
|
"params": map[string]interface{}{
|
|
"tunnel_id": tunnelID,
|
|
"target_host": targetHost,
|
|
"target_port": targetPort,
|
|
},
|
|
}
|
|
|
|
cmdJSON, _ := json.Marshal(cmd)
|
|
if err := tm.hub.SendToAgent(agentID, cmdJSON); err != nil {
|
|
tm.CloseTunnel(tunnelID)
|
|
return nil, fmt.Errorf("Tunnel-Command senden fehlgeschlagen: %w", err)
|
|
}
|
|
|
|
log.Printf("Tunnel %s geöffnet: Agent %s -> %s:%d (Proxy: :%d)",
|
|
tunnelID, agentID, targetHost, targetPort, proxyPort)
|
|
|
|
return tunnel, 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()
|
|
|
|
// Listener schließen
|
|
if tunnel.Listener != nil {
|
|
tunnel.Listener.Close()
|
|
}
|
|
|
|
// Port freigeben
|
|
tm.releasePort(tunnel.ProxyPort)
|
|
|
|
// Command an Agent senden
|
|
if tm.hub.IsAgentConnected(tunnel.AgentID) {
|
|
cmd := map[string]interface{}{
|
|
"type": "command",
|
|
"id": tm.generateTunnelID(),
|
|
"command": "tunnel_close",
|
|
"params": map[string]interface{}{
|
|
"tunnel_id": tunnelID,
|
|
},
|
|
}
|
|
|
|
cmdJSON, _ := json.Marshal(cmd)
|
|
tm.hub.SendToAgent(tunnel.AgentID, cmdJSON)
|
|
}
|
|
|
|
log.Printf("Tunnel %s geschlossen", tunnelID)
|
|
return nil
|
|
}
|
|
|
|
func (tm *TunnelManager) GetTunnel(tunnelID string) (*BackendTunnel, bool) {
|
|
tm.mutex.RLock()
|
|
defer tm.mutex.RUnlock()
|
|
|
|
tunnel, exists := tm.tunnels[tunnelID]
|
|
return tunnel, exists
|
|
}
|
|
|
|
func (tm *TunnelManager) GetAgentTunnels(agentID string) []*BackendTunnel {
|
|
tm.mutex.RLock()
|
|
defer tm.mutex.RUnlock()
|
|
|
|
var tunnels []*BackendTunnel
|
|
for _, tunnel := range tm.tunnels {
|
|
if tunnel.AgentID == agentID {
|
|
tunnels = append(tunnels, tunnel)
|
|
}
|
|
}
|
|
|
|
return tunnels
|
|
}
|
|
|
|
func (tm *TunnelManager) GetActiveTunnelCount() int {
|
|
tm.mutex.RLock()
|
|
defer tm.mutex.RUnlock()
|
|
|
|
return len(tm.tunnels)
|
|
}
|
|
|
|
func (tm *TunnelManager) CloseAgentTunnels(agentID string) {
|
|
tm.mutex.RLock()
|
|
var tunnelIDs []string
|
|
for id, tunnel := range tm.tunnels {
|
|
if tunnel.AgentID == agentID {
|
|
tunnelIDs = append(tunnelIDs, id)
|
|
}
|
|
}
|
|
tm.mutex.RUnlock()
|
|
|
|
for _, tunnelID := range tunnelIDs {
|
|
tm.CloseTunnel(tunnelID)
|
|
}
|
|
}
|
|
|
|
func (tm *TunnelManager) runTunnelProxy(tunnel *BackendTunnel) {
|
|
defer func() {
|
|
if tunnel.Active {
|
|
tm.CloseTunnel(tunnel.ID)
|
|
}
|
|
}()
|
|
|
|
log.Printf("Tunnel %s: Proxy läuft auf Port %d", tunnel.ID, tunnel.ProxyPort)
|
|
|
|
for tunnel.Active {
|
|
conn, err := tunnel.Listener.Accept()
|
|
if err != nil {
|
|
if tunnel.Active {
|
|
log.Printf("Tunnel %s: Accept-Fehler: %v", tunnel.ID, err)
|
|
}
|
|
break
|
|
}
|
|
|
|
// Jede eingehende Verbindung in eigener Goroutine behandeln
|
|
go tm.handleProxyConnection(tunnel, conn)
|
|
}
|
|
}
|
|
|
|
func (tm *TunnelManager) handleProxyConnection(tunnel *BackendTunnel, conn net.Conn) {
|
|
defer conn.Close()
|
|
|
|
log.Printf("Tunnel %s: Proxy-Verbindung von %s", tunnel.ID, conn.RemoteAddr())
|
|
|
|
// Aktive Proxy-Verbindung registrieren (fuer Rueckkanal vom Agent)
|
|
tunnel.proxyMutex.Lock()
|
|
tunnel.proxyConn = conn
|
|
tunnel.proxyMutex.Unlock()
|
|
|
|
defer func() {
|
|
tunnel.proxyMutex.Lock()
|
|
tunnel.proxyConn = nil
|
|
tunnel.proxyMutex.Unlock()
|
|
log.Printf("Tunnel %s: Proxy-Verbindung beendet", tunnel.ID)
|
|
}()
|
|
|
|
// Buffer fuer TCP-Data
|
|
buffer := make([]byte, 32768)
|
|
|
|
for tunnel.Active {
|
|
n, err := conn.Read(buffer)
|
|
if err != nil {
|
|
if err.Error() != "EOF" && tunnel.Active {
|
|
log.Printf("Tunnel %s: Proxy-Read-Fehler: %v", tunnel.ID, err)
|
|
}
|
|
break
|
|
}
|
|
|
|
if n > 0 {
|
|
// Daten ueber WebSocket an Agent weiterleiten
|
|
if err := tm.sendTunnelData(tunnel.ID, buffer[:n]); err != nil {
|
|
log.Printf("Tunnel %s: Weiterleitung fehlgeschlagen: %v", tunnel.ID, err)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tm *TunnelManager) sendTunnelData(tunnelID string, data []byte) error {
|
|
tunnel, exists := tm.GetTunnel(tunnelID)
|
|
if !exists || !tunnel.Active {
|
|
return fmt.Errorf("Tunnel nicht aktiv")
|
|
}
|
|
|
|
// Binary-Message-Format: [tunnel_id_length:1][tunnel_id][data]
|
|
tunnelIDBytes := []byte(tunnelID)
|
|
if len(tunnelIDBytes) > 255 {
|
|
return fmt.Errorf("tunnel_id zu lang")
|
|
}
|
|
|
|
message := make([]byte, 1+len(tunnelIDBytes)+len(data))
|
|
message[0] = byte(len(tunnelIDBytes))
|
|
copy(message[1:], tunnelIDBytes)
|
|
copy(message[1+len(tunnelIDBytes):], data)
|
|
|
|
return tm.hub.SendBinaryToAgent(tunnel.AgentID, message)
|
|
}
|
|
|
|
func (tm *TunnelManager) ProcessBinaryMessage(agentID string, data []byte) error {
|
|
// Binary-Message parsen: [tunnel_id_length:1][tunnel_id][data]
|
|
if len(data) < 1 {
|
|
return fmt.Errorf("Binary-Message zu kurz")
|
|
}
|
|
|
|
tunnelIDLen := int(data[0])
|
|
if len(data) < 1+tunnelIDLen {
|
|
return fmt.Errorf("Tunnel-ID zu kurz")
|
|
}
|
|
|
|
tunnelID := string(data[1 : 1+tunnelIDLen])
|
|
payload := data[1+tunnelIDLen:]
|
|
|
|
// Tunnel finden und Daten weiterleiten
|
|
tunnel, exists := tm.GetTunnel(tunnelID)
|
|
if !exists || !tunnel.Active || tunnel.AgentID != agentID {
|
|
return fmt.Errorf("Tunnel %s nicht gefunden oder inaktiv", tunnelID)
|
|
}
|
|
|
|
// Daten an aktive Proxy-Verbindung weiterleiten
|
|
tunnel.proxyMutex.Lock()
|
|
conn := tunnel.proxyConn
|
|
tunnel.proxyMutex.Unlock()
|
|
|
|
if conn == nil {
|
|
return fmt.Errorf("Tunnel %s: keine aktive Proxy-Verbindung", tunnelID)
|
|
}
|
|
|
|
_, writeErr := conn.Write(payload)
|
|
if writeErr != nil {
|
|
return fmt.Errorf("Tunnel %s: Proxy-Write fehlgeschlagen: %w", tunnelID, writeErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (tm *TunnelManager) allocatePort() (int, error) {
|
|
tm.mutex.Lock()
|
|
defer tm.mutex.Unlock()
|
|
|
|
for port := tm.portStart; port <= tm.portEnd; port++ {
|
|
if !tm.usedPorts[port] {
|
|
tm.usedPorts[port] = true
|
|
return port, nil
|
|
}
|
|
}
|
|
|
|
return 0, fmt.Errorf("keine freien Ports verfügbar")
|
|
}
|
|
|
|
func (tm *TunnelManager) releasePort(port int) {
|
|
tm.mutex.Lock()
|
|
defer tm.mutex.Unlock()
|
|
|
|
delete(tm.usedPorts, port)
|
|
}
|
|
|
|
func (tm *TunnelManager) generateTunnelID() string {
|
|
bytes := make([]byte, 8)
|
|
rand.Read(bytes)
|
|
return hex.EncodeToString(bytes)
|
|
}
|
|
|
|
// CreateHTTPProxy erstellt einen HTTP-Reverse-Proxy für einen Tunnel
|
|
func (tm *TunnelManager) CreateHTTPProxy(tunnelID string, w http.ResponseWriter, r *http.Request) error {
|
|
tunnel, exists := tm.GetTunnel(tunnelID)
|
|
if !exists || !tunnel.Active {
|
|
return fmt.Errorf("Tunnel nicht gefunden oder inaktiv")
|
|
}
|
|
|
|
// TODO: Implement HTTP-Reverse-Proxy durch Tunnel
|
|
// Das ist komplex und erfordert HTTP-Request-Serialisierung über WebSocket
|
|
|
|
http.Error(w, "HTTP-Proxy noch nicht implementiert", http.StatusNotImplemented)
|
|
return nil
|
|
} |