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:
parent
6120d0cffa
commit
f47154787d
439
README.md
439
README.md
@ -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
|
||||||
|
|
||||||
@ -42,9 +71,13 @@ make all
|
|||||||
# Nur Backend (linux/amd64)
|
# Nur Backend (linux/amd64)
|
||||||
make backend
|
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,211 +137,245 @@ 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
|
||||||
|
|
||||||
Intern.
|
Intern.
|
||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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():
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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
195
backend/api/tunnel_proxy.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
153
backend/ws/handler.go
Normal 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
238
backend/ws/hub.go
Normal 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
336
backend/ws/tunnel.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user