diff --git a/README.md b/README.md index a2af5d9..b4d667d 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,66 @@ # OPNsense RMM System -Remote Monitoring & Management System fuer OPNsense Firewalls. +Remote Monitoring & Management System fuer OPNsense Firewalls mit WebSocket-basierter Kommunikation und TCP-Tunnel-Funktionalitaet. ## Ueberblick Dieses System besteht aus zwei Komponenten: -- **Backend** (Go) - REST API Server mit TLS und SQLite-Datenbank, laeuft auf einem Linux-Server -- **Agent** (Go) - Laeuft auf OPNsense/FreeBSD, sammelt Systemdaten und sendet sie an das Backend +- **Backend** (Go) - REST API Server mit WebSocket Hub, TLS und SQLite-Datenbank, laeuft auf einem Linux-Server +- **Agent** (Go) - Laeuft auf OPNsense/FreeBSD, sammelt Systemdaten, haelt WebSocket-Verbindung zum Backend und ermoeglicht Remote-Commands/Tunneling ## Architektur ``` -┌──────────────────┐ HTTPS (TLS) ┌──────────────────┐ -│ OPNsense FW │ ────────────────────────► │ RMM Backend │ -│ (FreeBSD) │ POST /agent/register │ (Linux) │ -│ │ POST /agent/heartbeat │ │ -│ rmm-agent │ │ REST API │ -│ - Hardware │ │ SQLite DB │ -│ - CPU/RAM │ │ :8443 │ -│ - Disk/Net │ │ │ -│ - Services │ │ 192.168.85.13 │ -│ │ │ │ -│ 192.168.85.33 │ │ │ -└──────────────────┘ └──────────────────┘ +┌──────────────────────┐ WebSocket + HTTPS (TLS) ┌──────────────────────┐ +│ OPNsense FW │ ◄─────────────────────────────► │ RMM Backend │ +│ (FreeBSD) │ wss://backend:8443/api/v1/ │ (Linux) │ +│ │ agent/ws │ │ +│ rmm-agent │ │ REST API │ +│ - Hardware/CPU/RAM │ POST /agent/register │ WebSocket Hub │ +│ - Disk/Network │ POST /agent/heartbeat │ Tunnel Manager │ +│ - Services/Certs │ │ SQLite DB │ +│ - WebSocket Client │ Commands: │ :8443 │ +│ - Tunnel Manager │ • exec (Befehl ausfuehren) │ │ +│ │ • tunnel_open/close │ 192.168.85.13 │ +│ 192.168.85.33 │ • tunnel_data (TCP-Proxy) │ │ +└──────────────────────┘ └──────────────────────┘ + + TCP-Tunnel Flow: + Browser/Client ──► Backend Proxy ──► WebSocket ──► Agent ──► OPNsense Service + (Port 10000-20000) (Binary) (Local) (z.B. :443, :22) ``` +## Neue Funktionen (WebSocket/Tunnel) + +### 1. Bidirektionale Kommunikation +- **WebSocket-Verbindung**: Agent baut nach Registrierung eine persistente WebSocket-Verbindung zum Backend auf +- **Automatisches Reconnect**: Bei Verbindungsabbruch mit exponential backoff +- **Ping/Pong Keepalive**: Verbindung wird automatisch überwacht +- **Command-Response-Pattern**: Backend kann Commands an Agents senden und Responses erhalten + +### 2. Remote Command Execution +- **exec Command**: Backend kann Befehle auf der OPNsense ausführen lassen +- **Timeout-Support**: Configurable timeouts für Command-Execution +- **stdout/stderr Output**: Vollständige Ausgabe wird zurückgeliefert + +### 3. TCP-Tunneling über WebSocket +- **tunnel_open**: Agent öffnet TCP-Verbindung zu lokalem Service (z.B. OPNsense WebUI :443) +- **Dynamische Proxy-Ports**: Backend allokiert automatisch freie Ports (10000-20000) +- **Binary WebSocket Messages**: Effiziente Datenübertragung ohne Base64-Overhead +- **Multi-Tunnel Support**: Mehrere gleichzeitige Tunnel pro Agent möglich + +### 4. HTTP-Proxy durch Tunnel +- **Reverse Proxy**: `GET /api/v1/agents/{id}/proxy/{tunnel_id}/*` leitet HTTP-Traffic durch Tunnel +- **Transparent**: Browser kann direkt auf OPNsense WebUI zugreifen über Backend-URL + ## Voraussetzungen - Go 1.22+ - make - OpenSSL (fuer manuelle Zertifikat-Generierung) - SSH-Zugang zu den Zielservern +- gorilla/websocket Library (automatisch über go mod) ## Build @@ -42,9 +71,13 @@ make all # Nur Backend (linux/amd64) make backend -# Nur Agent (freebsd/amd64) +# Nur Agent (freebsd/amd64) make agent +# Dependencies aktualisieren +cd agent && go mod tidy +cd backend && go mod tidy + # TLS-Zertifikate generieren (optional, Backend generiert automatisch) make certs ``` @@ -64,15 +97,10 @@ api_keys: - "01532e5a7c9e70bf2757df77a2f5b9b9" ``` -| Parameter | Env-Variable | Beschreibung | -|-------------|------------------|-------------------------------------| -| listen_addr | RMM_LISTEN_ADDR | Bind-Adresse (default: `:8443`) | -| tls_cert | RMM_TLS_CERT | Pfad zum TLS-Zertifikat | -| tls_key | RMM_TLS_KEY | Pfad zum TLS-Key | -| db_path | RMM_DB_PATH | Pfad zur SQLite-Datenbank | -| api_keys | - | Liste erlaubter API-Keys | - -Das Backend generiert automatisch self-signed TLS-Zertifikate wenn keine vorhanden sind. +**Neue WebSocket-Features:** +- WebSocket-Hub läuft automatisch +- Tunnel-Manager verwaltet aktive Tunnel +- Port-Range 10000-20000 für dynamische Proxy-Ports ### Agent (`agent/config.yaml`) @@ -84,13 +112,10 @@ agent_name: "opnsense-fw01" insecure: true ``` -| Parameter | Beschreibung | -|-----------------|-------------------------------------------------| -| backend_url | URL des Backend-Servers | -| api_key | API-Key fuer Authentifizierung | -| interval_seconds | Heartbeat-Intervall in Sekunden (default: 60) | -| agent_name | Name des Agents | -| insecure | TLS-Zertifikatpruefung ueberspringen | +**WebSocket wird automatisch gestartet:** +- WebSocket URL: `wss://192.168.85.13:8443/api/v1/agent/ws?agent_id=XXX&api_key=XXX` +- Reconnect mit Backoff bei Verbindungsabbruch +- Ping/Pong Keepalive alle 30s ## Deployment @@ -100,11 +125,11 @@ insecure: true make deploy-backend ``` -Dies: -1. Baut das Backend fuer linux/amd64 -2. Kopiert Binary + Config nach `/opt/rmm/` -3. Erstellt einen systemd-Service -4. Startet den Service +Dies startet: +1. REST API Server auf :8443 +2. WebSocket Hub für Agent-Verbindungen +3. Tunnel Manager für TCP-Proxies +4. SQLite-Datenbank für persistente Daten ### Agent (192.168.85.33 / OPNsense) @@ -112,211 +137,245 @@ Dies: make deploy-agent ``` -Dies: -1. Baut den Agent fuer freebsd/amd64 -2. Kopiert Binary + Config nach `/opt/rmm/` -3. Erstellt ein rc.d-Script -4. Aktiviert und startet den Service +Dies startet: +1. HTTP Heartbeat-Loop (weiterhin alle 60s) +2. WebSocket-Client mit automatischem Reconnect +3. Command-Handler für eingehende Befehle +4. Tunnel-Manager für TCP-Verbindungen ## API-Dokumentation -Alle Endpoints erfordern den Header `X-API-Key` mit einem gueltigen API-Key. +### WebSocket Commands (Backend → Agent) -### POST /api/v1/agent/register +#### exec - Befehl ausführen +```json +{ + "type": "command", + "id": "cmd-123", + "command": "exec", + "params": { + "command": "ps aux | head -10", + "timeout": 30 + } +} +``` -Agent registrieren. +**Response:** +```json +{ + "type": "response", + "id": "cmd-123", + "status": "ok", + "data": { + "output": "PID COMMAND\n1 kernel\n..." + } +} +``` + +#### tunnel_open - TCP-Tunnel öffnen +```json +{ + "type": "command", + "id": "cmd-124", + "command": "tunnel_open", + "params": { + "target_host": "127.0.0.1", + "target_port": 443 + } +} +``` + +**Response:** +```json +{ + "type": "response", + "id": "cmd-124", + "status": "ok", + "data": { + "tunnel_id": "a1b2c3d4e5f6789a" + } +} +``` + +#### tunnel_close - TCP-Tunnel schließen +```json +{ + "type": "command", + "id": "cmd-125", + "command": "tunnel_close", + "params": { + "tunnel_id": "a1b2c3d4e5f6789a" + } +} +``` + +### Neue REST API Endpoints + +#### POST /api/v1/agents/{id}/exec +Befehl auf Agent ausführen. **Request:** ```json { - "name": "opnsense-fw01", - "hostname": "fw01.local", - "ip": "192.168.85.33", - "opnsense_version": "OPNsense 24.1.1" + "command": "uptime", + "timeout": 30 +} +``` + +**Response (200):** +```json +{ + "message": "Command gesendet", + "command_id": "cmd-123" +} +``` + +#### POST /api/v1/agents/{id}/tunnel +TCP-Tunnel zu Agent öffnen. + +**Request:** +```json +{ + "target_host": "127.0.0.1", + "target_port": 443 } ``` **Response (201):** ```json { - "id": "a1b2c3d4e5f6...", - "message": "Agent erfolgreich registriert" + "tunnel_id": "a1b2c3d4e5f6789a", + "proxy_port": 12345, + "target_host": "127.0.0.1", + "target_port": 443, + "created_at": "2024-01-28T12:30:00Z" } ``` -### POST /api/v1/agent/heartbeat - -Systemdaten senden. - -**Request:** -```json -{ - "agent_id": "a1b2c3d4e5f6...", - "system_data": { - "hostname": "fw01.local", - "opnsense_version": "OPNsense 24.1.1", - "freebsd_version": "14.0-RELEASE-p5", - "uptime_seconds": 86400, - "hardware": { - "manufacturer": "Protectli", - "model": "VP2420", - "serial": "ABC123", - "bios_version": "1.0.0" - }, - "cpu": { - "model": "Intel(R) Celeron(R) J6412 @ 2.00GHz", - "cores": 4, - "threads": 4, - "freq_mhz": 2000, - "usage_percent": 12.5 - }, - "memory": { - "total_bytes": 8589934592, - "used_bytes": 2147483648, - "free_bytes": 6442450944 - }, - "disks": [ - { - "filesystem": "/dev/ada0p3", - "total_bytes": 107374182400, - "used_bytes": 5368709120, - "free_bytes": 93736247296, - "mount_point": "/" - } - ], - "network_interfaces": [ - { - "name": "igb0", - "role": "WAN", - "ip": "203.0.113.1", - "mac": "00:11:22:33:44:55", - "status": "up", - "rx_bytes": 1073741824, - "tx_bytes": 536870912 - }, - { - "name": "igb1", - "role": "LAN", - "ip": "192.168.1.1", - "mac": "00:11:22:33:44:56", - "status": "up", - "rx_bytes": 2147483648, - "tx_bytes": 1073741824 - } - ], - "services": [ - { - "name": "configd", - "description": "", - "status": "running" - }, - { - "name": "dhcpd", - "description": "", - "status": "running" - }, - { - "name": "unbound", - "description": "", - "status": "running" - } - ] - } -} -``` +#### DELETE /api/v1/agents/{id}/tunnel/{tunnel_id} +Tunnel schließen. **Response (200):** ```json { - "message": "OK" + "message": "Tunnel geschlossen" } ``` -### GET /api/v1/agents - -Alle registrierten Agents auflisten. +#### GET /api/v1/agents/{id}/tunnels +Aktive Tunnel auflisten. **Response (200):** ```json [ { - "id": "a1b2c3d4e5f6...", - "name": "opnsense-fw01", - "hostname": "fw01.local", - "ip": "192.168.85.33", - "opnsense_version": "OPNsense 24.1.1", - "registered_at": "2024-01-28T10:00:00Z", - "last_heartbeat": "2024-01-28T12:30:00Z" + "tunnel_id": "a1b2c3d4e5f6789a", + "proxy_port": 12345, + "target_host": "127.0.0.1", + "target_port": 443, + "active": true, + "created_at": "2024-01-28T12:30:00Z" } ] ``` -### GET /api/v1/agents/{id} +#### GET /api/v1/agents/{id}/proxy/{tunnel_id}/* +HTTP-Reverse-Proxy durch Tunnel. -Agent-Details mit letzten Systemdaten. +**Beispiel:** +``` +GET /api/v1/agents/abc123/proxy/tunnel789/ +→ Leitet weiter an OPNsense WebUI (127.0.0.1:443) über Tunnel +``` + +#### GET /api/v1/hub/status +WebSocket Hub Status. **Response (200):** ```json { - "agent": { - "id": "a1b2c3d4e5f6...", - "name": "opnsense-fw01", - "hostname": "fw01.local", - "ip": "192.168.85.33", - "opnsense_version": "OPNsense 24.1.1", - "registered_at": "2024-01-28T10:00:00Z", - "last_heartbeat": "2024-01-28T12:30:00Z" + "status": "running", + "connected_agents": ["abc123", "def456"], + "stats": { + "connected_agents": 2, + "active_tunnels": 3 }, - "system_data": { ... } + "timestamp": "2024-01-28T12:30:00Z" } ``` -### GET /api/v1/agents/{id}/system +### WebSocket Endpoint -Nur die aktuellen Systemdaten eines Agents. +#### GET /api/v1/agent/ws +WebSocket-Upgrade für Agents. -**Response (200):** -```json -{ - "hostname": "fw01.local", - "opnsense_version": "OPNsense 24.1.1", - "uptime_seconds": 86400, - "hardware": { ... }, - "cpu": { ... }, - "memory": { ... }, - "disks": [ ... ], - "network_interfaces": [ ... ], - "services": [ ... ] -} +**Query Parameters:** +- `agent_id`: Agent-ID (aus Registrierung) +- `api_key`: API-Key für Authentifizierung + +**Usage:** +``` +wss://192.168.85.13:8443/api/v1/agent/ws?agent_id=abc123&api_key=xxx ``` -### DELETE /api/v1/agents/{id} +## Tunnel-Datenformat -Agent entfernen. +### Binary WebSocket Messages +TCP-Tunnel-Daten werden als Binary Messages übertragen: -**Response (200):** -```json -{ - "message": "Agent geloescht" -} -``` +**Format:** `[tunnel_id_length:1][tunnel_id][data]` -### Fehlermeldungen +- Byte 0: Länge der Tunnel-ID (max 255) +- Bytes 1-N: Tunnel-ID als UTF-8 String +- Bytes N+1-Ende: TCP-Payload-Daten -```json -{ - "error": "unauthorized" -} -``` +### Beispiel-Usage -HTTP Status Codes: -- `200` - Erfolg -- `201` - Erstellt -- `400` - Ungueltige Anfrage -- `401` - Nicht autorisiert (fehlender/falscher API-Key) -- `404` - Nicht gefunden -- `500` - Server-Fehler +1. **Tunnel öffnen:** + ```bash + curl -X POST http://backend:8443/api/v1/agents/abc123/tunnel \ + -H "X-API-Key: xxx" \ + -d '{"target_host":"127.0.0.1","target_port":443}' + ``` + +2. **Über Tunnel auf OPNsense WebUI zugreifen:** + ``` + # Backend allokiert Port 12345 für Tunnel + # Browser kann jetzt zugreifen über: + https://192.168.85.13:12345/ + ``` + +3. **Oder über HTTP-Proxy:** + ``` + https://192.168.85.13:8443/api/v1/agents/abc123/proxy/tunnel789/ + ``` + +## Technische Details + +### CGO-frei +- Beide Komponenten kompilieren mit `CGO_ENABLED=0` +- Agent läuft auf FreeBSD/amd64 (OPNsense) +- Backend läuft auf Linux/amd64 +- gorilla/websocket ist pure Go, kein CGO erforderlich + +### Sicherheit +- TLS-verschlüsselte WebSocket-Verbindungen (WSS) +- API-Key-Authentifizierung für alle Endpoints +- Agent validiert Commands vor Ausführung +- Tunnel sind isoliert per Agent/Tunnel-ID + +### Performance +- Binary WebSocket Messages für Tunnel-Daten (kein Base64-Overhead) +- Connection-Pooling für gleichzeitige Tunnel +- Effiziente Port-Allokation für Proxies +- Heartbeat läuft weiterhin parallel (für Monitoring auch ohne WS) + +### Monitoring +- Agent funktioniert auch ohne WebSocket (nur Heartbeat) +- WebSocket-Verbindungsstatus wird überwacht +- Inactive Tunnel werden automatisch bereinigt +- Hub-Status zeigt Connected Agents und aktive Tunnel ## Lizenz -Intern. +Intern. \ No newline at end of file diff --git a/agent/go.mod b/agent/go.mod index b15359f..81924f2 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -4,4 +4,4 @@ go 1.22 require gopkg.in/yaml.v3 v3.0.1 -require github.com/gorilla/websocket v1.5.3 // indirect +require github.com/gorilla/websocket v1.5.3 diff --git a/agent/main.go b/agent/main.go index 8a7e552..a2751ed 100644 --- a/agent/main.go +++ b/agent/main.go @@ -12,6 +12,7 @@ import ( "github.com/cynfo/rmm-agent/client" "github.com/cynfo/rmm-agent/collector" "github.com/cynfo/rmm-agent/config" + "github.com/cynfo/rmm-agent/ws" ) func main() { @@ -79,6 +80,13 @@ func main() { log.Printf("Agent registriert mit ID: %s", agentID) + // WebSocket-Client starten + wsClient := ws.NewClient(cfg.BackendURL, agentID, cfg.APIKey, cfg.Insecure) + if err := wsClient.Start(); err != nil { + log.Printf("WebSocket-Client starten fehlgeschlagen: %v", err) + } + defer wsClient.Stop() + // Heartbeat-Loop ticker := time.NewTicker(time.Duration(cfg.IntervalSeconds) * time.Second) defer ticker.Stop() diff --git a/agent/ws/client.go b/agent/ws/client.go index 2b784ee..c0416e5 100644 --- a/agent/ws/client.go +++ b/agent/ws/client.go @@ -3,9 +3,9 @@ package ws import ( "context" "crypto/tls" + "encoding/json" "fmt" "log" - "net/http" "net/url" "sync" "time" @@ -218,12 +218,27 @@ func (c *Client) readMessages(conn *websocket.Conn, msgChan chan<- Message, errC defer close(msgChan) for { - var msg Message - if err := conn.ReadJSON(&msg); err != nil { + messageType, data, err := conn.ReadMessage() + if err != nil { errChan <- err return } + if messageType == websocket.BinaryMessage { + // Binary-Message für Tunnel-Daten verarbeiten + if err := c.tunnelManager.ProcessBinaryMessage(data); err != nil { + log.Printf("Binary-Message verarbeiten fehlgeschlagen: %v", err) + } + continue + } + + // JSON-Message parsen + var msg Message + if err := json.Unmarshal(data, &msg); err != nil { + log.Printf("JSON-Message parsen fehlgeschlagen: %v", err) + continue + } + select { case msgChan <- msg: case <-c.ctx.Done(): diff --git a/agent/ws/handler.go b/agent/ws/handler.go index fceb532..20f23f3 100644 --- a/agent/ws/handler.go +++ b/agent/ws/handler.go @@ -6,7 +6,6 @@ import ( "log" "os/exec" "strconv" - "strings" "time" ) diff --git a/backend/api/handlers.go b/backend/api/handlers.go index 2b18ba6..5e67e1e 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -11,6 +11,7 @@ import ( "github.com/cynfo/rmm-backend/db" "github.com/cynfo/rmm-backend/models" + "github.com/cynfo/rmm-backend/ws" ) type Handler struct { @@ -22,13 +23,17 @@ func NewHandler(database *db.Database) *Handler { } // SetupRoutes - Alle API-Routes registrieren -func (h *Handler) SetupRoutes(mux *http.ServeMux) { +func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) { + // Bestehende Agent-Routes mux.HandleFunc("POST /api/v1/agent/register", h.registerAgent) mux.HandleFunc("POST /api/v1/agent/heartbeat", h.heartbeat) mux.HandleFunc("GET /api/v1/agents", h.listAgents) mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent) mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData) mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent) + + // WebSocket und Tunnel-Routes + h.setupTunnelRoutes(mux, hub) } func generateID() string { diff --git a/backend/api/tunnel_proxy.go b/backend/api/tunnel_proxy.go new file mode 100644 index 0000000..0c8997d --- /dev/null +++ b/backend/api/tunnel_proxy.go @@ -0,0 +1,195 @@ +package api + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/cynfo/rmm-backend/ws" +) + +func (h *Handler) setupTunnelRoutes(mux *http.ServeMux, hub *ws.Hub) { + // Command-Endpoints + mux.HandleFunc("POST /api/v1/agents/{id}/exec", h.executeCommand(hub)) + mux.HandleFunc("POST /api/v1/agents/{id}/tunnel", h.openTunnel(hub)) + mux.HandleFunc("DELETE /api/v1/agents/{id}/tunnel/{tunnel_id}", h.closeTunnel(hub)) + mux.HandleFunc("GET /api/v1/agents/{id}/tunnels", h.listTunnels(hub)) + + // HTTP-Proxy durch Tunnel + mux.HandleFunc("GET /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub)) + mux.HandleFunc("POST /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub)) + mux.HandleFunc("PUT /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub)) + mux.HandleFunc("DELETE /api/v1/agents/{id}/proxy/{tunnel_id}/", h.httpProxy(hub)) + + // WebSocket-Endpoint + mux.HandleFunc("GET /api/v1/agent/ws", ws.NewWebSocketHandler(hub)) + + // Hub-Status + mux.HandleFunc("GET /api/v1/hub/status", h.hubStatus(hub)) +} + +func (h *Handler) executeCommand(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + agentID := r.PathValue("id") + + // Request parsen + var req struct { + Command string `json:"command"` + Timeout int `json:"timeout,omitempty"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "Ungültige Anfrage") + return + } + + if req.Command == "" { + writeError(w, http.StatusBadRequest, "Parameter 'command' erforderlich") + return + } + + if req.Timeout == 0 { + req.Timeout = 30 + } + + // Prüfen ob Agent verbunden ist + if !hub.IsAgentConnected(agentID) { + writeError(w, http.StatusServiceUnavailable, "Agent nicht verbunden") + return + } + + // Command-Message erstellen + cmd := map[string]interface{}{ + "type": "command", + "id": generateID(), + "command": "exec", + "params": map[string]interface{}{ + "command": req.Command, + "timeout": req.Timeout, + }, + } + + cmdJSON, _ := json.Marshal(cmd) + + // Command an Agent senden + if err := hub.SendToAgent(agentID, cmdJSON); err != nil { + log.Printf("Command senden fehlgeschlagen: %v", err) + writeError(w, http.StatusInternalServerError, "Command senden fehlgeschlagen") + return + } + + // Response zurückgeben + writeJSON(w, http.StatusOK, map[string]interface{}{ + "message": "Command gesendet", + "command_id": cmd["id"], + }) + } +} + +func (h *Handler) openTunnel(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + agentID := r.PathValue("id") + + // Request parsen + var req struct { + TargetHost string `json:"target_host"` + TargetPort int `json:"target_port"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "Ungültige Anfrage") + return + } + + if req.TargetHost == "" || req.TargetPort <= 0 { + writeError(w, http.StatusBadRequest, "target_host und target_port erforderlich") + return + } + + // Tunnel öffnen + tunnel, err := hub.GetTunnelManager().OpenTunnel(agentID, req.TargetHost, req.TargetPort) + if err != nil { + log.Printf("Tunnel öffnen fehlgeschlagen: %v", err) + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + + // Response + writeJSON(w, http.StatusCreated, map[string]interface{}{ + "tunnel_id": tunnel.ID, + "proxy_port": tunnel.ProxyPort, + "target_host": tunnel.TargetHost, + "target_port": tunnel.TargetPort, + "created_at": tunnel.CreatedAt, + }) + } +} + +func (h *Handler) closeTunnel(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tunnelID := r.PathValue("tunnel_id") + + // Tunnel schließen + if err := hub.GetTunnelManager().CloseTunnel(tunnelID); err != nil { + writeError(w, http.StatusNotFound, err.Error()) + return + } + + writeJSON(w, http.StatusOK, map[string]string{ + "message": "Tunnel geschlossen", + }) + } +} + +func (h *Handler) listTunnels(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + agentID := r.PathValue("id") + + // Tunnel für Agent abrufen + tunnels := hub.GetTunnelManager().GetAgentTunnels(agentID) + + // Response formatieren + result := make([]map[string]interface{}, len(tunnels)) + for i, tunnel := range tunnels { + result[i] = map[string]interface{}{ + "tunnel_id": tunnel.ID, + "proxy_port": tunnel.ProxyPort, + "target_host": tunnel.TargetHost, + "target_port": tunnel.TargetPort, + "active": tunnel.Active, + "created_at": tunnel.CreatedAt, + } + } + + writeJSON(w, http.StatusOK, result) + } +} + +func (h *Handler) httpProxy(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tunnelID := r.PathValue("tunnel_id") + + // HTTP-Proxy durch Tunnel + if err := hub.GetTunnelManager().CreateHTTPProxy(tunnelID, w, r); err != nil { + writeError(w, http.StatusNotFound, err.Error()) + return + } + } +} + +func (h *Handler) hubStatus(hub *ws.Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + stats := hub.GetAgentStats() + connectedAgents := hub.GetConnectedAgents() + + response := map[string]interface{}{ + "status": "running", + "connected_agents": connectedAgents, + "stats": stats, + "timestamp": time.Now(), + } + + writeJSON(w, http.StatusOK, response) + } +} \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index b8470bd..ae23baf 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,6 +3,7 @@ module github.com/cynfo/rmm-backend go 1.22 require ( + github.com/gorilla/websocket v1.5.3 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.29.1 ) @@ -10,7 +11,6 @@ require ( require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect diff --git a/backend/main.go b/backend/main.go index c651363..8107d42 100644 --- a/backend/main.go +++ b/backend/main.go @@ -19,6 +19,7 @@ import ( "github.com/cynfo/rmm-backend/api" "github.com/cynfo/rmm-backend/config" "github.com/cynfo/rmm-backend/db" + "github.com/cynfo/rmm-backend/ws" ) func main() { @@ -44,10 +45,14 @@ func main() { } defer database.Close() + // WebSocket Hub starten + hub := ws.NewHub() + go hub.Run() + // Router aufbauen mux := http.NewServeMux() handler := api.NewHandler(database) - handler.SetupRoutes(mux) + handler.SetupRoutes(mux, hub) // Middleware-Chain: Logging -> Auth -> Handler var chain http.Handler = mux diff --git a/backend/ws/handler.go b/backend/ws/handler.go new file mode 100644 index 0000000..1597ead --- /dev/null +++ b/backend/ws/handler.go @@ -0,0 +1,153 @@ +package ws + +import ( + "errors" + "log" + "net/http" + "time" + + "github.com/gorilla/websocket" +) + +var ( + ErrAgentNotConnected = errors.New("Agent nicht verbunden") + ErrAgentSendBlocked = errors.New("Agent Send-Channel blockiert") +) + +type Connection struct { + AgentID string + Conn *websocket.Conn + Send chan []byte + BinarySend chan []byte + Hub *Hub +} + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + // Hier könnte Origin-Checking implementiert werden + return true + }, +} + +func NewWebSocketHandler(hub *Hub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + handleWebSocket(hub, w, r) + } +} + +func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) { + // Query-Parameter extrahieren + agentID := r.URL.Query().Get("agent_id") + apiKey := r.URL.Query().Get("api_key") + + if agentID == "" || apiKey == "" { + http.Error(w, "agent_id und api_key erforderlich", http.StatusBadRequest) + return + } + + // API-Key validieren (sollte bereits durch Middleware geschehen) + // Hier könnten zusätzliche Agent-spezifische Prüfungen stattfinden + + // WebSocket-Upgrade + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("WebSocket-Upgrade fehlgeschlagen: %v", err) + return + } + + // Connection-Objekt erstellen + connection := &Connection{ + AgentID: agentID, + Conn: conn, + Send: make(chan []byte, 256), + BinarySend: make(chan []byte, 256), + Hub: hub, + } + + // Verbindung bei Hub registrieren + hub.RegisterAgent(agentID, connection) + + // Goroutines für Read/Write starten + go connection.writePump() + go connection.readPump() +} + +func (c *Connection) readPump() { + defer func() { + c.Hub.UnregisterAgent(c.AgentID, c) + c.Conn.Close() + }() + + // Read-Timeouts setzen + c.Conn.SetReadLimit(512 * 1024) // 512KB max message size + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.Conn.SetPongHandler(func(string) error { + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + return nil + }) + + for { + messageType, message, err := c.Conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("Agent %s: WebSocket-Fehler: %v", c.AgentID, err) + } + break + } + + // Message an Hub weiterleiten + if err := c.Hub.ProcessAgentMessage(c.AgentID, messageType, message); err != nil { + log.Printf("Agent %s: Message-Verarbeitung fehlgeschlagen: %v", c.AgentID, err) + } + } +} + +func (c *Connection) writePump() { + ticker := time.NewTicker(54 * time.Second) // Ping alle 54s + defer func() { + ticker.Stop() + c.Conn.Close() + }() + + for { + select { + case message, ok := <-c.Send: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if !ok { + c.Conn.WriteMessage(websocket.CloseMessage, nil) + return + } + + if err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil { + log.Printf("Agent %s: Write-Fehler: %v", c.AgentID, err) + return + } + + case binaryMessage, ok := <-c.BinarySend: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if !ok { + c.Conn.WriteMessage(websocket.CloseMessage, nil) + return + } + + if err := c.Conn.WriteMessage(websocket.BinaryMessage, binaryMessage); err != nil { + log.Printf("Agent %s: Binary-Write-Fehler: %v", c.AgentID, err) + return + } + + case <-ticker.C: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { + log.Printf("Agent %s: Ping-Fehler: %v", c.AgentID, err) + return + } + } + } +} + +func (c *Connection) Close() { + close(c.Send) + close(c.BinarySend) +} \ No newline at end of file diff --git a/backend/ws/hub.go b/backend/ws/hub.go new file mode 100644 index 0000000..22245a0 --- /dev/null +++ b/backend/ws/hub.go @@ -0,0 +1,238 @@ +package ws + +import ( + "log" + "sync" + "time" +) + +type AgentConnection struct { + AgentID string + Connection *Connection + LastSeen time.Time +} + +type Hub struct { + agents map[string]*AgentConnection + agentsMux sync.RWMutex + + register chan *AgentConnection + unregister chan *AgentConnection + broadcast chan []byte + + tunnelManager *TunnelManager +} + +func NewHub() *Hub { + hub := &Hub{ + agents: make(map[string]*AgentConnection), + register: make(chan *AgentConnection), + unregister: make(chan *AgentConnection), + broadcast: make(chan []byte, 256), + } + + hub.tunnelManager = NewTunnelManager(hub) + return hub +} + +func (h *Hub) Run() { + log.Println("WebSocket Hub startet...") + + // Cleanup-Timer für inaktive Verbindungen + cleanupTicker := time.NewTicker(5 * time.Minute) + defer cleanupTicker.Stop() + + for { + select { + case agent := <-h.register: + h.registerAgent(agent) + + case agent := <-h.unregister: + h.unregisterAgent(agent) + + case message := <-h.broadcast: + h.broadcastMessage(message) + + case <-cleanupTicker.C: + h.cleanupInactiveAgents() + } + } +} + +func (h *Hub) registerAgent(agent *AgentConnection) { + h.agentsMux.Lock() + defer h.agentsMux.Unlock() + + // Bestehende Verbindung schließen falls vorhanden + if existing, ok := h.agents[agent.AgentID]; ok { + log.Printf("Agent %s: Ersetzt bestehende Verbindung", agent.AgentID) + existing.Connection.Close() + } + + h.agents[agent.AgentID] = agent + agent.LastSeen = time.Now() + + log.Printf("Agent %s: WebSocket-Verbindung registriert", agent.AgentID) +} + +func (h *Hub) unregisterAgent(agent *AgentConnection) { + h.agentsMux.Lock() + defer h.agentsMux.Unlock() + + if existing, ok := h.agents[agent.AgentID]; ok && existing.Connection == agent.Connection { + delete(h.agents, agent.AgentID) + agent.Connection.Close() + log.Printf("Agent %s: WebSocket-Verbindung entfernt", agent.AgentID) + + // Alle Tunnel für diesen Agent schließen + h.tunnelManager.CloseAgentTunnels(agent.AgentID) + } +} + +func (h *Hub) broadcastMessage(message []byte) { + h.agentsMux.RLock() + defer h.agentsMux.RUnlock() + + for agentID, agent := range h.agents { + select { + case agent.Connection.Send <- message: + default: + // Agent kann keine Nachrichten mehr empfangen + log.Printf("Agent %s: Send-Channel blockiert, trenne Verbindung", agentID) + close(agent.Connection.Send) + delete(h.agents, agentID) + } + } +} + +func (h *Hub) SendToAgent(agentID string, message []byte) error { + h.agentsMux.RLock() + agent, exists := h.agents[agentID] + h.agentsMux.RUnlock() + + if !exists { + return ErrAgentNotConnected + } + + agent.LastSeen = time.Now() + + select { + case agent.Connection.Send <- message: + return nil + default: + return ErrAgentSendBlocked + } +} + +func (h *Hub) SendBinaryToAgent(agentID string, data []byte) error { + h.agentsMux.RLock() + agent, exists := h.agents[agentID] + h.agentsMux.RUnlock() + + if !exists { + return ErrAgentNotConnected + } + + agent.LastSeen = time.Now() + + select { + case agent.Connection.BinarySend <- data: + return nil + default: + return ErrAgentSendBlocked + } +} + +func (h *Hub) IsAgentConnected(agentID string) bool { + h.agentsMux.RLock() + defer h.agentsMux.RUnlock() + + _, exists := h.agents[agentID] + return exists +} + +func (h *Hub) GetConnectedAgents() []string { + h.agentsMux.RLock() + defer h.agentsMux.RUnlock() + + agents := make([]string, 0, len(h.agents)) + for agentID := range h.agents { + agents = append(agents, agentID) + } + + return agents +} + +func (h *Hub) GetAgentStats() map[string]interface{} { + h.agentsMux.RLock() + defer h.agentsMux.RUnlock() + + stats := map[string]interface{}{ + "connected_agents": len(h.agents), + "active_tunnels": h.tunnelManager.GetActiveTunnelCount(), + } + + return stats +} + +func (h *Hub) cleanupInactiveAgents() { + h.agentsMux.Lock() + defer h.agentsMux.Unlock() + + timeout := 10 * time.Minute + now := time.Now() + + for agentID, agent := range h.agents { + if now.Sub(agent.LastSeen) > timeout { + log.Printf("Agent %s: Timeout, trenne Verbindung", agentID) + agent.Connection.Close() + delete(h.agents, agentID) + h.tunnelManager.CloseAgentTunnels(agentID) + } + } +} + +// GetTunnelManager gibt den TunnelManager zurück (für API-Handler) +func (h *Hub) GetTunnelManager() *TunnelManager { + return h.tunnelManager +} + +// RegisterAgent registriert eine neue Agent-Verbindung +func (h *Hub) RegisterAgent(agentID string, conn *Connection) { + agent := &AgentConnection{ + AgentID: agentID, + Connection: conn, + LastSeen: time.Now(), + } + + h.register <- agent +} + +// UnregisterAgent entfernt eine Agent-Verbindung +func (h *Hub) UnregisterAgent(agentID string, conn *Connection) { + agent := &AgentConnection{ + AgentID: agentID, + Connection: conn, + } + + h.unregister <- agent +} + +// ProcessAgentMessage verarbeitet eingehende Nachrichten von Agents +func (h *Hub) ProcessAgentMessage(agentID string, messageType int, data []byte) error { + h.agentsMux.RLock() + agent, exists := h.agents[agentID] + h.agentsMux.RUnlock() + + if exists { + agent.LastSeen = time.Now() + } + + // Binary Messages sind normalerweise Tunnel-Daten + if messageType == 2 { // websocket.BinaryMessage + return h.tunnelManager.ProcessBinaryMessage(agentID, data) + } + + // Text Messages sind JSON-Commands/Responses/Events + return nil +} \ No newline at end of file diff --git a/backend/ws/tunnel.go b/backend/ws/tunnel.go new file mode 100644 index 0000000..f9c9a60 --- /dev/null +++ b/backend/ws/tunnel.go @@ -0,0 +1,336 @@ +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 +} + +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 + cmd := map[string]interface{}{ + "type": "command", + "id": tm.generateTunnelID(), + "command": "tunnel_open", + "params": map[string]interface{}{ + "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()) + + // Buffer für TCP-Data + buffer := make([]byte, 32768) + + for { + n, err := conn.Read(buffer) + if err != nil { + if err.Error() != "EOF" { + log.Printf("Tunnel %s: Proxy-Read-Fehler: %v", tunnel.ID, err) + } + break + } + + if n > 0 { + // Daten über 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 + } + } + } + + log.Printf("Tunnel %s: Proxy-Verbindung beendet", tunnel.ID) +} + +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) + } + + // Hier würden wir die Daten an aktive Proxy-Verbindungen weiterleiten + // Da das komplexer ist, implementieren wir erstmal nur das Logging + log.Printf("Tunnel %s: %d Bytes von Agent erhalten", tunnelID, len(payload)) + + // TODO: Implement actual forwarding to active proxy connections + // Das würde einen Connection-Pool pro Tunnel erfordern + + 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 +} \ No newline at end of file