WebSocket-Infrastruktur: bidirektionaler Command-Kanal + TCP-Tunnel

- Agent: WS-Client mit Reconnect, Command-Handler (exec, tunnel_open/close/data)
- Backend: WS-Hub, Tunnel-Manager mit dynamischen Proxy-Ports (10000-20000)
- API: /agents/{id}/exec, /tunnel, /tunnels, /proxy/{tunnel_id}
- Binary WebSocket Messages fuer effiziente Tunnel-Daten
- gorilla/websocket (pure Go, kein CGO)
- README aktualisiert
This commit is contained in:
cynfo3000 2026-02-28 07:42:33 +01:00
parent 6120d0cffa
commit f47154787d
12 changed files with 1211 additions and 198 deletions

435
README.md
View File

@ -1,37 +1,66 @@
# OPNsense RMM System # 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 ## Ueberblick
Dieses System besteht aus zwei Komponenten: Dieses System besteht aus zwei Komponenten:
- **Backend** (Go) - REST API Server mit TLS und SQLite-Datenbank, laeuft auf einem Linux-Server - **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 und sendet sie an das Backend - **Agent** (Go) - Laeuft auf OPNsense/FreeBSD, sammelt Systemdaten, haelt WebSocket-Verbindung zum Backend und ermoeglicht Remote-Commands/Tunneling
## Architektur ## Architektur
``` ```
┌──────────────────┐ HTTPS (TLS) ┌──────────────────┐ ┌──────────────────────┐ WebSocket + HTTPS (TLS) ┌──────────────────────┐
│ OPNsense FW │ ────────────────────────► │ RMM Backend │ │ OPNsense FW │ ◄─────────────────────────────► │ RMM Backend │
│ (FreeBSD) │ POST /agent/register │ (Linux) │ │ (FreeBSD) │ wss://backend:8443/api/v1/ │ (Linux) │
│ │ POST /agent/heartbeat │ │ │ │ agent/ws │ │
│ rmm-agent │ │ REST API │ │ rmm-agent │ │ REST API │
│ - Hardware │ │ SQLite DB │ │ - Hardware/CPU/RAM │ POST /agent/register │ WebSocket Hub │
│ - CPU/RAM │ │ :8443 │ │ - Disk/Network │ POST /agent/heartbeat │ Tunnel Manager │
│ - Disk/Net │ │ │ │ - Services/Certs │ │ SQLite DB │
│ - Services │ │ 192.168.85.13 │ │ - WebSocket Client │ Commands: │ :8443 │
│ │ │ │ │ - Tunnel Manager │ • exec (Befehl ausfuehren) │ │
│ 192.168.85.33 │ │ │ │ │ • 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 ## Voraussetzungen
- Go 1.22+ - Go 1.22+
- make - make
- OpenSSL (fuer manuelle Zertifikat-Generierung) - OpenSSL (fuer manuelle Zertifikat-Generierung)
- SSH-Zugang zu den Zielservern - SSH-Zugang zu den Zielservern
- gorilla/websocket Library (automatisch über go mod)
## Build ## Build
@ -45,6 +74,10 @@ make backend
# Nur Agent (freebsd/amd64) # Nur Agent (freebsd/amd64)
make agent make agent
# Dependencies aktualisieren
cd agent && go mod tidy
cd backend && go mod tidy
# TLS-Zertifikate generieren (optional, Backend generiert automatisch) # TLS-Zertifikate generieren (optional, Backend generiert automatisch)
make certs make certs
``` ```
@ -64,15 +97,10 @@ api_keys:
- "01532e5a7c9e70bf2757df77a2f5b9b9" - "01532e5a7c9e70bf2757df77a2f5b9b9"
``` ```
| Parameter | Env-Variable | Beschreibung | **Neue WebSocket-Features:**
|-------------|------------------|-------------------------------------| - WebSocket-Hub läuft automatisch
| listen_addr | RMM_LISTEN_ADDR | Bind-Adresse (default: `:8443`) | - Tunnel-Manager verwaltet aktive Tunnel
| tls_cert | RMM_TLS_CERT | Pfad zum TLS-Zertifikat | - Port-Range 10000-20000 für dynamische Proxy-Ports
| 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.
### Agent (`agent/config.yaml`) ### Agent (`agent/config.yaml`)
@ -84,13 +112,10 @@ agent_name: "opnsense-fw01"
insecure: true insecure: true
``` ```
| Parameter | Beschreibung | **WebSocket wird automatisch gestartet:**
|-----------------|-------------------------------------------------| - WebSocket URL: `wss://192.168.85.13:8443/api/v1/agent/ws?agent_id=XXX&api_key=XXX`
| backend_url | URL des Backend-Servers | - Reconnect mit Backoff bei Verbindungsabbruch
| api_key | API-Key fuer Authentifizierung | - Ping/Pong Keepalive alle 30s
| interval_seconds | Heartbeat-Intervall in Sekunden (default: 60) |
| agent_name | Name des Agents |
| insecure | TLS-Zertifikatpruefung ueberspringen |
## Deployment ## Deployment
@ -100,11 +125,11 @@ insecure: true
make deploy-backend make deploy-backend
``` ```
Dies: Dies startet:
1. Baut das Backend fuer linux/amd64 1. REST API Server auf :8443
2. Kopiert Binary + Config nach `/opt/rmm/` 2. WebSocket Hub für Agent-Verbindungen
3. Erstellt einen systemd-Service 3. Tunnel Manager für TCP-Proxies
4. Startet den Service 4. SQLite-Datenbank für persistente Daten
### Agent (192.168.85.33 / OPNsense) ### Agent (192.168.85.33 / OPNsense)
@ -112,210 +137,244 @@ Dies:
make deploy-agent make deploy-agent
``` ```
Dies: Dies startet:
1. Baut den Agent fuer freebsd/amd64 1. HTTP Heartbeat-Loop (weiterhin alle 60s)
2. Kopiert Binary + Config nach `/opt/rmm/` 2. WebSocket-Client mit automatischem Reconnect
3. Erstellt ein rc.d-Script 3. Command-Handler für eingehende Befehle
4. Aktiviert und startet den Service 4. Tunnel-Manager für TCP-Verbindungen
## API-Dokumentation ## 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:** **Request:**
```json ```json
{ {
"name": "opnsense-fw01", "command": "uptime",
"hostname": "fw01.local", "timeout": 30
"ip": "192.168.85.33", }
"opnsense_version": "OPNsense 24.1.1" ```
**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):** **Response (201):**
```json ```json
{ {
"id": "a1b2c3d4e5f6...", "tunnel_id": "a1b2c3d4e5f6789a",
"message": "Agent erfolgreich registriert" "proxy_port": 12345,
"target_host": "127.0.0.1",
"target_port": 443,
"created_at": "2024-01-28T12:30:00Z"
} }
``` ```
### POST /api/v1/agent/heartbeat #### DELETE /api/v1/agents/{id}/tunnel/{tunnel_id}
Tunnel schließen.
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"
}
]
}
}
```
**Response (200):** **Response (200):**
```json ```json
{ {
"message": "OK" "message": "Tunnel geschlossen"
} }
``` ```
### GET /api/v1/agents #### GET /api/v1/agents/{id}/tunnels
Aktive Tunnel auflisten.
Alle registrierten Agents auflisten.
**Response (200):** **Response (200):**
```json ```json
[ [
{ {
"id": "a1b2c3d4e5f6...", "tunnel_id": "a1b2c3d4e5f6789a",
"name": "opnsense-fw01", "proxy_port": 12345,
"hostname": "fw01.local", "target_host": "127.0.0.1",
"ip": "192.168.85.33", "target_port": 443,
"opnsense_version": "OPNsense 24.1.1", "active": true,
"registered_at": "2024-01-28T10:00:00Z", "created_at": "2024-01-28T12:30:00Z"
"last_heartbeat": "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):** **Response (200):**
```json ```json
{ {
"agent": { "status": "running",
"id": "a1b2c3d4e5f6...", "connected_agents": ["abc123", "def456"],
"name": "opnsense-fw01", "stats": {
"hostname": "fw01.local", "connected_agents": 2,
"ip": "192.168.85.33", "active_tunnels": 3
"opnsense_version": "OPNsense 24.1.1",
"registered_at": "2024-01-28T10:00:00Z",
"last_heartbeat": "2024-01-28T12:30:00Z"
}, },
"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):** **Query Parameters:**
```json - `agent_id`: Agent-ID (aus Registrierung)
{ - `api_key`: API-Key für Authentifizierung
"hostname": "fw01.local",
"opnsense_version": "OPNsense 24.1.1", **Usage:**
"uptime_seconds": 86400, ```
"hardware": { ... }, wss://192.168.85.13:8443/api/v1/agent/ws?agent_id=abc123&api_key=xxx
"cpu": { ... },
"memory": { ... },
"disks": [ ... ],
"network_interfaces": [ ... ],
"services": [ ... ]
}
``` ```
### DELETE /api/v1/agents/{id} ## Tunnel-Datenformat
Agent entfernen. ### Binary WebSocket Messages
TCP-Tunnel-Daten werden als Binary Messages übertragen:
**Response (200):** **Format:** `[tunnel_id_length:1][tunnel_id][data]`
```json
{
"message": "Agent geloescht"
}
```
### 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 ### Beispiel-Usage
{
"error": "unauthorized"
}
```
HTTP Status Codes: 1. **Tunnel öffnen:**
- `200` - Erfolg ```bash
- `201` - Erstellt curl -X POST http://backend:8443/api/v1/agents/abc123/tunnel \
- `400` - Ungueltige Anfrage -H "X-API-Key: xxx" \
- `401` - Nicht autorisiert (fehlender/falscher API-Key) -d '{"target_host":"127.0.0.1","target_port":443}'
- `404` - Nicht gefunden ```
- `500` - Server-Fehler
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 ## Lizenz

View File

@ -4,4 +4,4 @@ go 1.22
require gopkg.in/yaml.v3 v3.0.1 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

View File

@ -12,6 +12,7 @@ import (
"github.com/cynfo/rmm-agent/client" "github.com/cynfo/rmm-agent/client"
"github.com/cynfo/rmm-agent/collector" "github.com/cynfo/rmm-agent/collector"
"github.com/cynfo/rmm-agent/config" "github.com/cynfo/rmm-agent/config"
"github.com/cynfo/rmm-agent/ws"
) )
func main() { func main() {
@ -79,6 +80,13 @@ func main() {
log.Printf("Agent registriert mit ID: %s", agentID) 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 // Heartbeat-Loop
ticker := time.NewTicker(time.Duration(cfg.IntervalSeconds) * time.Second) ticker := time.NewTicker(time.Duration(cfg.IntervalSeconds) * time.Second)
defer ticker.Stop() defer ticker.Stop()

View File

@ -3,9 +3,9 @@ package ws
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"log" "log"
"net/http"
"net/url" "net/url"
"sync" "sync"
"time" "time"
@ -218,12 +218,27 @@ func (c *Client) readMessages(conn *websocket.Conn, msgChan chan<- Message, errC
defer close(msgChan) defer close(msgChan)
for { for {
var msg Message messageType, data, err := conn.ReadMessage()
if err := conn.ReadJSON(&msg); err != nil { if err != nil {
errChan <- err errChan <- err
return 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 { select {
case msgChan <- msg: case msgChan <- msg:
case <-c.ctx.Done(): case <-c.ctx.Done():

View File

@ -6,7 +6,6 @@ import (
"log" "log"
"os/exec" "os/exec"
"strconv" "strconv"
"strings"
"time" "time"
) )

View File

@ -11,6 +11,7 @@ import (
"github.com/cynfo/rmm-backend/db" "github.com/cynfo/rmm-backend/db"
"github.com/cynfo/rmm-backend/models" "github.com/cynfo/rmm-backend/models"
"github.com/cynfo/rmm-backend/ws"
) )
type Handler struct { type Handler struct {
@ -22,13 +23,17 @@ func NewHandler(database *db.Database) *Handler {
} }
// SetupRoutes - Alle API-Routes registrieren // 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/register", h.registerAgent)
mux.HandleFunc("POST /api/v1/agent/heartbeat", h.heartbeat) mux.HandleFunc("POST /api/v1/agent/heartbeat", h.heartbeat)
mux.HandleFunc("GET /api/v1/agents", h.listAgents) mux.HandleFunc("GET /api/v1/agents", h.listAgents)
mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent) mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent)
mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData) mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData)
mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent) mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent)
// WebSocket und Tunnel-Routes
h.setupTunnelRoutes(mux, hub)
} }
func generateID() string { func generateID() string {

195
backend/api/tunnel_proxy.go Normal file
View File

@ -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)
}
}

View File

@ -3,6 +3,7 @@ module github.com/cynfo/rmm-backend
go 1.22 go 1.22
require ( require (
github.com/gorilla/websocket v1.5.3
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.29.1 modernc.org/sqlite v1.29.1
) )
@ -10,7 +11,6 @@ require (
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // 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/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect

View File

@ -19,6 +19,7 @@ import (
"github.com/cynfo/rmm-backend/api" "github.com/cynfo/rmm-backend/api"
"github.com/cynfo/rmm-backend/config" "github.com/cynfo/rmm-backend/config"
"github.com/cynfo/rmm-backend/db" "github.com/cynfo/rmm-backend/db"
"github.com/cynfo/rmm-backend/ws"
) )
func main() { func main() {
@ -44,10 +45,14 @@ func main() {
} }
defer database.Close() defer database.Close()
// WebSocket Hub starten
hub := ws.NewHub()
go hub.Run()
// Router aufbauen // Router aufbauen
mux := http.NewServeMux() mux := http.NewServeMux()
handler := api.NewHandler(database) handler := api.NewHandler(database)
handler.SetupRoutes(mux) handler.SetupRoutes(mux, hub)
// Middleware-Chain: Logging -> Auth -> Handler // Middleware-Chain: Logging -> Auth -> Handler
var chain http.Handler = mux var chain http.Handler = mux

153
backend/ws/handler.go Normal file
View File

@ -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)
}

238
backend/ws/hub.go Normal file
View File

@ -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
}

336
backend/ws/tunnel.go Normal file
View File

@ -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
}