Meilenstein 2: Session-basierte TCP-Tunnel
- Tunnel unterstuetzen mehrere gleichzeitige TCP-Verbindungen - Jede Proxy-Verbindung bekommt eigene Session-ID - Agent: Lazy-Connect (Ziel-Verbindung erst bei Datentransfer) - Backend: Ready-Handshake (wartet auf Agent-Bestaetigung) - Binary WebSocket Messages mit Session-ID statt Tunnel-ID - Connection.Close() Race-Condition behoben (sync.Once) - SendToAgent/SendBinaryToAgent mit Timeout statt Blocking - Hub verarbeitet Agent-Responses (Session-Confirm) - README komplett aktualisiert
This commit is contained in:
parent
fa68e191f9
commit
68e712b3a3
503
README.md
503
README.md
@ -1,380 +1,249 @@
|
|||||||
# OPNsense RMM System
|
# OPNsense RMM System
|
||||||
|
|
||||||
Remote Monitoring & Management System fuer OPNsense Firewalls mit WebSocket-basierter Kommunikation und TCP-Tunnel-Funktionalitaet.
|
Remote Monitoring & Management fuer OPNsense Firewalls.
|
||||||
|
Go-basierter Agent + Backend mit WebSocket-Kommunikation und Session-basierten TCP-Tunneln.
|
||||||
## Ueberblick
|
|
||||||
|
|
||||||
Dieses System besteht aus zwei Komponenten:
|
|
||||||
|
|
||||||
- **Backend** (Go) - REST API Server mit WebSocket Hub, TLS und SQLite-Datenbank, laeuft auf einem Linux-Server
|
|
||||||
- **Agent** (Go) - Laeuft auf OPNsense/FreeBSD, sammelt Systemdaten, haelt WebSocket-Verbindung zum Backend und ermoeglicht Remote-Commands/Tunneling
|
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────┐ WebSocket + HTTPS (TLS) ┌──────────────────────┐
|
┌────────────────┐ HTTPS/WSS ┌────────────────┐ TCP Proxy ┌────────────┐
|
||||||
│ OPNsense FW │ ◄─────────────────────────────► │ RMM Backend │
|
│ OPNsense FW │◄──────────────►│ RMM Backend │◄───────────────│ Browser/ │
|
||||||
│ (FreeBSD) │ wss://backend:8443/api/v1/ │ (Linux) │
|
│ (FreeBSD) │ :8443 │ (Linux) │ :10000-20000 │ SSH/etc. │
|
||||||
│ │ agent/ws │ │
|
│ │ │ │ └────────────┘
|
||||||
│ rmm-agent │ │ REST API │
|
│ rmm-agent │ Heartbeat │ REST API │
|
||||||
│ - Hardware/CPU/RAM │ POST /agent/register │ WebSocket Hub │
|
│ - Collectors │ WebSocket │ WebSocket Hub │
|
||||||
│ - Disk/Network │ POST /agent/heartbeat │ Tunnel Manager │
|
│ - WS Client │ Binary Msgs │ Tunnel Mgr │
|
||||||
│ - Services/Certs │ │ SQLite DB │
|
│ - Tunnel Mgr │ │ SQLite DB │
|
||||||
│ - WebSocket Client │ Commands: │ :8443 │
|
└────────────────┘ └────────────────┘
|
||||||
│ - Tunnel Manager │ • exec (Befehl ausfuehren) │ │
|
|
||||||
│ │ • tunnel_open/close │ 192.168.85.13 │
|
|
||||||
│ 192.168.85.33 │ • tunnel_data (TCP-Proxy) │ │
|
|
||||||
└──────────────────────┘ └──────────────────────┘
|
|
||||||
|
|
||||||
TCP-Tunnel Flow:
|
|
||||||
Browser/Client ──► Backend Proxy ──► WebSocket ──► Agent ──► OPNsense Service
|
|
||||||
(Port 10000-20000) (Binary) (Local) (z.B. :443, :22)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Neue Funktionen (WebSocket/Tunnel)
|
### Tunnel-Flow
|
||||||
|
|
||||||
### 1. Bidirektionale Kommunikation
|
```
|
||||||
- **WebSocket-Verbindung**: Agent baut nach Registrierung eine persistente WebSocket-Verbindung zum Backend auf
|
Client ──► Backend:ProxyPort ──► WebSocket (Binary) ──► Agent ──► Zieldienst
|
||||||
- **Automatisches Reconnect**: Bei Verbindungsabbruch mit exponential backoff
|
(TCP-Listener) (Session-basiert) (TCP) (SSH/HTTPS/etc)
|
||||||
- **Ping/Pong Keepalive**: Verbindung wird automatisch überwacht
|
```
|
||||||
- **Command-Response-Pattern**: Backend kann Commands an Agents senden und Responses erhalten
|
|
||||||
|
|
||||||
### 2. Remote Command Execution
|
- Jeder Tunnel oeffnet einen dynamischen Proxy-Port (10000-20000) auf dem Backend
|
||||||
- **exec Command**: Backend kann Befehle auf der OPNsense ausführen lassen
|
- Jede TCP-Verbindung am Proxy bekommt eine eigene **Session**
|
||||||
- **Timeout-Support**: Configurable timeouts für Command-Execution
|
- Der Agent oeffnet pro Session eine separate Ziel-Verbindung (Lazy Connect)
|
||||||
- **stdout/stderr Output**: Vollständige Ausgabe wird zurückgeliefert
|
- Mehrere gleichzeitige Verbindungen pro Tunnel moeglich
|
||||||
|
- Tunnel bleiben offen wenn einzelne Sessions beendet werden
|
||||||
|
|
||||||
### 3. TCP-Tunneling über WebSocket
|
## Komponenten
|
||||||
- **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
|
### Agent (FreeBSD/OPNsense)
|
||||||
- **Reverse Proxy**: `GET /api/v1/agents/{id}/proxy/{tunnel_id}/*` leitet HTTP-Traffic durch Tunnel
|
- **Collectors**: Hardware, CPU, Memory, Disk, Network, Services, WireGuard, DHCP (KEA/ISC/dnsmasq), Routing, Gateways, Zertifikate, Plugins, Updates
|
||||||
- **Transparent**: Browser kann direkt auf OPNsense WebUI zugreifen über Backend-URL
|
- **WebSocket Client**: Persistente Verbindung zum Backend, automatisches Reconnect mit Backoff
|
||||||
|
- **Command Handler**: Remote-Befehlsausfuehrung, Tunnel-Connect/Disconnect
|
||||||
|
- **Tunnel Manager**: Session-basierte TCP-Verbindungen zu lokalen Diensten
|
||||||
|
|
||||||
## Voraussetzungen
|
### Backend (Linux)
|
||||||
|
- **REST API**: Agent-Registrierung, Heartbeat, Systemdaten, Tunnel-Management
|
||||||
- Go 1.22+
|
- **WebSocket Hub**: Verwaltet Agent-Verbindungen, routet Commands und Binary-Messages
|
||||||
- make
|
- **Tunnel Manager**: Proxy-Listener, Session-Tracking, Ready-Handshake
|
||||||
- OpenSSL (fuer manuelle Zertifikat-Generierung)
|
- **SQLite DB**: Persistente Agent- und Systemdaten
|
||||||
- SSH-Zugang zu den Zielservern
|
|
||||||
- gorilla/websocket Library (automatisch über go mod)
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Beide Komponenten bauen
|
make all # Backend (linux/amd64) + Agent (freebsd/amd64)
|
||||||
make all
|
make backend # Nur Backend
|
||||||
|
make agent # Nur Agent
|
||||||
# Nur Backend (linux/amd64)
|
|
||||||
make backend
|
|
||||||
|
|
||||||
# Nur Agent (freebsd/amd64)
|
|
||||||
make agent
|
|
||||||
|
|
||||||
# Dependencies aktualisieren
|
|
||||||
cd agent && go mod tidy
|
|
||||||
cd backend && go mod tidy
|
|
||||||
|
|
||||||
# TLS-Zertifikate generieren (optional, Backend generiert automatisch)
|
|
||||||
make certs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Binaries landen in `build/`.
|
Binaries in `build/`. Benoetigt Go 1.22+.
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
### Backend (`backend/config.yaml`)
|
### Backend (`config.yaml`)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
listen_addr: ":8443"
|
listen_addr: ":8443"
|
||||||
tls_cert: "certs/server.crt"
|
tls_cert: "certs/server.crt" # Wird automatisch generiert
|
||||||
tls_key: "certs/server.key"
|
tls_key: "certs/server.key"
|
||||||
db_path: "rmm.db"
|
db_path: "rmm.db"
|
||||||
api_keys:
|
api_keys:
|
||||||
- "01532e5a7c9e70bf2757df77a2f5b9b9"
|
- "dein-api-key-hier"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Neue WebSocket-Features:**
|
### Agent (`config.yaml`)
|
||||||
- WebSocket-Hub läuft automatisch
|
|
||||||
- Tunnel-Manager verwaltet aktive Tunnel
|
|
||||||
- Port-Range 10000-20000 für dynamische Proxy-Ports
|
|
||||||
|
|
||||||
### Agent (`agent/config.yaml`)
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
backend_url: "https://192.168.85.13:8443"
|
backend_url: "https://backend-ip:8443"
|
||||||
api_key: "01532e5a7c9e70bf2757df77a2f5b9b9"
|
api_key: "dein-api-key-hier"
|
||||||
interval_seconds: 60
|
interval_seconds: 60
|
||||||
agent_name: "opnsense-fw01"
|
agent_name: "opnsense-fw01"
|
||||||
insecure: true
|
insecure: true # Self-signed Certs akzeptieren
|
||||||
```
|
```
|
||||||
|
|
||||||
**WebSocket wird automatisch gestartet:**
|
|
||||||
- WebSocket URL: `wss://192.168.85.13:8443/api/v1/agent/ws?agent_id=XXX&api_key=XXX`
|
|
||||||
- Reconnect mit Backoff bei Verbindungsabbruch
|
|
||||||
- Ping/Pong Keepalive alle 30s
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Backend (192.168.85.13)
|
### Backend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make deploy-backend
|
# Build + Copy
|
||||||
|
make backend
|
||||||
|
scp build/rmm-backend user@backend:/pfad/rmm-backend
|
||||||
|
|
||||||
|
# Starten (als normaler User, kein root noetig)
|
||||||
|
cd /pfad && nohup ./rmm-backend config.yaml > rmm-backend.log 2>&1 &
|
||||||
```
|
```
|
||||||
|
|
||||||
Dies startet:
|
### Agent (OPNsense/FreeBSD)
|
||||||
1. REST API Server auf :8443
|
|
||||||
2. WebSocket Hub für Agent-Verbindungen
|
|
||||||
3. Tunnel Manager für TCP-Proxies
|
|
||||||
4. SQLite-Datenbank für persistente Daten
|
|
||||||
|
|
||||||
### Agent (192.168.85.33 / OPNsense)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make deploy-agent
|
# Build + Copy
|
||||||
|
make agent
|
||||||
|
scp build/rmm-agent root@opnsense:/usr/local/rmm/rmm-agent
|
||||||
|
|
||||||
|
# Starten via FreeBSD daemon
|
||||||
|
daemon -f -p /var/run/rmm-agent.pid -o /usr/local/rmm/rmm-agent.log \
|
||||||
|
/usr/local/rmm/rmm-agent --config /usr/local/rmm/config.yaml --insecure
|
||||||
```
|
```
|
||||||
|
|
||||||
Dies startet:
|
**Wichtig**: Agent-ID wird persistent in `/usr/local/rmm/agent_id` gespeichert.
|
||||||
1. HTTP Heartbeat-Loop (weiterhin alle 60s)
|
|
||||||
2. WebSocket-Client mit automatischem Reconnect
|
|
||||||
3. Command-Handler für eingehende Befehle
|
|
||||||
4. Tunnel-Manager für TCP-Verbindungen
|
|
||||||
|
|
||||||
## API-Dokumentation
|
## API
|
||||||
|
|
||||||
|
### Agent-Management
|
||||||
|
|
||||||
|
| Methode | Endpoint | Beschreibung |
|
||||||
|
|---------|----------|-------------|
|
||||||
|
| POST | `/api/v1/agent/register` | Agent registrieren (mit optionaler `agent_id`) |
|
||||||
|
| POST | `/api/v1/agent/heartbeat` | Systemdaten senden |
|
||||||
|
| GET | `/api/v1/agents` | Alle Agents auflisten |
|
||||||
|
| GET | `/api/v1/agents/{id}` | Agent + Systemdaten abrufen |
|
||||||
|
| GET | `/api/v1/agents/{id}/system` | Nur Systemdaten |
|
||||||
|
| DELETE | `/api/v1/agents/{id}` | Agent loeschen |
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
| Methode | Endpoint | Beschreibung |
|
||||||
|
|---------|----------|-------------|
|
||||||
|
| GET | `/api/v1/agent/ws?agent_id=X&api_key=X` | WebSocket-Verbindung (Agent) |
|
||||||
|
| GET | `/api/v1/hub/status` | Hub-Status (connected agents, aktive Tunnel) |
|
||||||
|
|
||||||
|
### Tunnel-Management
|
||||||
|
|
||||||
|
| Methode | Endpoint | Beschreibung |
|
||||||
|
|---------|----------|-------------|
|
||||||
|
| POST | `/api/v1/agents/{id}/tunnel` | Tunnel oeffnen |
|
||||||
|
| GET | `/api/v1/agents/{id}/tunnels` | Aktive Tunnel auflisten |
|
||||||
|
| DELETE | `/api/v1/agents/{id}/tunnel/{tid}` | Tunnel schliessen |
|
||||||
|
| POST | `/api/v1/agents/{id}/exec` | Remote-Command ausfuehren |
|
||||||
|
|
||||||
|
### Tunnel erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSH-Tunnel zur OPNsense
|
||||||
|
curl -sk -H "X-API-Key: KEY" -X POST \
|
||||||
|
https://backend:8443/api/v1/agents/AGENT_ID/tunnel \
|
||||||
|
-d '{"target_host":"127.0.0.1","target_port":22}'
|
||||||
|
|
||||||
|
# Response: {"tunnel_id":"xxx","proxy_port":10000,...}
|
||||||
|
|
||||||
|
# Verbinden:
|
||||||
|
ssh -p 10000 root@backend-ip
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WebGUI-Tunnel (OPNsense auf Port 4444)
|
||||||
|
curl -sk -H "X-API-Key: KEY" -X POST \
|
||||||
|
https://backend:8443/api/v1/agents/AGENT_ID/tunnel \
|
||||||
|
-d '{"target_host":"127.0.0.1","target_port":4444}'
|
||||||
|
|
||||||
|
# Response: {"tunnel_id":"xxx","proxy_port":10001,...}
|
||||||
|
|
||||||
|
# Im Browser: https://backend-ip:10001/
|
||||||
|
```
|
||||||
|
|
||||||
### WebSocket Commands (Backend → Agent)
|
### WebSocket Commands (Backend → Agent)
|
||||||
|
|
||||||
#### exec - Befehl ausführen
|
|
||||||
```json
|
```json
|
||||||
{
|
// Remote-Befehl ausfuehren
|
||||||
"type": "command",
|
{"type":"command","id":"cmd1","command":"exec","params":{"command":"uptime","timeout":30}}
|
||||||
"id": "cmd-123",
|
|
||||||
"command": "exec",
|
// Tunnel-Session oeffnen (wird automatisch vom Backend gesendet)
|
||||||
"params": {
|
{"type":"command","id":"cmd2","command":"tunnel_connect","params":{"session_id":"xxx","tunnel_id":"xxx","target_host":"127.0.0.1","target_port":22}}
|
||||||
"command": "ps aux | head -10",
|
|
||||||
"timeout": 30
|
// Tunnel-Session schliessen
|
||||||
}
|
{"type":"command","id":"cmd3","command":"tunnel_disconnect","params":{"session_id":"xxx"}}
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
### Binary WebSocket Messages (Tunnel-Daten)
|
||||||
```json
|
|
||||||
{
|
Format: `[session_id_length:1][session_id][tcp_payload]`
|
||||||
"type": "response",
|
|
||||||
"id": "cmd-123",
|
Tunnel-Daten werden als Binary WebSocket Messages uebertragen (kein Base64-Overhead).
|
||||||
"status": "ok",
|
|
||||||
"data": {
|
## Dateistruktur
|
||||||
"output": "PID COMMAND\n1 kernel\n..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
rmm/
|
||||||
#### tunnel_open - TCP-Tunnel öffnen
|
├── Makefile
|
||||||
```json
|
├── README.md
|
||||||
{
|
├── agent/
|
||||||
"type": "command",
|
│ ├── main.go # Entry, Registration, Heartbeat-Loop, WS-Start
|
||||||
"id": "cmd-124",
|
│ ├── config/
|
||||||
"command": "tunnel_open",
|
│ │ └── config.go # YAML Config Loader
|
||||||
"params": {
|
│ ├── client/
|
||||||
"target_host": "127.0.0.1",
|
│ │ └── client.go # HTTP Client (Register, Heartbeat)
|
||||||
"target_port": 443
|
│ ├── collector/
|
||||||
}
|
│ │ ├── hardware.go # Hersteller, Modell, BIOS
|
||||||
}
|
│ │ ├── cpu.go # CPU-Modell, Cores, Usage
|
||||||
|
│ │ ├── memory.go # RAM total/used/free
|
||||||
|
│ │ ├── disk.go # ZFS Datasets, Belegung
|
||||||
|
│ │ ├── network.go # Interfaces, IPs, Status
|
||||||
|
│ │ ├── services.go # Laufende Dienste
|
||||||
|
│ │ ├── wireguard.go # WG-Tunnel, Peers, Transfer
|
||||||
|
│ │ ├── dhcp.go # KEA/ISC/dnsmasq Leases
|
||||||
|
│ │ ├── opnsense.go # OPN-Version, FreeBSD, Hostname, Uptime
|
||||||
|
│ │ ├── routing.go # Routing-Tabelle (netstat -rn)
|
||||||
|
│ │ ├── gateways.go # Gateway-Status (configctl)
|
||||||
|
│ │ ├── certificates.go # Zertifikate + CAs (config.xml + PEM)
|
||||||
|
│ │ ├── plugins.go # Installierte Plugins (pkg info)
|
||||||
|
│ │ └── updates.go # Pending Updates (core + packages)
|
||||||
|
│ └── ws/
|
||||||
|
│ ├── client.go # WebSocket Client, Reconnect, Ping/Pong
|
||||||
|
│ ├── handler.go # Command Handler (exec, tunnel_connect/disconnect)
|
||||||
|
│ └── tunnel.go # Session-basierter Tunnel Manager
|
||||||
|
├── backend/
|
||||||
|
│ ├── main.go # Entry, TLS, HTTP Server
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── config.go # YAML Config Loader
|
||||||
|
│ ├── db/
|
||||||
|
│ │ └── sqlite.go # SQLite (Agent + Systemdaten)
|
||||||
|
│ ├── models/
|
||||||
|
│ │ ├── agent.go # Agent, Register/Heartbeat Structs
|
||||||
|
│ │ └── system.go # SystemData, alle Collector-Structs
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── handlers.go # REST API Handler
|
||||||
|
│ │ ├── tunnel_proxy.go # Tunnel + Exec REST Endpoints
|
||||||
|
│ │ └── middleware.go # API-Key Auth, Logging
|
||||||
|
│ └── ws/
|
||||||
|
│ ├── handler.go # WebSocket Upgrade, Connection R/W Pumps
|
||||||
|
│ ├── hub.go # Agent-Registry, Message Routing
|
||||||
|
│ └── tunnel.go # Session-basierter Tunnel + Proxy Manager
|
||||||
|
└── build/ # Kompilierte Binaries
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "response",
|
|
||||||
"id": "cmd-124",
|
|
||||||
"status": "ok",
|
|
||||||
"data": {
|
|
||||||
"tunnel_id": "a1b2c3d4e5f6789a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### tunnel_close - TCP-Tunnel schließen
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"id": "cmd-125",
|
|
||||||
"command": "tunnel_close",
|
|
||||||
"params": {
|
|
||||||
"tunnel_id": "a1b2c3d4e5f6789a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Neue REST API Endpoints
|
|
||||||
|
|
||||||
#### POST /api/v1/agents/{id}/exec
|
|
||||||
Befehl auf Agent ausführen.
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"command": "uptime",
|
|
||||||
"timeout": 30
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (200):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Command gesendet",
|
|
||||||
"command_id": "cmd-123"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### POST /api/v1/agents/{id}/tunnel
|
|
||||||
TCP-Tunnel zu Agent öffnen.
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"target_host": "127.0.0.1",
|
|
||||||
"target_port": 443
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response (201):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tunnel_id": "a1b2c3d4e5f6789a",
|
|
||||||
"proxy_port": 12345,
|
|
||||||
"target_host": "127.0.0.1",
|
|
||||||
"target_port": 443,
|
|
||||||
"created_at": "2024-01-28T12:30:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### DELETE /api/v1/agents/{id}/tunnel/{tunnel_id}
|
|
||||||
Tunnel schließen.
|
|
||||||
|
|
||||||
**Response (200):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Tunnel geschlossen"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GET /api/v1/agents/{id}/tunnels
|
|
||||||
Aktive Tunnel auflisten.
|
|
||||||
|
|
||||||
**Response (200):**
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"tunnel_id": "a1b2c3d4e5f6789a",
|
|
||||||
"proxy_port": 12345,
|
|
||||||
"target_host": "127.0.0.1",
|
|
||||||
"target_port": 443,
|
|
||||||
"active": true,
|
|
||||||
"created_at": "2024-01-28T12:30:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GET /api/v1/agents/{id}/proxy/{tunnel_id}/*
|
|
||||||
HTTP-Reverse-Proxy durch Tunnel.
|
|
||||||
|
|
||||||
**Beispiel:**
|
|
||||||
```
|
|
||||||
GET /api/v1/agents/abc123/proxy/tunnel789/
|
|
||||||
→ Leitet weiter an OPNsense WebUI (127.0.0.1:443) über Tunnel
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GET /api/v1/hub/status
|
|
||||||
WebSocket Hub Status.
|
|
||||||
|
|
||||||
**Response (200):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "running",
|
|
||||||
"connected_agents": ["abc123", "def456"],
|
|
||||||
"stats": {
|
|
||||||
"connected_agents": 2,
|
|
||||||
"active_tunnels": 3
|
|
||||||
},
|
|
||||||
"timestamp": "2024-01-28T12:30:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### WebSocket Endpoint
|
|
||||||
|
|
||||||
#### GET /api/v1/agent/ws
|
|
||||||
WebSocket-Upgrade für Agents.
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
- `agent_id`: Agent-ID (aus Registrierung)
|
|
||||||
- `api_key`: API-Key für Authentifizierung
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```
|
|
||||||
wss://192.168.85.13:8443/api/v1/agent/ws?agent_id=abc123&api_key=xxx
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tunnel-Datenformat
|
|
||||||
|
|
||||||
### Binary WebSocket Messages
|
|
||||||
TCP-Tunnel-Daten werden als Binary Messages übertragen:
|
|
||||||
|
|
||||||
**Format:** `[tunnel_id_length:1][tunnel_id][data]`
|
|
||||||
|
|
||||||
- Byte 0: Länge der Tunnel-ID (max 255)
|
|
||||||
- Bytes 1-N: Tunnel-ID als UTF-8 String
|
|
||||||
- Bytes N+1-Ende: TCP-Payload-Daten
|
|
||||||
|
|
||||||
### Beispiel-Usage
|
|
||||||
|
|
||||||
1. **Tunnel öffnen:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://backend:8443/api/v1/agents/abc123/tunnel \
|
|
||||||
-H "X-API-Key: xxx" \
|
|
||||||
-d '{"target_host":"127.0.0.1","target_port":443}'
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Über Tunnel auf OPNsense WebUI zugreifen:**
|
|
||||||
```
|
|
||||||
# Backend allokiert Port 12345 für Tunnel
|
|
||||||
# Browser kann jetzt zugreifen über:
|
|
||||||
https://192.168.85.13:12345/
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Oder über HTTP-Proxy:**
|
|
||||||
```
|
|
||||||
https://192.168.85.13:8443/api/v1/agents/abc123/proxy/tunnel789/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technische Details
|
## Technische Details
|
||||||
|
|
||||||
### CGO-frei
|
- **CGO-frei**: `CGO_ENABLED=0`, pure-Go SQLite (modernc.org/sqlite)
|
||||||
- Beide Komponenten kompilieren mit `CGO_ENABLED=0`
|
- **Cross-Compilation**: Backend linux/amd64, Agent freebsd/amd64
|
||||||
- Agent läuft auf FreeBSD/amd64 (OPNsense)
|
- **TLS**: Self-signed Zertifikate (automatisch generiert)
|
||||||
- Backend läuft auf Linux/amd64
|
- **WebSocket**: gorilla/websocket, Binary Messages fuer Tunnel-Daten
|
||||||
- gorilla/websocket ist pure Go, kein CGO erforderlich
|
- **Persistente Agent-ID**: Ueberlebt Agent- und Backend-Neustarts
|
||||||
|
- **FreeBSD-kompatibel**: Volle Pfade (/usr/bin/netstat etc.), daemon statt nohup
|
||||||
|
|
||||||
### Sicherheit
|
## Hinweise
|
||||||
- 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
|
- Backend braucht **kein root** — laeuft als normaler User auf Port 8443
|
||||||
- Binary WebSocket Messages für Tunnel-Daten (kein Base64-Overhead)
|
- Agent braucht **root** auf OPNsense (fuer Hardware-/Service-Daten)
|
||||||
- Connection-Pooling für gleichzeitige Tunnel
|
- OPNsense WebGUI typischerweise auf **Port 4444** (nicht 443)
|
||||||
- Effiziente Port-Allokation für Proxies
|
- FreeBSD csh hat kein `$()` — Agent verwendet `/bin/sh -c` fuer Commands
|
||||||
- Heartbeat läuft weiterhin parallel (für Monitoring auch ohne WS)
|
- Beim Backend-Deploy: **alte Prozesse killen** bevor neuer startet (sonst "bind: address already in use")
|
||||||
|
|
||||||
### 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
|
||||||
|
|
||||||
|
|||||||
@ -28,13 +28,17 @@ func (h *Handler) HandleCommand(msg Message) {
|
|||||||
case "exec":
|
case "exec":
|
||||||
response = h.handleExec(msg)
|
response = h.handleExec(msg)
|
||||||
case "tunnel_open":
|
case "tunnel_open":
|
||||||
response = h.handleTunnelOpen(msg)
|
// Legacy: wird nicht mehr verwendet, aber fuer Kompatibilitaet
|
||||||
|
response.Status = "ok"
|
||||||
|
response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"}
|
||||||
|
case "tunnel_connect":
|
||||||
|
response = h.handleTunnelConnect(msg)
|
||||||
|
case "tunnel_disconnect":
|
||||||
|
h.handleTunnelDisconnect(msg)
|
||||||
|
return // Keine Response noetig
|
||||||
case "tunnel_close":
|
case "tunnel_close":
|
||||||
response = h.handleTunnelClose(msg)
|
// Legacy: alle Sessions fuer diesen Tunnel schliessen
|
||||||
case "tunnel_data":
|
response.Status = "ok"
|
||||||
// Tunnel-Daten werden als Binary-Messages gesendet, nicht als JSON
|
|
||||||
h.handleTunnelData(msg)
|
|
||||||
return
|
|
||||||
default:
|
default:
|
||||||
response.Status = "error"
|
response.Status = "error"
|
||||||
response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command)
|
response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command)
|
||||||
@ -51,7 +55,6 @@ func (h *Handler) handleExec(msg Message) Message {
|
|||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameter extrahieren
|
|
||||||
command, ok := msg.Params["command"].(string)
|
command, ok := msg.Params["command"].(string)
|
||||||
if !ok || command == "" {
|
if !ok || command == "" {
|
||||||
response.Status = "error"
|
response.Status = "error"
|
||||||
@ -59,7 +62,7 @@ func (h *Handler) handleExec(msg Message) Message {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutSec := 30 // Default
|
timeoutSec := 30
|
||||||
if t, ok := msg.Params["timeout"]; ok {
|
if t, ok := msg.Params["timeout"]; ok {
|
||||||
if ts, ok := t.(float64); ok {
|
if ts, ok := t.(float64); ok {
|
||||||
timeoutSec = int(ts)
|
timeoutSec = int(ts)
|
||||||
@ -70,11 +73,9 @@ func (h *Handler) handleExec(msg Message) Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command ausführen mit Timeout
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// FreeBSD/OPNsense nutzt /bin/sh
|
|
||||||
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
@ -94,123 +95,58 @@ func (h *Handler) handleExec(msg Message) Message {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleTunnelOpen(msg Message) Message {
|
func (h *Handler) handleTunnelConnect(msg Message) Message {
|
||||||
response := Message{
|
response := Message{
|
||||||
Type: "response",
|
Type: "response",
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameter extrahieren
|
sessionID, _ := msg.Params["session_id"].(string)
|
||||||
targetHost, ok := msg.Params["target_host"].(string)
|
tunnelID, _ := msg.Params["tunnel_id"].(string)
|
||||||
if !ok || targetHost == "" {
|
targetHost, _ := msg.Params["target_host"].(string)
|
||||||
response.Status = "error"
|
targetPortFloat, _ := msg.Params["target_port"].(float64)
|
||||||
response.Error = "Parameter 'target_host' erforderlich"
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPortFloat, ok := msg.Params["target_port"].(float64)
|
|
||||||
if !ok {
|
|
||||||
response.Status = "error"
|
|
||||||
response.Error = "Parameter 'target_port' erforderlich"
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
targetPort := int(targetPortFloat)
|
targetPort := int(targetPortFloat)
|
||||||
|
|
||||||
// Tunnel-ID vom Backend uebernehmen (falls vorhanden)
|
if sessionID == "" || targetHost == "" || targetPort <= 0 {
|
||||||
backendTunnelID, _ := msg.Params["tunnel_id"].(string)
|
|
||||||
|
|
||||||
// Tunnel oeffnen
|
|
||||||
tunnelID, err := h.client.tunnelManager.OpenTunnel(targetHost, targetPort, backendTunnelID)
|
|
||||||
if err != nil {
|
|
||||||
response.Status = "error"
|
response.Status = "error"
|
||||||
response.Error = fmt.Sprintf("Tunnel öffnen fehlgeschlagen: %v", err)
|
response.Error = "session_id, target_host und target_port erforderlich"
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.client.tunnelManager.ConnectSession(sessionID, tunnelID, targetHost, targetPort); err != nil {
|
||||||
|
response.Status = "error"
|
||||||
|
response.Error = fmt.Sprintf("Session-Connect fehlgeschlagen: %v", err)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Status = "ok"
|
response.Status = "ok"
|
||||||
response.Data = map[string]interface{}{
|
response.Data = map[string]interface{}{
|
||||||
"tunnel_id": tunnelID,
|
"session_id": sessionID,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Tunnel geöffnet: %s -> %s:%d", tunnelID, targetHost, targetPort)
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleTunnelClose(msg Message) Message {
|
func (h *Handler) handleTunnelDisconnect(msg Message) {
|
||||||
response := Message{
|
sessionID, _ := msg.Params["session_id"].(string)
|
||||||
Type: "response",
|
if sessionID == "" {
|
||||||
ID: msg.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tunnel-ID extrahieren
|
|
||||||
tunnelID, ok := msg.Params["tunnel_id"].(string)
|
|
||||||
if !ok || tunnelID == "" {
|
|
||||||
response.Status = "error"
|
|
||||||
response.Error = "Parameter 'tunnel_id' erforderlich"
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tunnel schließen
|
|
||||||
if err := h.client.tunnelManager.CloseTunnel(tunnelID); err != nil {
|
|
||||||
response.Status = "error"
|
|
||||||
response.Error = fmt.Sprintf("Tunnel schließen fehlgeschlagen: %v", err)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Status = "ok"
|
|
||||||
log.Printf("Tunnel geschlossen: %s", tunnelID)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) handleTunnelData(msg Message) {
|
|
||||||
// Diese Funktion wird für JSON-basierte Tunnel-Daten verwendet
|
|
||||||
// In der Praxis verwenden wir Binary WebSocket Messages für bessere Performance
|
|
||||||
tunnelID, ok := msg.Params["tunnel_id"].(string)
|
|
||||||
if !ok {
|
|
||||||
log.Printf("Tunnel-Daten ohne tunnel_id erhalten")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dataStr, ok := msg.Params["data"].(string)
|
h.client.tunnelManager.DisconnectSession(sessionID)
|
||||||
if !ok {
|
|
||||||
log.Printf("Tunnel-Daten ohne data erhalten")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base64-Dekodierung würde hier stattfinden
|
|
||||||
// Aber wir verwenden Binary Messages, daher ist das nur ein Fallback
|
|
||||||
log.Printf("Tunnel-Daten via JSON erhalten für %s (Länge: %d)", tunnelID, len(dataStr))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTunnelData sendet Tunnel-Daten als Binary-Message zurück zum Backend
|
// SendSessionData schickt Daten als Binary-Message zurueck zum Backend
|
||||||
func (h *Handler) SendTunnelData(tunnelID string, data []byte) error {
|
func (h *Handler) SendSessionData(sessionID string, data []byte) error {
|
||||||
// Format: [tunnel_id_length:1][tunnel_id][data]
|
idBytes := []byte(sessionID)
|
||||||
tunnelIDBytes := []byte(tunnelID)
|
if len(idBytes) > 255 {
|
||||||
if len(tunnelIDBytes) > 255 {
|
return fmt.Errorf("session_id zu lang")
|
||||||
return fmt.Errorf("tunnel_id zu lang")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message := make([]byte, 1+len(tunnelIDBytes)+len(data))
|
message := make([]byte, 1+len(idBytes)+len(data))
|
||||||
message[0] = byte(len(tunnelIDBytes))
|
message[0] = byte(len(idBytes))
|
||||||
copy(message[1:], tunnelIDBytes)
|
copy(message[1:], idBytes)
|
||||||
copy(message[1+len(tunnelIDBytes):], data)
|
copy(message[1+len(idBytes):], data)
|
||||||
|
|
||||||
return h.client.SendBinaryData(message)
|
return h.client.SendBinaryData(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseTunnelData parst eingehende Binary-Messages mit Tunnel-Daten
|
|
||||||
func ParseTunnelData(data []byte) (tunnelID string, payload []byte, err error) {
|
|
||||||
if len(data) < 1 {
|
|
||||||
return "", nil, fmt.Errorf("Daten zu kurz")
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnelIDLen := int(data[0])
|
|
||||||
if len(data) < 1+tunnelIDLen {
|
|
||||||
return "", nil, fmt.Errorf("Tunnel-ID zu kurz")
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnelID = string(data[1 : 1+tunnelIDLen])
|
|
||||||
payload = data[1+tunnelIDLen:]
|
|
||||||
|
|
||||||
return tunnelID, payload, nil
|
|
||||||
}
|
|
||||||
@ -11,202 +11,168 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tunnel struct {
|
// TunnelSession repraesentiert eine einzelne TCP-Verbindung zu einem Ziel
|
||||||
|
type TunnelSession struct {
|
||||||
ID string
|
ID string
|
||||||
|
TunnelID string
|
||||||
TargetHost string
|
TargetHost string
|
||||||
TargetPort int
|
TargetPort int
|
||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
Active bool
|
Active bool
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TunnelManager verwaltet alle Sessions auf Agent-Seite
|
||||||
type TunnelManager struct {
|
type TunnelManager struct {
|
||||||
client *Client
|
client *Client
|
||||||
tunnels map[string]*Tunnel
|
sessions map[string]*TunnelSession
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTunnelManager(client *Client) *TunnelManager {
|
func NewTunnelManager(client *Client) *TunnelManager {
|
||||||
return &TunnelManager{
|
return &TunnelManager{
|
||||||
client: client,
|
client: client,
|
||||||
tunnels: make(map[string]*Tunnel),
|
sessions: make(map[string]*TunnelSession),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) OpenTunnel(targetHost string, targetPort int, tunnelID ...string) (string, error) {
|
// ConnectSession oeffnet eine neue TCP-Verbindung fuer eine Session
|
||||||
// Tunnel-ID vom Backend uebernehmen oder selbst generieren
|
func (tm *TunnelManager) ConnectSession(sessionID, tunnelID, targetHost string, targetPort int) error {
|
||||||
id := ""
|
|
||||||
if len(tunnelID) > 0 && tunnelID[0] != "" {
|
|
||||||
id = tunnelID[0]
|
|
||||||
} else {
|
|
||||||
id = tm.generateTunnelID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verbindung zum Ziel aufbauen
|
|
||||||
target := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
target := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
||||||
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
|
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Verbindung zu %s fehlgeschlagen: %w", target, err)
|
return fmt.Errorf("Verbindung zu %s fehlgeschlagen: %w", target, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tunnel erstellen
|
session := &TunnelSession{
|
||||||
tunnel := &Tunnel{
|
ID: sessionID,
|
||||||
ID: id,
|
TunnelID: tunnelID,
|
||||||
TargetHost: targetHost,
|
TargetHost: targetHost,
|
||||||
TargetPort: targetPort,
|
TargetPort: targetPort,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
Active: true,
|
Active: true,
|
||||||
CreatedAt: time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tunnel registrieren
|
|
||||||
tm.mutex.Lock()
|
tm.mutex.Lock()
|
||||||
tm.tunnels[id] = tunnel
|
tm.sessions[sessionID] = session
|
||||||
tm.mutex.Unlock()
|
tm.mutex.Unlock()
|
||||||
|
|
||||||
// Data-Forwarding starten
|
// Ziel -> Backend Forwarding starten
|
||||||
go tm.forwardData(tunnel)
|
go tm.forwardFromTarget(session)
|
||||||
|
|
||||||
return id, nil
|
log.Printf("Session %s: Verbindung zu %s hergestellt", sessionID, target)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) CloseTunnel(tunnelID string) error {
|
// DisconnectSession schliesst eine Session
|
||||||
|
func (tm *TunnelManager) DisconnectSession(sessionID string) {
|
||||||
tm.mutex.Lock()
|
tm.mutex.Lock()
|
||||||
tunnel, exists := tm.tunnels[tunnelID]
|
session, exists := tm.sessions[sessionID]
|
||||||
if !exists {
|
if !exists {
|
||||||
tm.mutex.Unlock()
|
tm.mutex.Unlock()
|
||||||
return fmt.Errorf("Tunnel %s nicht gefunden", tunnelID)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnel.Active = false
|
session.Active = false
|
||||||
delete(tm.tunnels, tunnelID)
|
delete(tm.sessions, sessionID)
|
||||||
tm.mutex.Unlock()
|
tm.mutex.Unlock()
|
||||||
|
|
||||||
// Verbindung schließen
|
if session.Conn != nil {
|
||||||
if tunnel.Conn != nil {
|
session.Conn.Close()
|
||||||
tunnel.Conn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
log.Printf("Session %s: geschlossen", sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) GetActiveTunnels() []string {
|
// SendDataToSession schreibt Daten in die Ziel-Verbindung einer Session
|
||||||
|
func (tm *TunnelManager) SendDataToSession(sessionID string, data []byte) error {
|
||||||
tm.mutex.RLock()
|
tm.mutex.RLock()
|
||||||
defer tm.mutex.RUnlock()
|
session, exists := tm.sessions[sessionID]
|
||||||
|
|
||||||
tunnels := make([]string, 0, len(tm.tunnels))
|
|
||||||
for id := range tm.tunnels {
|
|
||||||
tunnels = append(tunnels, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tunnels
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tm *TunnelManager) SendDataToTunnel(tunnelID string, data []byte) error {
|
|
||||||
tm.mutex.RLock()
|
|
||||||
tunnel, exists := tm.tunnels[tunnelID]
|
|
||||||
tm.mutex.RUnlock()
|
tm.mutex.RUnlock()
|
||||||
|
|
||||||
if !exists || !tunnel.Active {
|
if !exists || !session.Active {
|
||||||
return fmt.Errorf("Tunnel %s nicht aktiv", tunnelID)
|
return fmt.Errorf("Session %s nicht aktiv", sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tunnel.Conn == nil {
|
_, err := session.Conn.Write(data)
|
||||||
return fmt.Errorf("Tunnel %s hat keine Verbindung", tunnelID)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := tunnel.Conn.Write(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Bei Fehler Tunnel schließen
|
tm.DisconnectSession(sessionID)
|
||||||
tm.CloseTunnel(tunnelID)
|
return fmt.Errorf("Session %s: Write fehlgeschlagen: %w", sessionID, err)
|
||||||
return fmt.Errorf("Daten schreiben fehlgeschlagen: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) forwardData(tunnel *Tunnel) {
|
// forwardFromTarget liest Daten vom Ziel und schickt sie als Binary-Message ans Backend
|
||||||
|
func (tm *TunnelManager) forwardFromTarget(session *TunnelSession) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if tunnel.Active {
|
if session.Active {
|
||||||
tm.CloseTunnel(tunnel.ID)
|
tm.DisconnectSession(session.ID)
|
||||||
}
|
}
|
||||||
|
log.Printf("Session %s: Forwarding beendet", session.ID)
|
||||||
// Event senden: Tunnel geschlossen
|
|
||||||
event := Message{
|
|
||||||
Type: "event",
|
|
||||||
Event: "tunnel_closed",
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"tunnel_id": tunnel.ID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
tm.client.SendMessage(event)
|
|
||||||
|
|
||||||
log.Printf("Tunnel %s: Data-Forwarding beendet", tunnel.ID)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
buffer := make([]byte, 32768) // 32KB Buffer
|
buffer := make([]byte, 32768)
|
||||||
|
|
||||||
for tunnel.Active {
|
for session.Active {
|
||||||
// Read-Timeout setzen um regelmäßig Active-Status zu prüfen
|
session.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||||
tunnel.Conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
||||||
|
|
||||||
n, err := tunnel.Conn.Read(buffer)
|
n, err := session.Conn.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
// Timeout ist OK, einfach weiter versuchen
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
log.Printf("Tunnel %s: Ziel-Verbindung geschlossen", tunnel.ID)
|
log.Printf("Session %s: Ziel-Verbindung geschlossen", session.ID)
|
||||||
} else {
|
} else if session.Active {
|
||||||
log.Printf("Tunnel %s: Read-Fehler: %v", tunnel.ID, err)
|
log.Printf("Session %s: Read-Fehler: %v", session.ID, err)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
// Daten über WebSocket an Backend weiterleiten
|
if err := tm.client.messageHandler.SendSessionData(session.ID, buffer[:n]); err != nil {
|
||||||
data := buffer[:n]
|
log.Printf("Session %s: WebSocket senden fehlgeschlagen: %v", session.ID, err)
|
||||||
if err := tm.client.messageHandler.SendTunnelData(tunnel.ID, data); err != nil {
|
|
||||||
log.Printf("Tunnel %s: WebSocket senden fehlgeschlagen: %v", tunnel.ID, err)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) generateTunnelID() string {
|
// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Backend (Proxy -> Ziel)
|
||||||
|
func (tm *TunnelManager) ProcessBinaryMessage(data []byte) error {
|
||||||
|
sessionID, payload, err := ParseBinaryMessage(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Binary-Message parsen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tm.SendDataToSession(sessionID, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveSessions gibt die Anzahl aktiver Sessions zurueck
|
||||||
|
func (tm *TunnelManager) GetActiveSessions() int {
|
||||||
|
tm.mutex.RLock()
|
||||||
|
defer tm.mutex.RUnlock()
|
||||||
|
return len(tm.sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TunnelManager) generateID() string {
|
||||||
bytes := make([]byte, 8)
|
bytes := make([]byte, 8)
|
||||||
rand.Read(bytes)
|
rand.Read(bytes)
|
||||||
return hex.EncodeToString(bytes)
|
return hex.EncodeToString(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Backend
|
// ParseBinaryMessage parst das Binary-Format: [id_len:1][id][data]
|
||||||
func (tm *TunnelManager) ProcessBinaryMessage(data []byte) error {
|
func ParseBinaryMessage(data []byte) (id string, payload []byte, err error) {
|
||||||
tunnelID, payload, err := ParseTunnelData(data)
|
if len(data) < 1 {
|
||||||
if err != nil {
|
return "", nil, fmt.Errorf("Daten zu kurz")
|
||||||
return fmt.Errorf("Binary-Message parsen: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tm.SendDataToTunnel(tunnelID, payload)
|
idLen := int(data[0])
|
||||||
}
|
if len(data) < 1+idLen {
|
||||||
|
return "", nil, fmt.Errorf("ID zu kurz")
|
||||||
// CleanupInactiveTunnels schließt inaktive Tunnel (Cleanup-Task)
|
}
|
||||||
func (tm *TunnelManager) CleanupInactiveTunnels() {
|
|
||||||
tm.mutex.Lock()
|
id = string(data[1 : 1+idLen])
|
||||||
defer tm.mutex.Unlock()
|
payload = data[1+idLen:]
|
||||||
|
return id, payload, nil
|
||||||
now := time.Now()
|
|
||||||
for id, tunnel := range tm.tunnels {
|
|
||||||
// Tunnel die länger als 1 Stunde inaktiv sind, schließen
|
|
||||||
if !tunnel.Active || now.Sub(tunnel.CreatedAt) > time.Hour {
|
|
||||||
tunnel.Active = false
|
|
||||||
if tunnel.Conn != nil {
|
|
||||||
tunnel.Conn.Close()
|
|
||||||
}
|
|
||||||
delete(tm.tunnels, id)
|
|
||||||
log.Printf("Inaktiver Tunnel %s bereinigt", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@ -20,6 +21,7 @@ type Connection struct {
|
|||||||
Send chan []byte
|
Send chan []byte
|
||||||
BinarySend chan []byte
|
BinarySend chan []byte
|
||||||
Hub *Hub
|
Hub *Hub
|
||||||
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
@ -77,7 +79,6 @@ func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
|||||||
func (c *Connection) readPump() {
|
func (c *Connection) readPump() {
|
||||||
defer func() {
|
defer func() {
|
||||||
c.Hub.UnregisterAgent(c.AgentID, c)
|
c.Hub.UnregisterAgent(c.AgentID, c)
|
||||||
c.Conn.Close()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Read-Timeouts setzen
|
// Read-Timeouts setzen
|
||||||
@ -108,7 +109,7 @@ func (c *Connection) writePump() {
|
|||||||
ticker := time.NewTicker(54 * time.Second) // Ping alle 54s
|
ticker := time.NewTicker(54 * time.Second) // Ping alle 54s
|
||||||
defer func() {
|
defer func() {
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
c.Conn.Close()
|
c.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -148,6 +149,9 @@ func (c *Connection) writePump() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connection) Close() {
|
func (c *Connection) Close() {
|
||||||
|
c.closeOnce.Do(func() {
|
||||||
close(c.Send)
|
close(c.Send)
|
||||||
close(c.BinarySend)
|
close(c.BinarySend)
|
||||||
|
c.Conn.Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package ws
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -81,7 +82,7 @@ func (h *Hub) unregisterAgent(agent *AgentConnection) {
|
|||||||
|
|
||||||
if existing, ok := h.agents[agent.AgentID]; ok && existing.Connection == agent.Connection {
|
if existing, ok := h.agents[agent.AgentID]; ok && existing.Connection == agent.Connection {
|
||||||
delete(h.agents, agent.AgentID)
|
delete(h.agents, agent.AgentID)
|
||||||
agent.Connection.Close()
|
existing.Connection.Close()
|
||||||
log.Printf("Agent %s: WebSocket-Verbindung entfernt", agent.AgentID)
|
log.Printf("Agent %s: WebSocket-Verbindung entfernt", agent.AgentID)
|
||||||
|
|
||||||
// Alle Tunnel für diesen Agent schließen
|
// Alle Tunnel für diesen Agent schließen
|
||||||
@ -119,7 +120,7 @@ func (h *Hub) SendToAgent(agentID string, message []byte) error {
|
|||||||
select {
|
select {
|
||||||
case agent.Connection.Send <- message:
|
case agent.Connection.Send <- message:
|
||||||
return nil
|
return nil
|
||||||
default:
|
case <-time.After(5 * time.Second):
|
||||||
return ErrAgentSendBlocked
|
return ErrAgentSendBlocked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,7 +139,7 @@ func (h *Hub) SendBinaryToAgent(agentID string, data []byte) error {
|
|||||||
select {
|
select {
|
||||||
case agent.Connection.BinarySend <- data:
|
case agent.Connection.BinarySend <- data:
|
||||||
return nil
|
return nil
|
||||||
default:
|
case <-time.After(5 * time.Second):
|
||||||
return ErrAgentSendBlocked
|
return ErrAgentSendBlocked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,8 +186,8 @@ func (h *Hub) cleanupInactiveAgents() {
|
|||||||
for agentID, agent := range h.agents {
|
for agentID, agent := range h.agents {
|
||||||
if now.Sub(agent.LastSeen) > timeout {
|
if now.Sub(agent.LastSeen) > timeout {
|
||||||
log.Printf("Agent %s: Timeout, trenne Verbindung", agentID)
|
log.Printf("Agent %s: Timeout, trenne Verbindung", agentID)
|
||||||
agent.Connection.Close()
|
|
||||||
delete(h.agents, agentID)
|
delete(h.agents, agentID)
|
||||||
|
agent.Connection.Close()
|
||||||
h.tunnelManager.CloseAgentTunnels(agentID)
|
h.tunnelManager.CloseAgentTunnels(agentID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,6 +234,23 @@ func (h *Hub) ProcessAgentMessage(agentID string, messageType int, data []byte)
|
|||||||
return h.tunnelManager.ProcessBinaryMessage(agentID, data)
|
return h.tunnelManager.ProcessBinaryMessage(agentID, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text Messages sind JSON-Commands/Responses/Events
|
// Text Messages sind JSON-Responses/Events vom Agent
|
||||||
|
var msg struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data map[string]interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
|
return nil // Ignorieren wenn nicht parsebar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tunnel-Connect Responses verarbeiten
|
||||||
|
if msg.Type == "response" && msg.Status == "ok" && msg.Data != nil {
|
||||||
|
if sessionID, ok := msg.Data["session_id"].(string); ok {
|
||||||
|
h.tunnelManager.ConfirmSession(sessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -12,6 +13,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TunnelSession repraesentiert eine einzelne TCP-Verbindung durch einen Tunnel
|
||||||
|
type TunnelSession struct {
|
||||||
|
ID string
|
||||||
|
TunnelID string
|
||||||
|
ProxyConn net.Conn
|
||||||
|
Active bool
|
||||||
|
Ready chan struct{} // Wird geschlossen wenn Agent die Session bestaetigt hat
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackendTunnel repraesentiert einen offenen Tunnel (Proxy-Port -> Agent -> Ziel)
|
||||||
type BackendTunnel struct {
|
type BackendTunnel struct {
|
||||||
ID string
|
ID string
|
||||||
AgentID string
|
AgentID string
|
||||||
@ -21,17 +33,20 @@ type BackendTunnel struct {
|
|||||||
Listener net.Listener
|
Listener net.Listener
|
||||||
Active bool
|
Active bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
// Aktive Proxy-Verbindungen (fuer Rueckkanal)
|
Sessions map[string]*TunnelSession
|
||||||
proxyConn net.Conn
|
sessMutex sync.RWMutex
|
||||||
proxyMutex sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TunnelManager verwaltet alle Tunnel und Sessions
|
||||||
type TunnelManager struct {
|
type TunnelManager struct {
|
||||||
hub *Hub
|
hub *Hub
|
||||||
tunnels map[string]*BackendTunnel
|
tunnels map[string]*BackendTunnel
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
|
||||||
// Port-Range für dynamische Tunnel-Proxies
|
// Session-Index fuer schnellen Lookup bei Binary-Messages
|
||||||
|
sessions map[string]*TunnelSession
|
||||||
|
sessMutex sync.RWMutex
|
||||||
|
|
||||||
portStart int
|
portStart int
|
||||||
portEnd int
|
portEnd int
|
||||||
usedPorts map[int]bool
|
usedPorts map[int]bool
|
||||||
@ -41,6 +56,7 @@ func NewTunnelManager(hub *Hub) *TunnelManager {
|
|||||||
return &TunnelManager{
|
return &TunnelManager{
|
||||||
hub: hub,
|
hub: hub,
|
||||||
tunnels: make(map[string]*BackendTunnel),
|
tunnels: make(map[string]*BackendTunnel),
|
||||||
|
sessions: make(map[string]*TunnelSession),
|
||||||
portStart: 10000,
|
portStart: 10000,
|
||||||
portEnd: 20000,
|
portEnd: 20000,
|
||||||
usedPorts: make(map[int]bool),
|
usedPorts: make(map[int]bool),
|
||||||
@ -48,28 +64,23 @@ func NewTunnelManager(hub *Hub) *TunnelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) OpenTunnel(agentID, targetHost string, targetPort int) (*BackendTunnel, error) {
|
func (tm *TunnelManager) OpenTunnel(agentID, targetHost string, targetPort int) (*BackendTunnel, error) {
|
||||||
// Prüfen ob Agent verbunden ist
|
|
||||||
if !tm.hub.IsAgentConnected(agentID) {
|
if !tm.hub.IsAgentConnected(agentID) {
|
||||||
return nil, ErrAgentNotConnected
|
return nil, ErrAgentNotConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tunnel-ID generieren
|
tunnelID := tm.generateID()
|
||||||
tunnelID := tm.generateTunnelID()
|
|
||||||
|
|
||||||
// Freien Port finden
|
|
||||||
proxyPort, err := tm.allocatePort()
|
proxyPort, err := tm.allocatePort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Proxy-Port allokieren fehlgeschlagen: %w", err)
|
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))
|
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", proxyPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tm.releasePort(proxyPort)
|
tm.releasePort(proxyPort)
|
||||||
return nil, fmt.Errorf("Proxy-Listener erstellen fehlgeschlagen: %w", err)
|
return nil, fmt.Errorf("Proxy-Listener erstellen fehlgeschlagen: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tunnel-Objekt erstellen
|
|
||||||
tunnel := &BackendTunnel{
|
tunnel := &BackendTunnel{
|
||||||
ID: tunnelID,
|
ID: tunnelID,
|
||||||
AgentID: agentID,
|
AgentID: agentID,
|
||||||
@ -79,35 +90,17 @@ func (tm *TunnelManager) OpenTunnel(agentID, targetHost string, targetPort int)
|
|||||||
Listener: listener,
|
Listener: listener,
|
||||||
Active: true,
|
Active: true,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
|
Sessions: make(map[string]*TunnelSession),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tunnel registrieren
|
|
||||||
tm.mutex.Lock()
|
tm.mutex.Lock()
|
||||||
tm.tunnels[tunnelID] = tunnel
|
tm.tunnels[tunnelID] = tunnel
|
||||||
tm.mutex.Unlock()
|
tm.mutex.Unlock()
|
||||||
|
|
||||||
// Proxy-Server starten
|
// Proxy-Accept-Loop starten
|
||||||
go tm.runTunnelProxy(tunnel)
|
go tm.runProxyAccept(tunnel)
|
||||||
|
|
||||||
// Command an Agent senden — tunnel_id vorgeben damit Backend und Agent dieselbe ID nutzen
|
log.Printf("Tunnel %s geoeffnet: Agent %s -> %s:%d (Proxy: :%d)",
|
||||||
cmd := map[string]interface{}{
|
|
||||||
"type": "command",
|
|
||||||
"id": tm.generateTunnelID(),
|
|
||||||
"command": "tunnel_open",
|
|
||||||
"params": map[string]interface{}{
|
|
||||||
"tunnel_id": tunnelID,
|
|
||||||
"target_host": targetHost,
|
|
||||||
"target_port": targetPort,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdJSON, _ := json.Marshal(cmd)
|
|
||||||
if err := tm.hub.SendToAgent(agentID, cmdJSON); err != nil {
|
|
||||||
tm.CloseTunnel(tunnelID)
|
|
||||||
return nil, fmt.Errorf("Tunnel-Command senden fehlgeschlagen: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Tunnel %s geöffnet: Agent %s -> %s:%d (Proxy: :%d)",
|
|
||||||
tunnelID, agentID, targetHost, targetPort, proxyPort)
|
tunnelID, agentID, targetHost, targetPort, proxyPort)
|
||||||
|
|
||||||
return tunnel, nil
|
return tunnel, nil
|
||||||
@ -125,29 +118,31 @@ func (tm *TunnelManager) CloseTunnel(tunnelID string) error {
|
|||||||
delete(tm.tunnels, tunnelID)
|
delete(tm.tunnels, tunnelID)
|
||||||
tm.mutex.Unlock()
|
tm.mutex.Unlock()
|
||||||
|
|
||||||
// Listener schließen
|
// Listener schliessen
|
||||||
if tunnel.Listener != nil {
|
if tunnel.Listener != nil {
|
||||||
tunnel.Listener.Close()
|
tunnel.Listener.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Port freigeben
|
// Alle Sessions schliessen
|
||||||
|
tunnel.sessMutex.Lock()
|
||||||
|
for sessID, sess := range tunnel.Sessions {
|
||||||
|
sess.Active = false
|
||||||
|
if sess.ProxyConn != nil {
|
||||||
|
sess.ProxyConn.Close()
|
||||||
|
}
|
||||||
|
// Session aus globalem Index entfernen
|
||||||
|
tm.sessMutex.Lock()
|
||||||
|
delete(tm.sessions, sessID)
|
||||||
|
tm.sessMutex.Unlock()
|
||||||
|
|
||||||
|
// Agent informieren
|
||||||
|
tm.sendSessionDisconnect(tunnel.AgentID, sessID)
|
||||||
|
}
|
||||||
|
tunnel.Sessions = make(map[string]*TunnelSession)
|
||||||
|
tunnel.sessMutex.Unlock()
|
||||||
|
|
||||||
tm.releasePort(tunnel.ProxyPort)
|
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)
|
log.Printf("Tunnel %s geschlossen", tunnelID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -155,7 +150,6 @@ func (tm *TunnelManager) CloseTunnel(tunnelID string) error {
|
|||||||
func (tm *TunnelManager) GetTunnel(tunnelID string) (*BackendTunnel, bool) {
|
func (tm *TunnelManager) GetTunnel(tunnelID string) (*BackendTunnel, bool) {
|
||||||
tm.mutex.RLock()
|
tm.mutex.RLock()
|
||||||
defer tm.mutex.RUnlock()
|
defer tm.mutex.RUnlock()
|
||||||
|
|
||||||
tunnel, exists := tm.tunnels[tunnelID]
|
tunnel, exists := tm.tunnels[tunnelID]
|
||||||
return tunnel, exists
|
return tunnel, exists
|
||||||
}
|
}
|
||||||
@ -170,14 +164,12 @@ func (tm *TunnelManager) GetAgentTunnels(agentID string) []*BackendTunnel {
|
|||||||
tunnels = append(tunnels, tunnel)
|
tunnels = append(tunnels, tunnel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tunnels
|
return tunnels
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) GetActiveTunnelCount() int {
|
func (tm *TunnelManager) GetActiveTunnelCount() int {
|
||||||
tm.mutex.RLock()
|
tm.mutex.RLock()
|
||||||
defer tm.mutex.RUnlock()
|
defer tm.mutex.RUnlock()
|
||||||
|
|
||||||
return len(tm.tunnels)
|
return len(tm.tunnels)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,14 +188,15 @@ func (tm *TunnelManager) CloseAgentTunnels(agentID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) runTunnelProxy(tunnel *BackendTunnel) {
|
// runProxyAccept nimmt eingehende TCP-Verbindungen am Proxy-Port an
|
||||||
|
func (tm *TunnelManager) runProxyAccept(tunnel *BackendTunnel) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if tunnel.Active {
|
if tunnel.Active {
|
||||||
tm.CloseTunnel(tunnel.ID)
|
tm.CloseTunnel(tunnel.ID)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Printf("Tunnel %s: Proxy läuft auf Port %d", tunnel.ID, tunnel.ProxyPort)
|
log.Printf("Tunnel %s: Proxy lauscht auf Port %d", tunnel.ID, tunnel.ProxyPort)
|
||||||
|
|
||||||
for tunnel.Active {
|
for tunnel.Active {
|
||||||
conn, err := tunnel.Listener.Accept()
|
conn, err := tunnel.Listener.Accept()
|
||||||
@ -214,105 +207,190 @@ func (tm *TunnelManager) runTunnelProxy(tunnel *BackendTunnel) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jede eingehende Verbindung in eigener Goroutine behandeln
|
// Neue Session fuer diese TCP-Verbindung
|
||||||
go tm.handleProxyConnection(tunnel, conn)
|
go tm.handleNewSession(tunnel, conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) handleProxyConnection(tunnel *BackendTunnel, conn net.Conn) {
|
// handleNewSession erstellt eine neue Session fuer eine eingehende Proxy-Verbindung
|
||||||
defer conn.Close()
|
func (tm *TunnelManager) handleNewSession(tunnel *BackendTunnel, proxyConn net.Conn) {
|
||||||
|
sessionID := tm.generateID()
|
||||||
|
|
||||||
log.Printf("Tunnel %s: Proxy-Verbindung von %s", tunnel.ID, conn.RemoteAddr())
|
session := &TunnelSession{
|
||||||
|
ID: sessionID,
|
||||||
|
TunnelID: tunnel.ID,
|
||||||
|
ProxyConn: proxyConn,
|
||||||
|
Active: true,
|
||||||
|
Ready: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
// Aktive Proxy-Verbindung registrieren (fuer Rueckkanal vom Agent)
|
// Session registrieren
|
||||||
tunnel.proxyMutex.Lock()
|
tunnel.sessMutex.Lock()
|
||||||
tunnel.proxyConn = conn
|
tunnel.Sessions[sessionID] = session
|
||||||
tunnel.proxyMutex.Unlock()
|
tunnel.sessMutex.Unlock()
|
||||||
|
|
||||||
|
tm.sessMutex.Lock()
|
||||||
|
tm.sessions[sessionID] = session
|
||||||
|
tm.sessMutex.Unlock()
|
||||||
|
|
||||||
|
log.Printf("Tunnel %s: Neue Session %s von %s", tunnel.ID, sessionID, proxyConn.RemoteAddr())
|
||||||
|
|
||||||
|
// Agent auffordern, Ziel-Verbindung fuer diese Session zu oeffnen
|
||||||
|
cmd := map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"id": tm.generateID(),
|
||||||
|
"command": "tunnel_connect",
|
||||||
|
"params": map[string]interface{}{
|
||||||
|
"session_id": sessionID,
|
||||||
|
"tunnel_id": tunnel.ID,
|
||||||
|
"target_host": tunnel.TargetHost,
|
||||||
|
"target_port": tunnel.TargetPort,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdJSON, _ := json.Marshal(cmd)
|
||||||
|
if err := tm.hub.SendToAgent(tunnel.AgentID, cmdJSON); err != nil {
|
||||||
|
log.Printf("Session %s: tunnel_connect senden fehlgeschlagen: %v", sessionID, err)
|
||||||
|
tm.cleanupSession(tunnel, sessionID)
|
||||||
|
proxyConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warten bis Agent die Session bestaetigt hat (max 10s)
|
||||||
|
select {
|
||||||
|
case <-session.Ready:
|
||||||
|
log.Printf("Session %s: Agent bereit, starte Forwarding", sessionID)
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
log.Printf("Session %s: Timeout beim Warten auf Agent", sessionID)
|
||||||
|
tm.cleanupSession(tunnel, sessionID)
|
||||||
|
proxyConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy -> Agent Data-Forwarding (liest von Proxy-Conn, schickt als Binary an Agent)
|
||||||
defer func() {
|
defer func() {
|
||||||
tunnel.proxyMutex.Lock()
|
tm.cleanupSession(tunnel, sessionID)
|
||||||
tunnel.proxyConn = nil
|
proxyConn.Close()
|
||||||
tunnel.proxyMutex.Unlock()
|
// Agent informieren dass Session beendet ist
|
||||||
log.Printf("Tunnel %s: Proxy-Verbindung beendet", tunnel.ID)
|
tm.sendSessionDisconnect(tunnel.AgentID, sessionID)
|
||||||
|
log.Printf("Tunnel %s: Session %s beendet", tunnel.ID, sessionID)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Buffer fuer TCP-Data
|
|
||||||
buffer := make([]byte, 32768)
|
buffer := make([]byte, 32768)
|
||||||
|
for session.Active && tunnel.Active {
|
||||||
for tunnel.Active {
|
n, err := proxyConn.Read(buffer)
|
||||||
n, err := conn.Read(buffer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() != "EOF" && tunnel.Active {
|
if err != io.EOF && session.Active {
|
||||||
log.Printf("Tunnel %s: Proxy-Read-Fehler: %v", tunnel.ID, err)
|
log.Printf("Session %s: Proxy-Read-Fehler: %v", sessionID, err)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
// Daten ueber WebSocket an Agent weiterleiten
|
if err := tm.sendSessionData(tunnel.AgentID, sessionID, buffer[:n]); err != nil {
|
||||||
if err := tm.sendTunnelData(tunnel.ID, buffer[:n]); err != nil {
|
log.Printf("Session %s: Weiterleitung fehlgeschlagen: %v", sessionID, err)
|
||||||
log.Printf("Tunnel %s: Weiterleitung fehlgeschlagen: %v", tunnel.ID, err)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) sendTunnelData(tunnelID string, data []byte) error {
|
// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Agent (Ziel -> Proxy)
|
||||||
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 {
|
func (tm *TunnelManager) ProcessBinaryMessage(agentID string, data []byte) error {
|
||||||
// Binary-Message parsen: [tunnel_id_length:1][tunnel_id][data]
|
|
||||||
if len(data) < 1 {
|
if len(data) < 1 {
|
||||||
return fmt.Errorf("Binary-Message zu kurz")
|
return fmt.Errorf("Binary-Message zu kurz")
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnelIDLen := int(data[0])
|
idLen := int(data[0])
|
||||||
if len(data) < 1+tunnelIDLen {
|
if len(data) < 1+idLen {
|
||||||
return fmt.Errorf("Tunnel-ID zu kurz")
|
return fmt.Errorf("Session-ID zu kurz")
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnelID := string(data[1 : 1+tunnelIDLen])
|
sessionID := string(data[1 : 1+idLen])
|
||||||
payload := data[1+tunnelIDLen:]
|
payload := data[1+idLen:]
|
||||||
|
|
||||||
// Tunnel finden und Daten weiterleiten
|
// Session finden
|
||||||
tunnel, exists := tm.GetTunnel(tunnelID)
|
tm.sessMutex.RLock()
|
||||||
if !exists || !tunnel.Active || tunnel.AgentID != agentID {
|
session, exists := tm.sessions[sessionID]
|
||||||
return fmt.Errorf("Tunnel %s nicht gefunden oder inaktiv", tunnelID)
|
tm.sessMutex.RUnlock()
|
||||||
|
|
||||||
|
if !exists || !session.Active {
|
||||||
|
return fmt.Errorf("Session %s nicht gefunden oder inaktiv", sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daten an aktive Proxy-Verbindung weiterleiten
|
// Daten an Proxy-Verbindung schreiben
|
||||||
tunnel.proxyMutex.Lock()
|
session.mutex.Lock()
|
||||||
conn := tunnel.proxyConn
|
defer session.mutex.Unlock()
|
||||||
tunnel.proxyMutex.Unlock()
|
|
||||||
|
|
||||||
if conn == nil {
|
if session.ProxyConn == nil {
|
||||||
return fmt.Errorf("Tunnel %s: keine aktive Proxy-Verbindung", tunnelID)
|
return fmt.Errorf("Session %s: keine Proxy-Verbindung", sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, writeErr := conn.Write(payload)
|
_, err := session.ProxyConn.Write(payload)
|
||||||
if writeErr != nil {
|
return err
|
||||||
return fmt.Errorf("Tunnel %s: Proxy-Write fehlgeschlagen: %w", tunnelID, writeErr)
|
}
|
||||||
|
|
||||||
|
// sendSessionData schickt Daten als Binary-Message an den Agent
|
||||||
|
func (tm *TunnelManager) sendSessionData(agentID, sessionID string, data []byte) error {
|
||||||
|
idBytes := []byte(sessionID)
|
||||||
|
if len(idBytes) > 255 {
|
||||||
|
return fmt.Errorf("session_id zu lang")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
message := make([]byte, 1+len(idBytes)+len(data))
|
||||||
|
message[0] = byte(len(idBytes))
|
||||||
|
copy(message[1:], idBytes)
|
||||||
|
copy(message[1+len(idBytes):], data)
|
||||||
|
|
||||||
|
return tm.hub.SendBinaryToAgent(agentID, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendSessionDisconnect informiert den Agent dass eine Session beendet ist
|
||||||
|
func (tm *TunnelManager) sendSessionDisconnect(agentID, sessionID string) {
|
||||||
|
if !tm.hub.IsAgentConnected(agentID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"id": tm.generateID(),
|
||||||
|
"command": "tunnel_disconnect",
|
||||||
|
"params": map[string]interface{}{
|
||||||
|
"session_id": sessionID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdJSON, _ := json.Marshal(cmd)
|
||||||
|
tm.hub.SendToAgent(agentID, cmdJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *TunnelManager) cleanupSession(tunnel *BackendTunnel, sessionID string) {
|
||||||
|
tunnel.sessMutex.Lock()
|
||||||
|
if sess, ok := tunnel.Sessions[sessionID]; ok {
|
||||||
|
sess.Active = false
|
||||||
|
delete(tunnel.Sessions, sessionID)
|
||||||
|
}
|
||||||
|
tunnel.sessMutex.Unlock()
|
||||||
|
|
||||||
|
tm.sessMutex.Lock()
|
||||||
|
delete(tm.sessions, sessionID)
|
||||||
|
tm.sessMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmSession markiert eine Session als bereit (Agent hat Ziel-Verbindung hergestellt)
|
||||||
|
func (tm *TunnelManager) ConfirmSession(sessionID string) {
|
||||||
|
tm.sessMutex.RLock()
|
||||||
|
session, exists := tm.sessions[sessionID]
|
||||||
|
tm.sessMutex.RUnlock()
|
||||||
|
|
||||||
|
if exists && session.Active {
|
||||||
|
select {
|
||||||
|
case <-session.Ready:
|
||||||
|
// Bereits geschlossen
|
||||||
|
default:
|
||||||
|
close(session.Ready)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) allocatePort() (int, error) {
|
func (tm *TunnelManager) allocatePort() (int, error) {
|
||||||
@ -325,33 +403,23 @@ func (tm *TunnelManager) allocatePort() (int, error) {
|
|||||||
return port, nil
|
return port, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 0, fmt.Errorf("keine freien Ports verfuegbar")
|
||||||
return 0, fmt.Errorf("keine freien Ports verfügbar")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) releasePort(port int) {
|
func (tm *TunnelManager) releasePort(port int) {
|
||||||
tm.mutex.Lock()
|
tm.mutex.Lock()
|
||||||
defer tm.mutex.Unlock()
|
defer tm.mutex.Unlock()
|
||||||
|
|
||||||
delete(tm.usedPorts, port)
|
delete(tm.usedPorts, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TunnelManager) generateTunnelID() string {
|
func (tm *TunnelManager) generateID() string {
|
||||||
bytes := make([]byte, 8)
|
bytes := make([]byte, 8)
|
||||||
rand.Read(bytes)
|
rand.Read(bytes)
|
||||||
return hex.EncodeToString(bytes)
|
return hex.EncodeToString(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateHTTPProxy erstellt einen HTTP-Reverse-Proxy für einen Tunnel
|
// CreateHTTPProxy - HTTP-Reverse-Proxy (noch nicht implementiert)
|
||||||
func (tm *TunnelManager) CreateHTTPProxy(tunnelID string, w http.ResponseWriter, r *http.Request) error {
|
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)
|
http.Error(w, "HTTP-Proxy noch nicht implementiert", http.StatusNotImplemented)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user