From 68e712b3a3a2aab92a1121916f1b2d4dbc15f3db Mon Sep 17 00:00:00 2001 From: cynfo3000 Date: Sat, 28 Feb 2026 08:53:10 +0100 Subject: [PATCH] Meilenstein 2: Session-basierte TCP-Tunnel - Tunnel unterstuetzen mehrere gleichzeitige TCP-Verbindungen - Jede Proxy-Verbindung bekommt eigene Session-ID - Agent: Lazy-Connect (Ziel-Verbindung erst bei Datentransfer) - Backend: Ready-Handshake (wartet auf Agent-Bestaetigung) - Binary WebSocket Messages mit Session-ID statt Tunnel-ID - Connection.Close() Race-Condition behoben (sync.Once) - SendToAgent/SendBinaryToAgent mit Timeout statt Blocking - Hub verarbeitet Agent-Responses (Session-Confirm) - README komplett aktualisiert --- README.md | 505 ++++++++++++++++-------------------------- agent/ws/handler.go | 172 +++++--------- agent/ws/tunnel.go | 224 ++++++++----------- backend/ws/handler.go | 12 +- backend/ws/hub.go | 28 ++- backend/ws/tunnel.go | 388 +++++++++++++++++++------------- 6 files changed, 595 insertions(+), 734 deletions(-) diff --git a/README.md b/README.md index b4d667d..bfbd54b 100644 --- a/README.md +++ b/README.md @@ -1,381 +1,250 @@ # OPNsense RMM System -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 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 +Remote Monitoring & Management fuer OPNsense Firewalls. +Go-basierter Agent + Backend mit WebSocket-Kommunikation und Session-basierten TCP-Tunneln. ## Architektur ``` -┌──────────────────────┐ 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) +┌────────────────┐ HTTPS/WSS ┌────────────────┐ TCP Proxy ┌────────────┐ +│ OPNsense FW │◄──────────────►│ RMM Backend │◄───────────────│ Browser/ │ +│ (FreeBSD) │ :8443 │ (Linux) │ :10000-20000 │ SSH/etc. │ +│ │ │ │ └────────────┘ +│ rmm-agent │ Heartbeat │ REST API │ +│ - Collectors │ WebSocket │ WebSocket Hub │ +│ - WS Client │ Binary Msgs │ Tunnel Mgr │ +│ - Tunnel Mgr │ │ SQLite DB │ +└────────────────┘ └────────────────┘ ``` -## Neue Funktionen (WebSocket/Tunnel) +### Tunnel-Flow -### 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 +``` +Client ──► Backend:ProxyPort ──► WebSocket (Binary) ──► Agent ──► Zieldienst + (TCP-Listener) (Session-basiert) (TCP) (SSH/HTTPS/etc) +``` -### 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 +- Jeder Tunnel oeffnet einen dynamischen Proxy-Port (10000-20000) auf dem Backend +- Jede TCP-Verbindung am Proxy bekommt eine eigene **Session** +- Der Agent oeffnet pro Session eine separate Ziel-Verbindung (Lazy Connect) +- Mehrere gleichzeitige Verbindungen pro Tunnel moeglich +- Tunnel bleiben offen wenn einzelne Sessions beendet werden -### 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 +## Komponenten -### 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 +### Agent (FreeBSD/OPNsense) +- **Collectors**: Hardware, CPU, Memory, Disk, Network, Services, WireGuard, DHCP (KEA/ISC/dnsmasq), Routing, Gateways, Zertifikate, Plugins, Updates +- **WebSocket Client**: Persistente Verbindung zum Backend, automatisches Reconnect mit Backoff +- **Command Handler**: Remote-Befehlsausfuehrung, Tunnel-Connect/Disconnect +- **Tunnel Manager**: Session-basierte TCP-Verbindungen zu lokalen Diensten -## Voraussetzungen - -- Go 1.22+ -- make -- OpenSSL (fuer manuelle Zertifikat-Generierung) -- SSH-Zugang zu den Zielservern -- gorilla/websocket Library (automatisch über go mod) +### Backend (Linux) +- **REST API**: Agent-Registrierung, Heartbeat, Systemdaten, Tunnel-Management +- **WebSocket Hub**: Verwaltet Agent-Verbindungen, routet Commands und Binary-Messages +- **Tunnel Manager**: Proxy-Listener, Session-Tracking, Ready-Handshake +- **SQLite DB**: Persistente Agent- und Systemdaten ## Build ```bash -# Beide Komponenten bauen -make all - -# Nur Backend (linux/amd64) -make backend - -# 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 +make all # Backend (linux/amd64) + Agent (freebsd/amd64) +make backend # Nur Backend +make agent # Nur Agent ``` -Die Binaries landen in `build/`. +Binaries in `build/`. Benoetigt Go 1.22+. ## Konfiguration -### Backend (`backend/config.yaml`) +### Backend (`config.yaml`) ```yaml listen_addr: ":8443" -tls_cert: "certs/server.crt" +tls_cert: "certs/server.crt" # Wird automatisch generiert tls_key: "certs/server.key" db_path: "rmm.db" api_keys: - - "01532e5a7c9e70bf2757df77a2f5b9b9" + - "dein-api-key-hier" ``` -**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`) +### Agent (`config.yaml`) ```yaml -backend_url: "https://192.168.85.13:8443" -api_key: "01532e5a7c9e70bf2757df77a2f5b9b9" +backend_url: "https://backend-ip:8443" +api_key: "dein-api-key-hier" interval_seconds: 60 agent_name: "opnsense-fw01" -insecure: true +insecure: true # Self-signed Certs akzeptieren ``` -**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 -### Backend (192.168.85.13) +### Backend ```bash -make deploy-backend +# Build + Copy +make backend +scp build/rmm-backend user@backend:/pfad/rmm-backend + +# Starten (als normaler User, kein root noetig) +cd /pfad && nohup ./rmm-backend config.yaml > rmm-backend.log 2>&1 & ``` -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) +### Agent (OPNsense/FreeBSD) ```bash -make deploy-agent +# Build + Copy +make agent +scp build/rmm-agent root@opnsense:/usr/local/rmm/rmm-agent + +# Starten via FreeBSD daemon +daemon -f -p /var/run/rmm-agent.pid -o /usr/local/rmm/rmm-agent.log \ + /usr/local/rmm/rmm-agent --config /usr/local/rmm/config.yaml --insecure ``` -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 +**Wichtig**: Agent-ID wird persistent in `/usr/local/rmm/agent_id` gespeichert. -## API-Dokumentation +## API + +### Agent-Management + +| Methode | Endpoint | Beschreibung | +|---------|----------|-------------| +| POST | `/api/v1/agent/register` | Agent registrieren (mit optionaler `agent_id`) | +| POST | `/api/v1/agent/heartbeat` | Systemdaten senden | +| GET | `/api/v1/agents` | Alle Agents auflisten | +| GET | `/api/v1/agents/{id}` | Agent + Systemdaten abrufen | +| GET | `/api/v1/agents/{id}/system` | Nur Systemdaten | +| DELETE | `/api/v1/agents/{id}` | Agent loeschen | + +### WebSocket + +| Methode | Endpoint | Beschreibung | +|---------|----------|-------------| +| GET | `/api/v1/agent/ws?agent_id=X&api_key=X` | WebSocket-Verbindung (Agent) | +| GET | `/api/v1/hub/status` | Hub-Status (connected agents, aktive Tunnel) | + +### Tunnel-Management + +| Methode | Endpoint | Beschreibung | +|---------|----------|-------------| +| POST | `/api/v1/agents/{id}/tunnel` | Tunnel oeffnen | +| GET | `/api/v1/agents/{id}/tunnels` | Aktive Tunnel auflisten | +| DELETE | `/api/v1/agents/{id}/tunnel/{tid}` | Tunnel schliessen | +| POST | `/api/v1/agents/{id}/exec` | Remote-Command ausfuehren | + +### Tunnel erstellen + +```bash +# SSH-Tunnel zur OPNsense +curl -sk -H "X-API-Key: KEY" -X POST \ + https://backend:8443/api/v1/agents/AGENT_ID/tunnel \ + -d '{"target_host":"127.0.0.1","target_port":22}' + +# Response: {"tunnel_id":"xxx","proxy_port":10000,...} + +# Verbinden: +ssh -p 10000 root@backend-ip +``` + +```bash +# WebGUI-Tunnel (OPNsense auf Port 4444) +curl -sk -H "X-API-Key: KEY" -X POST \ + https://backend:8443/api/v1/agents/AGENT_ID/tunnel \ + -d '{"target_host":"127.0.0.1","target_port":4444}' + +# Response: {"tunnel_id":"xxx","proxy_port":10001,...} + +# Im Browser: https://backend-ip:10001/ +``` ### WebSocket Commands (Backend → Agent) -#### exec - Befehl ausführen ```json -{ - "type": "command", - "id": "cmd-123", - "command": "exec", - "params": { - "command": "ps aux | head -10", - "timeout": 30 - } -} +// Remote-Befehl ausfuehren +{"type":"command","id":"cmd1","command":"exec","params":{"command":"uptime","timeout":30}} + +// Tunnel-Session oeffnen (wird automatisch vom Backend gesendet) +{"type":"command","id":"cmd2","command":"tunnel_connect","params":{"session_id":"xxx","tunnel_id":"xxx","target_host":"127.0.0.1","target_port":22}} + +// Tunnel-Session schliessen +{"type":"command","id":"cmd3","command":"tunnel_disconnect","params":{"session_id":"xxx"}} ``` -**Response:** -```json -{ - "type": "response", - "id": "cmd-123", - "status": "ok", - "data": { - "output": "PID COMMAND\n1 kernel\n..." - } -} +### Binary WebSocket Messages (Tunnel-Daten) + +Format: `[session_id_length:1][session_id][tcp_payload]` + +Tunnel-Daten werden als Binary WebSocket Messages uebertragen (kein Base64-Overhead). + +## Dateistruktur + ``` - -#### tunnel_open - TCP-Tunnel öffnen -```json -{ - "type": "command", - "id": "cmd-124", - "command": "tunnel_open", - "params": { - "target_host": "127.0.0.1", - "target_port": 443 - } -} +rmm/ +├── Makefile +├── README.md +├── agent/ +│ ├── main.go # Entry, Registration, Heartbeat-Loop, WS-Start +│ ├── config/ +│ │ └── config.go # YAML Config Loader +│ ├── client/ +│ │ └── client.go # HTTP Client (Register, Heartbeat) +│ ├── collector/ +│ │ ├── hardware.go # Hersteller, Modell, BIOS +│ │ ├── cpu.go # CPU-Modell, Cores, Usage +│ │ ├── memory.go # RAM total/used/free +│ │ ├── disk.go # ZFS Datasets, Belegung +│ │ ├── network.go # Interfaces, IPs, Status +│ │ ├── services.go # Laufende Dienste +│ │ ├── wireguard.go # WG-Tunnel, Peers, Transfer +│ │ ├── dhcp.go # KEA/ISC/dnsmasq Leases +│ │ ├── opnsense.go # OPN-Version, FreeBSD, Hostname, Uptime +│ │ ├── routing.go # Routing-Tabelle (netstat -rn) +│ │ ├── gateways.go # Gateway-Status (configctl) +│ │ ├── certificates.go # Zertifikate + CAs (config.xml + PEM) +│ │ ├── plugins.go # Installierte Plugins (pkg info) +│ │ └── updates.go # Pending Updates (core + packages) +│ └── ws/ +│ ├── client.go # WebSocket Client, Reconnect, Ping/Pong +│ ├── handler.go # Command Handler (exec, tunnel_connect/disconnect) +│ └── tunnel.go # Session-basierter Tunnel Manager +├── backend/ +│ ├── main.go # Entry, TLS, HTTP Server +│ ├── config/ +│ │ └── config.go # YAML Config Loader +│ ├── db/ +│ │ └── sqlite.go # SQLite (Agent + Systemdaten) +│ ├── models/ +│ │ ├── agent.go # Agent, Register/Heartbeat Structs +│ │ └── system.go # SystemData, alle Collector-Structs +│ ├── api/ +│ │ ├── handlers.go # REST API Handler +│ │ ├── tunnel_proxy.go # Tunnel + Exec REST Endpoints +│ │ └── middleware.go # API-Key Auth, Logging +│ └── ws/ +│ ├── handler.go # WebSocket Upgrade, Connection R/W Pumps +│ ├── hub.go # Agent-Registry, Message Routing +│ └── tunnel.go # Session-basierter Tunnel + Proxy Manager +└── build/ # Kompilierte Binaries ``` -**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 -{ - "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 -{ - "tunnel_id": "a1b2c3d4e5f6789a", - "proxy_port": 12345, - "target_host": "127.0.0.1", - "target_port": 443, - "created_at": "2024-01-28T12:30:00Z" -} -``` - -#### DELETE /api/v1/agents/{id}/tunnel/{tunnel_id} -Tunnel schließen. - -**Response (200):** -```json -{ - "message": "Tunnel geschlossen" -} -``` - -#### GET /api/v1/agents/{id}/tunnels -Aktive Tunnel auflisten. - -**Response (200):** -```json -[ - { - "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}/proxy/{tunnel_id}/* -HTTP-Reverse-Proxy durch Tunnel. - -**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 -{ - "status": "running", - "connected_agents": ["abc123", "def456"], - "stats": { - "connected_agents": 2, - "active_tunnels": 3 - }, - "timestamp": "2024-01-28T12:30:00Z" -} -``` - -### WebSocket Endpoint - -#### GET /api/v1/agent/ws -WebSocket-Upgrade für Agents. - -**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 -``` - -## Tunnel-Datenformat - -### Binary WebSocket Messages -TCP-Tunnel-Daten werden als Binary Messages übertragen: - -**Format:** `[tunnel_id_length:1][tunnel_id][data]` - -- 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 - -### Beispiel-Usage - -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 +- **CGO-frei**: `CGO_ENABLED=0`, pure-Go SQLite (modernc.org/sqlite) +- **Cross-Compilation**: Backend linux/amd64, Agent freebsd/amd64 +- **TLS**: Self-signed Zertifikate (automatisch generiert) +- **WebSocket**: gorilla/websocket, Binary Messages fuer Tunnel-Daten +- **Persistente Agent-ID**: Ueberlebt Agent- und Backend-Neustarts +- **FreeBSD-kompatibel**: Volle Pfade (/usr/bin/netstat etc.), daemon statt nohup -### 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 +## Hinweise -### 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 +- Backend braucht **kein root** — laeuft als normaler User auf Port 8443 +- Agent braucht **root** auf OPNsense (fuer Hardware-/Service-Daten) +- OPNsense WebGUI typischerweise auf **Port 4444** (nicht 443) +- FreeBSD csh hat kein `$()` — Agent verwendet `/bin/sh -c` fuer Commands +- Beim Backend-Deploy: **alte Prozesse killen** bevor neuer startet (sonst "bind: address already in use") ## Lizenz -Intern. \ No newline at end of file +Intern. diff --git a/agent/ws/handler.go b/agent/ws/handler.go index 29dd8f5..7c415b8 100644 --- a/agent/ws/handler.go +++ b/agent/ws/handler.go @@ -19,27 +19,31 @@ func NewHandler(client *Client) *Handler { func (h *Handler) HandleCommand(msg Message) { log.Printf("Command erhalten: %s (ID: %s)", msg.Command, msg.ID) - + var response Message response.Type = "response" response.ID = msg.ID - + switch msg.Command { case "exec": response = h.handleExec(msg) case "tunnel_open": - response = h.handleTunnelOpen(msg) + // Legacy: wird nicht mehr verwendet, aber fuer Kompatibilitaet + response.Status = "ok" + response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"} + case "tunnel_connect": + response = h.handleTunnelConnect(msg) + case "tunnel_disconnect": + h.handleTunnelDisconnect(msg) + return // Keine Response noetig case "tunnel_close": - response = h.handleTunnelClose(msg) - case "tunnel_data": - // Tunnel-Daten werden als Binary-Messages gesendet, nicht als JSON - h.handleTunnelData(msg) - return + // Legacy: alle Sessions fuer diesen Tunnel schliessen + response.Status = "ok" default: response.Status = "error" response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command) } - + if err := h.client.SendMessage(response); err != nil { log.Printf("Response senden fehlgeschlagen: %v", err) } @@ -50,16 +54,15 @@ func (h *Handler) handleExec(msg Message) Message { Type: "response", ID: msg.ID, } - - // Parameter extrahieren + command, ok := msg.Params["command"].(string) if !ok || command == "" { response.Status = "error" response.Error = "Parameter 'command' erforderlich" return response } - - timeoutSec := 30 // Default + + timeoutSec := 30 if t, ok := msg.Params["timeout"]; ok { if ts, ok := t.(float64); ok { timeoutSec = int(ts) @@ -69,15 +72,13 @@ func (h *Handler) handleExec(msg Message) Message { } } } - - // Command ausführen mit Timeout + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second) defer cancel() - - // FreeBSD/OPNsense nutzt /bin/sh + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command) output, err := cmd.CombinedOutput() - + if err != nil { response.Status = "error" response.Error = err.Error() @@ -90,127 +91,62 @@ func (h *Handler) handleExec(msg Message) Message { "output": string(output), } } - + return response } -func (h *Handler) handleTunnelOpen(msg Message) Message { +func (h *Handler) handleTunnelConnect(msg Message) Message { response := Message{ Type: "response", ID: msg.ID, } - - // Parameter extrahieren - targetHost, ok := msg.Params["target_host"].(string) - if !ok || targetHost == "" { - response.Status = "error" - response.Error = "Parameter 'target_host' erforderlich" - return response - } - - targetPortFloat, ok := msg.Params["target_port"].(float64) - if !ok { - response.Status = "error" - response.Error = "Parameter 'target_port' erforderlich" - return response - } - targetPort := int(targetPortFloat) - - // Tunnel-ID vom Backend uebernehmen (falls vorhanden) - backendTunnelID, _ := msg.Params["tunnel_id"].(string) - // Tunnel oeffnen - tunnelID, err := h.client.tunnelManager.OpenTunnel(targetHost, targetPort, backendTunnelID) - if err != nil { + sessionID, _ := msg.Params["session_id"].(string) + tunnelID, _ := msg.Params["tunnel_id"].(string) + targetHost, _ := msg.Params["target_host"].(string) + targetPortFloat, _ := msg.Params["target_port"].(float64) + targetPort := int(targetPortFloat) + + if sessionID == "" || targetHost == "" || targetPort <= 0 { response.Status = "error" - response.Error = fmt.Sprintf("Tunnel öffnen fehlgeschlagen: %v", err) + response.Error = "session_id, target_host und target_port erforderlich" return response } - + + if err := h.client.tunnelManager.ConnectSession(sessionID, tunnelID, targetHost, targetPort); err != nil { + response.Status = "error" + response.Error = fmt.Sprintf("Session-Connect fehlgeschlagen: %v", err) + return response + } + response.Status = "ok" response.Data = map[string]interface{}{ - "tunnel_id": tunnelID, + "session_id": sessionID, } - - log.Printf("Tunnel geöffnet: %s -> %s:%d", tunnelID, targetHost, targetPort) + return response } -func (h *Handler) handleTunnelClose(msg Message) Message { - response := Message{ - Type: "response", - ID: msg.ID, - } - - // Tunnel-ID extrahieren - tunnelID, ok := msg.Params["tunnel_id"].(string) - if !ok || tunnelID == "" { - response.Status = "error" - response.Error = "Parameter 'tunnel_id' erforderlich" - return response - } - - // Tunnel schließen - if err := h.client.tunnelManager.CloseTunnel(tunnelID); err != nil { - response.Status = "error" - response.Error = fmt.Sprintf("Tunnel schließen fehlgeschlagen: %v", err) - return response - } - - response.Status = "ok" - log.Printf("Tunnel geschlossen: %s", tunnelID) - return response -} - -func (h *Handler) handleTunnelData(msg Message) { - // Diese Funktion wird für JSON-basierte Tunnel-Daten verwendet - // In der Praxis verwenden wir Binary WebSocket Messages für bessere Performance - tunnelID, ok := msg.Params["tunnel_id"].(string) - if !ok { - log.Printf("Tunnel-Daten ohne tunnel_id erhalten") +func (h *Handler) handleTunnelDisconnect(msg Message) { + sessionID, _ := msg.Params["session_id"].(string) + if sessionID == "" { return } - - dataStr, ok := msg.Params["data"].(string) - if !ok { - log.Printf("Tunnel-Daten ohne data erhalten") - return - } - - // Base64-Dekodierung würde hier stattfinden - // Aber wir verwenden Binary Messages, daher ist das nur ein Fallback - log.Printf("Tunnel-Daten via JSON erhalten für %s (Länge: %d)", tunnelID, len(dataStr)) + + h.client.tunnelManager.DisconnectSession(sessionID) } -// SendTunnelData sendet Tunnel-Daten als Binary-Message zurück zum Backend -func (h *Handler) SendTunnelData(tunnelID string, data []byte) error { - // Format: [tunnel_id_length:1][tunnel_id][data] - tunnelIDBytes := []byte(tunnelID) - if len(tunnelIDBytes) > 255 { - return fmt.Errorf("tunnel_id zu lang") +// SendSessionData schickt Daten als Binary-Message zurueck zum Backend +func (h *Handler) SendSessionData(sessionID string, data []byte) error { + idBytes := []byte(sessionID) + if len(idBytes) > 255 { + return fmt.Errorf("session_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) - + + message := make([]byte, 1+len(idBytes)+len(data)) + message[0] = byte(len(idBytes)) + copy(message[1:], idBytes) + copy(message[1+len(idBytes):], data) + return h.client.SendBinaryData(message) } - -// ParseTunnelData parst eingehende Binary-Messages mit Tunnel-Daten -func ParseTunnelData(data []byte) (tunnelID string, payload []byte, err error) { - if len(data) < 1 { - return "", nil, fmt.Errorf("Daten zu kurz") - } - - tunnelIDLen := int(data[0]) - if len(data) < 1+tunnelIDLen { - return "", nil, fmt.Errorf("Tunnel-ID zu kurz") - } - - tunnelID = string(data[1 : 1+tunnelIDLen]) - payload = data[1+tunnelIDLen:] - - return tunnelID, payload, nil -} \ No newline at end of file diff --git a/agent/ws/tunnel.go b/agent/ws/tunnel.go index f321e86..a4219f1 100644 --- a/agent/ws/tunnel.go +++ b/agent/ws/tunnel.go @@ -11,202 +11,168 @@ import ( "time" ) -type Tunnel struct { +// TunnelSession repraesentiert eine einzelne TCP-Verbindung zu einem Ziel +type TunnelSession struct { ID string + TunnelID string TargetHost string TargetPort int Conn net.Conn Active bool - CreatedAt time.Time } +// TunnelManager verwaltet alle Sessions auf Agent-Seite type TunnelManager struct { - client *Client - tunnels map[string]*Tunnel - mutex sync.RWMutex + client *Client + sessions map[string]*TunnelSession + mutex sync.RWMutex } func NewTunnelManager(client *Client) *TunnelManager { return &TunnelManager{ - client: client, - tunnels: make(map[string]*Tunnel), + client: client, + sessions: make(map[string]*TunnelSession), } } -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 +// ConnectSession oeffnet eine neue TCP-Verbindung fuer eine Session +func (tm *TunnelManager) ConnectSession(sessionID, tunnelID, targetHost string, targetPort int) error { 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) + return fmt.Errorf("Verbindung zu %s fehlgeschlagen: %w", target, err) } - - // Tunnel erstellen - tunnel := &Tunnel{ - ID: id, + + session := &TunnelSession{ + ID: sessionID, + TunnelID: tunnelID, TargetHost: targetHost, TargetPort: targetPort, Conn: conn, Active: true, - CreatedAt: time.Now(), } - - // Tunnel registrieren + tm.mutex.Lock() - tm.tunnels[id] = tunnel + tm.sessions[sessionID] = session tm.mutex.Unlock() - - // Data-Forwarding starten - go tm.forwardData(tunnel) - - return id, nil + + // Ziel -> Backend Forwarding starten + go tm.forwardFromTarget(session) + + log.Printf("Session %s: Verbindung zu %s hergestellt", sessionID, target) + return nil } -func (tm *TunnelManager) CloseTunnel(tunnelID string) error { +// DisconnectSession schliesst eine Session +func (tm *TunnelManager) DisconnectSession(sessionID string) { tm.mutex.Lock() - tunnel, exists := tm.tunnels[tunnelID] + session, exists := tm.sessions[sessionID] if !exists { tm.mutex.Unlock() - return fmt.Errorf("Tunnel %s nicht gefunden", tunnelID) + return } - - tunnel.Active = false - delete(tm.tunnels, tunnelID) + + session.Active = false + delete(tm.sessions, sessionID) tm.mutex.Unlock() - - // Verbindung schließen - if tunnel.Conn != nil { - tunnel.Conn.Close() + + if session.Conn != nil { + session.Conn.Close() } - - return nil + + log.Printf("Session %s: geschlossen", sessionID) } -func (tm *TunnelManager) GetActiveTunnels() []string { +// SendDataToSession schreibt Daten in die Ziel-Verbindung einer Session +func (tm *TunnelManager) SendDataToSession(sessionID string, data []byte) error { 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] + session, exists := tm.sessions[sessionID] tm.mutex.RUnlock() - - if !exists || !tunnel.Active { - return fmt.Errorf("Tunnel %s nicht aktiv", tunnelID) + + if !exists || !session.Active { + return fmt.Errorf("Session %s nicht aktiv", sessionID) } - - if tunnel.Conn == nil { - return fmt.Errorf("Tunnel %s hat keine Verbindung", tunnelID) - } - - _, err := tunnel.Conn.Write(data) + + _, err := session.Conn.Write(data) if err != nil { - // Bei Fehler Tunnel schließen - tm.CloseTunnel(tunnelID) - return fmt.Errorf("Daten schreiben fehlgeschlagen: %w", err) + tm.DisconnectSession(sessionID) + return fmt.Errorf("Session %s: Write fehlgeschlagen: %w", sessionID, err) } - + return nil } -func (tm *TunnelManager) forwardData(tunnel *Tunnel) { +// forwardFromTarget liest Daten vom Ziel und schickt sie als Binary-Message ans Backend +func (tm *TunnelManager) forwardFromTarget(session *TunnelSession) { defer func() { - if tunnel.Active { - tm.CloseTunnel(tunnel.ID) + if session.Active { + tm.DisconnectSession(session.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) + log.Printf("Session %s: Forwarding beendet", session.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) + + buffer := make([]byte, 32768) + + for session.Active { + session.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + + n, err := session.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) + log.Printf("Session %s: Ziel-Verbindung geschlossen", session.ID) + } else if session.Active { + log.Printf("Session %s: Read-Fehler: %v", session.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) + if err := tm.client.messageHandler.SendSessionData(session.ID, buffer[:n]); err != nil { + log.Printf("Session %s: WebSocket senden fehlgeschlagen: %v", session.ID, err) break } } } } -func (tm *TunnelManager) generateTunnelID() string { +// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Backend (Proxy -> Ziel) +func (tm *TunnelManager) ProcessBinaryMessage(data []byte) error { + sessionID, payload, err := ParseBinaryMessage(data) + if err != nil { + return fmt.Errorf("Binary-Message parsen: %w", err) + } + + return tm.SendDataToSession(sessionID, payload) +} + +// GetActiveSessions gibt die Anzahl aktiver Sessions zurueck +func (tm *TunnelManager) GetActiveSessions() int { + tm.mutex.RLock() + defer tm.mutex.RUnlock() + return len(tm.sessions) +} + +func (tm *TunnelManager) generateID() 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) +// ParseBinaryMessage parst das Binary-Format: [id_len:1][id][data] +func ParseBinaryMessage(data []byte) (id string, payload []byte, err error) { + if len(data) < 1 { + return "", nil, fmt.Errorf("Daten zu kurz") } - - 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) - } + idLen := int(data[0]) + if len(data) < 1+idLen { + return "", nil, fmt.Errorf("ID zu kurz") } -} \ No newline at end of file + + id = string(data[1 : 1+idLen]) + payload = data[1+idLen:] + return id, payload, nil +} diff --git a/backend/ws/handler.go b/backend/ws/handler.go index 1597ead..49e9e45 100644 --- a/backend/ws/handler.go +++ b/backend/ws/handler.go @@ -4,6 +4,7 @@ import ( "errors" "log" "net/http" + "sync" "time" "github.com/gorilla/websocket" @@ -20,6 +21,7 @@ type Connection struct { Send chan []byte BinarySend chan []byte Hub *Hub + closeOnce sync.Once } var upgrader = websocket.Upgrader{ @@ -77,7 +79,6 @@ func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) { func (c *Connection) readPump() { defer func() { c.Hub.UnregisterAgent(c.AgentID, c) - c.Conn.Close() }() // Read-Timeouts setzen @@ -108,7 +109,7 @@ func (c *Connection) writePump() { ticker := time.NewTicker(54 * time.Second) // Ping alle 54s defer func() { ticker.Stop() - c.Conn.Close() + c.Close() }() for { @@ -148,6 +149,9 @@ func (c *Connection) writePump() { } func (c *Connection) Close() { - close(c.Send) - close(c.BinarySend) + c.closeOnce.Do(func() { + close(c.Send) + close(c.BinarySend) + c.Conn.Close() + }) } \ No newline at end of file diff --git a/backend/ws/hub.go b/backend/ws/hub.go index 22245a0..67b9c7a 100644 --- a/backend/ws/hub.go +++ b/backend/ws/hub.go @@ -1,6 +1,7 @@ package ws import ( + "encoding/json" "log" "sync" "time" @@ -81,7 +82,7 @@ func (h *Hub) unregisterAgent(agent *AgentConnection) { if existing, ok := h.agents[agent.AgentID]; ok && existing.Connection == agent.Connection { delete(h.agents, agent.AgentID) - agent.Connection.Close() + existing.Connection.Close() log.Printf("Agent %s: WebSocket-Verbindung entfernt", agent.AgentID) // Alle Tunnel für diesen Agent schließen @@ -119,7 +120,7 @@ func (h *Hub) SendToAgent(agentID string, message []byte) error { select { case agent.Connection.Send <- message: return nil - default: + case <-time.After(5 * time.Second): return ErrAgentSendBlocked } } @@ -138,7 +139,7 @@ func (h *Hub) SendBinaryToAgent(agentID string, data []byte) error { select { case agent.Connection.BinarySend <- data: return nil - default: + case <-time.After(5 * time.Second): return ErrAgentSendBlocked } } @@ -185,8 +186,8 @@ func (h *Hub) cleanupInactiveAgents() { 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) + agent.Connection.Close() h.tunnelManager.CloseAgentTunnels(agentID) } } @@ -233,6 +234,23 @@ func (h *Hub) ProcessAgentMessage(agentID string, messageType int, data []byte) return h.tunnelManager.ProcessBinaryMessage(agentID, data) } - // Text Messages sind JSON-Commands/Responses/Events + // Text Messages sind JSON-Responses/Events vom Agent + var msg struct { + Type string `json:"type"` + ID string `json:"id"` + Status string `json:"status"` + Data map[string]interface{} `json:"data"` + } + if err := json.Unmarshal(data, &msg); err != nil { + return nil // Ignorieren wenn nicht parsebar + } + + // Tunnel-Connect Responses verarbeiten + if msg.Type == "response" && msg.Status == "ok" && msg.Data != nil { + if sessionID, ok := msg.Data["session_id"].(string); ok { + h.tunnelManager.ConfirmSession(sessionID) + } + } + return nil } \ No newline at end of file diff --git a/backend/ws/tunnel.go b/backend/ws/tunnel.go index 4ec9ac0..c24b49b 100644 --- a/backend/ws/tunnel.go +++ b/backend/ws/tunnel.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "io" "log" "net" "net/http" @@ -12,6 +13,17 @@ import ( "time" ) +// TunnelSession repraesentiert eine einzelne TCP-Verbindung durch einen Tunnel +type TunnelSession struct { + ID string + TunnelID string + ProxyConn net.Conn + Active bool + Ready chan struct{} // Wird geschlossen wenn Agent die Session bestaetigt hat + mutex sync.Mutex +} + +// BackendTunnel repraesentiert einen offenen Tunnel (Proxy-Port -> Agent -> Ziel) type BackendTunnel struct { ID string AgentID string @@ -21,17 +33,20 @@ type BackendTunnel struct { Listener net.Listener Active bool CreatedAt time.Time - // Aktive Proxy-Verbindungen (fuer Rueckkanal) - proxyConn net.Conn - proxyMutex sync.Mutex + Sessions map[string]*TunnelSession + sessMutex sync.RWMutex } +// TunnelManager verwaltet alle Tunnel und Sessions type TunnelManager struct { hub *Hub tunnels map[string]*BackendTunnel mutex sync.RWMutex - - // Port-Range für dynamische Tunnel-Proxies + + // Session-Index fuer schnellen Lookup bei Binary-Messages + sessions map[string]*TunnelSession + sessMutex sync.RWMutex + portStart int portEnd int usedPorts map[int]bool @@ -41,6 +56,7 @@ func NewTunnelManager(hub *Hub) *TunnelManager { return &TunnelManager{ hub: hub, tunnels: make(map[string]*BackendTunnel), + sessions: make(map[string]*TunnelSession), portStart: 10000, portEnd: 20000, usedPorts: make(map[int]bool), @@ -48,28 +64,23 @@ func NewTunnelManager(hub *Hub) *TunnelManager { } 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 + + tunnelID := tm.generateID() + 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, @@ -79,37 +90,19 @@ func (tm *TunnelManager) OpenTunnel(agentID, targetHost string, targetPort int) Listener: listener, Active: true, CreatedAt: time.Now(), + Sessions: make(map[string]*TunnelSession), } - - // 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)", + + // Proxy-Accept-Loop starten + go tm.runProxyAccept(tunnel) + + log.Printf("Tunnel %s geoeffnet: Agent %s -> %s:%d (Proxy: :%d)", tunnelID, agentID, targetHost, targetPort, proxyPort) - + return tunnel, nil } @@ -120,34 +113,36 @@ func (tm *TunnelManager) CloseTunnel(tunnelID string) error { tm.mutex.Unlock() return fmt.Errorf("Tunnel %s nicht gefunden", tunnelID) } - + tunnel.Active = false delete(tm.tunnels, tunnelID) tm.mutex.Unlock() - - // Listener schließen + + // Listener schliessen 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, - }, + + // Alle Sessions schliessen + tunnel.sessMutex.Lock() + for sessID, sess := range tunnel.Sessions { + sess.Active = false + if sess.ProxyConn != nil { + sess.ProxyConn.Close() } - - cmdJSON, _ := json.Marshal(cmd) - tm.hub.SendToAgent(tunnel.AgentID, cmdJSON) + // Session aus globalem Index entfernen + tm.sessMutex.Lock() + delete(tm.sessions, sessID) + tm.sessMutex.Unlock() + + // Agent informieren + tm.sendSessionDisconnect(tunnel.AgentID, sessID) } - + tunnel.Sessions = make(map[string]*TunnelSession) + tunnel.sessMutex.Unlock() + + tm.releasePort(tunnel.ProxyPort) + log.Printf("Tunnel %s geschlossen", tunnelID) return nil } @@ -155,7 +150,6 @@ func (tm *TunnelManager) CloseTunnel(tunnelID string) error { func (tm *TunnelManager) GetTunnel(tunnelID string) (*BackendTunnel, bool) { tm.mutex.RLock() defer tm.mutex.RUnlock() - tunnel, exists := tm.tunnels[tunnelID] return tunnel, exists } @@ -163,21 +157,19 @@ func (tm *TunnelManager) GetTunnel(tunnelID string) (*BackendTunnel, bool) { 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) } @@ -190,21 +182,22 @@ func (tm *TunnelManager) CloseAgentTunnels(agentID string) { } } tm.mutex.RUnlock() - + for _, tunnelID := range tunnelIDs { tm.CloseTunnel(tunnelID) } } -func (tm *TunnelManager) runTunnelProxy(tunnel *BackendTunnel) { +// runProxyAccept nimmt eingehende TCP-Verbindungen am Proxy-Port an +func (tm *TunnelManager) runProxyAccept(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) - + + log.Printf("Tunnel %s: Proxy lauscht auf Port %d", tunnel.ID, tunnel.ProxyPort) + for tunnel.Active { conn, err := tunnel.Listener.Accept() if err != nil { @@ -213,145 +206,220 @@ func (tm *TunnelManager) runTunnelProxy(tunnel *BackendTunnel) { } break } - - // Jede eingehende Verbindung in eigener Goroutine behandeln - go tm.handleProxyConnection(tunnel, conn) + + // Neue Session fuer diese TCP-Verbindung + go tm.handleNewSession(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() - +// handleNewSession erstellt eine neue Session fuer eine eingehende Proxy-Verbindung +func (tm *TunnelManager) handleNewSession(tunnel *BackendTunnel, proxyConn net.Conn) { + sessionID := tm.generateID() + + session := &TunnelSession{ + ID: sessionID, + TunnelID: tunnel.ID, + ProxyConn: proxyConn, + Active: true, + Ready: make(chan struct{}), + } + + // Session registrieren + tunnel.sessMutex.Lock() + tunnel.Sessions[sessionID] = session + tunnel.sessMutex.Unlock() + + tm.sessMutex.Lock() + tm.sessions[sessionID] = session + tm.sessMutex.Unlock() + + log.Printf("Tunnel %s: Neue Session %s von %s", tunnel.ID, sessionID, proxyConn.RemoteAddr()) + + // Agent auffordern, Ziel-Verbindung fuer diese Session zu oeffnen + cmd := map[string]interface{}{ + "type": "command", + "id": tm.generateID(), + "command": "tunnel_connect", + "params": map[string]interface{}{ + "session_id": sessionID, + "tunnel_id": tunnel.ID, + "target_host": tunnel.TargetHost, + "target_port": tunnel.TargetPort, + }, + } + + cmdJSON, _ := json.Marshal(cmd) + if err := tm.hub.SendToAgent(tunnel.AgentID, cmdJSON); err != nil { + log.Printf("Session %s: tunnel_connect senden fehlgeschlagen: %v", sessionID, err) + tm.cleanupSession(tunnel, sessionID) + proxyConn.Close() + return + } + + // Warten bis Agent die Session bestaetigt hat (max 10s) + select { + case <-session.Ready: + log.Printf("Session %s: Agent bereit, starte Forwarding", sessionID) + case <-time.After(10 * time.Second): + log.Printf("Session %s: Timeout beim Warten auf Agent", sessionID) + tm.cleanupSession(tunnel, sessionID) + proxyConn.Close() + return + } + + // Proxy -> Agent Data-Forwarding (liest von Proxy-Conn, schickt als Binary an Agent) defer func() { - tunnel.proxyMutex.Lock() - tunnel.proxyConn = nil - tunnel.proxyMutex.Unlock() - log.Printf("Tunnel %s: Proxy-Verbindung beendet", tunnel.ID) + tm.cleanupSession(tunnel, sessionID) + proxyConn.Close() + // Agent informieren dass Session beendet ist + tm.sendSessionDisconnect(tunnel.AgentID, sessionID) + log.Printf("Tunnel %s: Session %s beendet", tunnel.ID, sessionID) }() - - // Buffer fuer TCP-Data + buffer := make([]byte, 32768) - - for tunnel.Active { - n, err := conn.Read(buffer) + for session.Active && tunnel.Active { + n, err := proxyConn.Read(buffer) if err != nil { - if err.Error() != "EOF" && tunnel.Active { - log.Printf("Tunnel %s: Proxy-Read-Fehler: %v", tunnel.ID, err) + if err != io.EOF && session.Active { + log.Printf("Session %s: Proxy-Read-Fehler: %v", sessionID, 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) + if err := tm.sendSessionData(tunnel.AgentID, sessionID, buffer[:n]); err != nil { + log.Printf("Session %s: Weiterleitung fehlgeschlagen: %v", sessionID, 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) -} - +// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Agent (Ziel -> Proxy) 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") + + idLen := int(data[0]) + if len(data) < 1+idLen { + return fmt.Errorf("Session-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) + + sessionID := string(data[1 : 1+idLen]) + payload := data[1+idLen:] + + // Session finden + tm.sessMutex.RLock() + session, exists := tm.sessions[sessionID] + tm.sessMutex.RUnlock() + + if !exists || !session.Active { + return fmt.Errorf("Session %s nicht gefunden oder inaktiv", sessionID) } - - // 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) + + // Daten an Proxy-Verbindung schreiben + session.mutex.Lock() + defer session.mutex.Unlock() + + if session.ProxyConn == nil { + return fmt.Errorf("Session %s: keine Proxy-Verbindung", sessionID) } - - _, writeErr := conn.Write(payload) - if writeErr != nil { - return fmt.Errorf("Tunnel %s: Proxy-Write fehlgeschlagen: %w", tunnelID, writeErr) + + _, err := session.ProxyConn.Write(payload) + return err +} + +// sendSessionData schickt Daten als Binary-Message an den Agent +func (tm *TunnelManager) sendSessionData(agentID, sessionID string, data []byte) error { + idBytes := []byte(sessionID) + if len(idBytes) > 255 { + return fmt.Errorf("session_id zu lang") + } + + message := make([]byte, 1+len(idBytes)+len(data)) + message[0] = byte(len(idBytes)) + copy(message[1:], idBytes) + copy(message[1+len(idBytes):], data) + + return tm.hub.SendBinaryToAgent(agentID, message) +} + +// sendSessionDisconnect informiert den Agent dass eine Session beendet ist +func (tm *TunnelManager) sendSessionDisconnect(agentID, sessionID string) { + if !tm.hub.IsAgentConnected(agentID) { + return + } + + cmd := map[string]interface{}{ + "type": "command", + "id": tm.generateID(), + "command": "tunnel_disconnect", + "params": map[string]interface{}{ + "session_id": sessionID, + }, + } + + cmdJSON, _ := json.Marshal(cmd) + tm.hub.SendToAgent(agentID, cmdJSON) +} + +func (tm *TunnelManager) cleanupSession(tunnel *BackendTunnel, sessionID string) { + tunnel.sessMutex.Lock() + if sess, ok := tunnel.Sessions[sessionID]; ok { + sess.Active = false + delete(tunnel.Sessions, sessionID) + } + tunnel.sessMutex.Unlock() + + tm.sessMutex.Lock() + delete(tm.sessions, sessionID) + tm.sessMutex.Unlock() +} + +// ConfirmSession markiert eine Session als bereit (Agent hat Ziel-Verbindung hergestellt) +func (tm *TunnelManager) ConfirmSession(sessionID string) { + tm.sessMutex.RLock() + session, exists := tm.sessions[sessionID] + tm.sessMutex.RUnlock() + + if exists && session.Active { + select { + case <-session.Ready: + // Bereits geschlossen + default: + close(session.Ready) + } } - - 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") + return 0, fmt.Errorf("keine freien Ports verfuegbar") } func (tm *TunnelManager) releasePort(port int) { tm.mutex.Lock() defer tm.mutex.Unlock() - delete(tm.usedPorts, port) } -func (tm *TunnelManager) generateTunnelID() string { +func (tm *TunnelManager) generateID() string { bytes := make([]byte, 8) rand.Read(bytes) return hex.EncodeToString(bytes) } -// CreateHTTPProxy erstellt einen HTTP-Reverse-Proxy für einen Tunnel +// CreateHTTPProxy - HTTP-Reverse-Proxy (noch nicht implementiert) 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 +}