Linux/Proxmox Agent + Plattform-Updater + Proxmox Frontend
- agent-linux/: Kompletter Linux/Proxmox RMM Agent - Collectors: CPU, Memory, Disk, Network, Services, Updates, Proxmox (VMs, Container, Storage, ZFS), Backups - Backup Collector: Jobs nach Node gefiltert (Cluster-aware) - WebSocket: Tunnel-Support, writeMux fuer stabile Verbindungen - Systemd Service Template - updater/: Plattform-unabhaengiger Updater (FreeBSD + Linux) - runtime.GOOS erkennt Plattform automatisch - FreeBSD: /usr/local/rmm/, service rmm_agent - Linux: /usr/local/bin/, /etc/rmm/, systemctl rmm-agent - Firmware-Download mit ?platform= Parameter - frontend/: Proxmox-Bereich - ProxmoxServers.jsx: Eigene Seite fuer PVE Server mit VM/CT Counts - ProxmoxPanel.jsx: Detail-Panel mit Uebersicht, VMs, Container, Storage, ZFS, Tunnel, Dienste, Updates, Backups - Agents.jsx: Filtert Linux-Agents raus (nur OPNsense) - Sidebar: Proxmox Navigationspunkt - Installationsanleitung mit rmm-agent-linux + rmm-updater-linux - backend: Platform-Feld fuer Agents, Firmware multi-platform
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
backendURL string
|
||||
agentID string
|
||||
apiKey string
|
||||
insecure bool
|
||||
|
||||
conn *websocket.Conn
|
||||
connMux sync.RWMutex
|
||||
writeMux sync.Mutex
|
||||
|
||||
reconnectInterval time.Duration
|
||||
pingInterval time.Duration
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
messageHandler *Handler
|
||||
tunnelManager *TunnelManager
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
Event string `json:"event,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func NewClient(backendURL, agentID, apiKey string, insecure bool) *Client {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
client := &Client{
|
||||
backendURL: backendURL,
|
||||
agentID: agentID,
|
||||
apiKey: apiKey,
|
||||
insecure: insecure,
|
||||
reconnectInterval: 5 * time.Second,
|
||||
pingInterval: 30 * time.Second,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
client.messageHandler = NewHandler(client)
|
||||
client.tunnelManager = NewTunnelManager(client)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
log.Println("WebSocket Client startet...")
|
||||
|
||||
go c.reconnectLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Stop() {
|
||||
log.Println("WebSocket Client stoppt...")
|
||||
c.cancel()
|
||||
|
||||
c.connMux.Lock()
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
c.connMux.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) reconnectLoop() {
|
||||
backoff := c.reconnectInterval
|
||||
maxBackoff := 5 * time.Minute
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if err := c.connect(); err != nil {
|
||||
log.Printf("WebSocket-Verbindung fehlgeschlagen: %v", err)
|
||||
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
backoff = time.Duration(float64(backoff) * 1.5)
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
connStart := time.Now()
|
||||
log.Println("WebSocket-Verbindung hergestellt")
|
||||
|
||||
// Ping/Pong und Message-Handling starten
|
||||
c.runConnection()
|
||||
|
||||
connDuration := time.Since(connStart)
|
||||
log.Printf("WebSocket-Verbindung getrennt (Dauer: %v)", connDuration)
|
||||
|
||||
// Alte Verbindung explizit schliessen
|
||||
c.connMux.Lock()
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
c.connMux.Unlock()
|
||||
|
||||
// Minimum-Delay zwischen Reconnects (verhindert Tight-Loop)
|
||||
if connDuration < 5*time.Second {
|
||||
backoff = time.Duration(float64(backoff) * 1.5)
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
log.Printf("Verbindung zu kurz, warte %v vor Reconnect", backoff)
|
||||
select {
|
||||
case <-time.After(backoff):
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
} else {
|
||||
backoff = c.reconnectInterval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) connect() error {
|
||||
// WebSocket-URL aufbauen
|
||||
u, err := url.Parse(c.backendURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Backend-URL parsen: %w", err)
|
||||
}
|
||||
|
||||
if u.Scheme == "https" {
|
||||
u.Scheme = "wss"
|
||||
} else {
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
|
||||
u.Path = "/api/v1/agent/ws"
|
||||
q := u.Query()
|
||||
q.Set("agent_id", c.agentID)
|
||||
q.Set("api_key", c.apiKey)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
// Dialer mit TLS-Config
|
||||
dialer := websocket.Dialer{
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: c.insecure,
|
||||
},
|
||||
}
|
||||
|
||||
conn, _, err := dialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebSocket-Dial: %w", err)
|
||||
}
|
||||
|
||||
c.connMux.Lock()
|
||||
c.conn = conn
|
||||
c.connMux.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) runConnection() {
|
||||
// Ping-Ticker starten
|
||||
pingTicker := time.NewTicker(c.pingInterval)
|
||||
defer pingTicker.Stop()
|
||||
|
||||
// Pong-Handler setzen
|
||||
c.connMux.RLock()
|
||||
conn := c.conn
|
||||
c.connMux.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
conn.SetPongHandler(func(string) error {
|
||||
conn.SetReadDeadline(time.Now().Add(c.pingInterval * 2))
|
||||
return nil
|
||||
})
|
||||
conn.SetReadDeadline(time.Now().Add(c.pingInterval * 2))
|
||||
|
||||
// Goroutine für eingehende Nachrichten
|
||||
msgChan := make(chan Message, 10)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go c.readMessages(conn, msgChan, errChan)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
|
||||
case <-pingTicker.C:
|
||||
c.writeMux.Lock()
|
||||
c.connMux.RLock()
|
||||
conn := c.conn
|
||||
c.connMux.RUnlock()
|
||||
if conn != nil {
|
||||
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
c.writeMux.Unlock()
|
||||
log.Printf("Ping senden fehlgeschlagen: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.writeMux.Unlock()
|
||||
|
||||
case msg := <-msgChan:
|
||||
go c.handleMessage(msg)
|
||||
|
||||
case err := <-errChan:
|
||||
log.Printf("WebSocket-Lesefehler: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readMessages(conn *websocket.Conn, msgChan chan<- Message, errChan chan<- error) {
|
||||
defer close(msgChan)
|
||||
|
||||
for {
|
||||
messageType, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
if messageType == websocket.BinaryMessage {
|
||||
if err := c.tunnelManager.ProcessBinaryMessage(data); err != nil {
|
||||
log.Printf("Binary-Message verarbeiten fehlgeschlagen: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// JSON-Message parsen
|
||||
var msg Message
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
log.Printf("JSON-Message parsen fehlgeschlagen: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case msgChan <- msg:
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleMessage(msg Message) {
|
||||
switch msg.Type {
|
||||
case "command":
|
||||
c.messageHandler.HandleCommand(msg)
|
||||
case "event":
|
||||
// Events vom Backend verarbeiten (falls nötig)
|
||||
log.Printf("Event erhalten: %s", msg.Event)
|
||||
default:
|
||||
log.Printf("Unbekannter Nachrichtentyp: %s", msg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SendMessage(msg Message) error {
|
||||
c.connMux.RLock()
|
||||
conn := c.conn
|
||||
c.connMux.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("keine WebSocket-Verbindung")
|
||||
}
|
||||
|
||||
c.writeMux.Lock()
|
||||
err := conn.WriteJSON(msg)
|
||||
c.writeMux.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) SendBinaryData(data []byte) error {
|
||||
c.connMux.RLock()
|
||||
conn := c.conn
|
||||
c.connMux.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return fmt.Errorf("keine WebSocket-Verbindung")
|
||||
}
|
||||
|
||||
c.writeMux.Lock()
|
||||
err := conn.WriteMessage(websocket.BinaryMessage, data)
|
||||
c.writeMux.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.connMux.RLock()
|
||||
connected := c.conn != nil
|
||||
c.connMux.RUnlock()
|
||||
|
||||
return connected
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func NewHandler(client *Client) *Handler {
|
||||
return &Handler{client: client}
|
||||
}
|
||||
|
||||
func (h *Handler) HandleCommand(msg Message) {
|
||||
log.Printf("Command erhalten: %s (ID: %s)", msg.Command, msg.ID)
|
||||
|
||||
var response Message
|
||||
response.Type = "response"
|
||||
response.ID = msg.ID
|
||||
|
||||
switch msg.Command {
|
||||
case "exec":
|
||||
response = h.handleExec(msg)
|
||||
case "reboot":
|
||||
response = h.handleReboot(msg)
|
||||
case "tunnel_connect":
|
||||
response = h.handleTunnelConnect(msg)
|
||||
case "tunnel_disconnect":
|
||||
h.handleTunnelDisconnect(msg)
|
||||
return
|
||||
case "agent_update":
|
||||
response = h.handleAgentUpdate(msg)
|
||||
default:
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Unbekanntes Command: %s", msg.Command)
|
||||
}
|
||||
|
||||
if err := h.client.SendMessage(response); err != nil {
|
||||
log.Printf("Response senden fehlgeschlagen: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleExec fuehrt einen beliebigen Befehl aus
|
||||
func (h *Handler) handleExec(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
command, ok := msg.Params["command"].(string)
|
||||
if !ok || command == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Parameter 'command' erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
timeoutSec := 30
|
||||
if t, ok := msg.Params["timeout"]; ok {
|
||||
if ts, ok := t.(float64); ok {
|
||||
timeoutSec = int(ts)
|
||||
} else if ts, ok := t.(string); ok {
|
||||
if parsed, err := strconv.Atoi(ts); err == nil {
|
||||
timeoutSec = parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output, err := h.runCommand(command, timeoutSec)
|
||||
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = err.Error()
|
||||
response.Data = map[string]interface{}{"output": output}
|
||||
} else {
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{"output": output}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// handleReboot fuehrt einen Reboot durch
|
||||
func (h *Handler) handleReboot(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
delaySec := 5
|
||||
if d, ok := msg.Params["delay"].(float64); ok && d > 0 {
|
||||
delaySec = int(d)
|
||||
}
|
||||
|
||||
log.Printf("Reboot angefordert (Verzoegerung: %ds)", delaySec)
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"message": fmt.Sprintf("Reboot in %d Sekunden", delaySec),
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Duration(delaySec) * time.Second)
|
||||
log.Println("Reboot wird ausgefuehrt...")
|
||||
exec.Command("/sbin/shutdown", "-r", "now").Run()
|
||||
}()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// handleAgentUpdate aktualisiert den Agent
|
||||
func (h *Handler) handleAgentUpdate(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
// Binary kommt als Base64 im Data-Feld
|
||||
data, ok := msg.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
response.Status = "error"
|
||||
response.Error = "Ungueltige Daten"
|
||||
return response
|
||||
}
|
||||
|
||||
binaryB64, _ := data["binary"].(string)
|
||||
expectedHash, _ := data["hash"].(string)
|
||||
newVersion, _ := data["version"].(string)
|
||||
|
||||
if binaryB64 == "" || expectedHash == "" {
|
||||
response.Status = "error"
|
||||
response.Error = "Binary oder Hash fehlt"
|
||||
return response
|
||||
}
|
||||
|
||||
log.Printf("Agent-Update empfangen: Version %s, Hash %s", newVersion, expectedHash[:16])
|
||||
|
||||
// Base64 dekodieren
|
||||
binary, err := base64.StdEncoding.DecodeString(binaryB64)
|
||||
if err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Base64-Dekodierung fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// Hash pruefen
|
||||
hash := sha256.Sum256(binary)
|
||||
actualHash := hex.EncodeToString(hash[:])
|
||||
if actualHash != expectedHash {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Hash-Mismatch: erwartet %s, bekommen %s", expectedHash[:16], actualHash[:16])
|
||||
return response
|
||||
}
|
||||
|
||||
// Aktuelles Binary finden
|
||||
binaryPath := "/usr/local/bin/rmm-agent-linux"
|
||||
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||
// Fallback: eigener Pfad
|
||||
binaryPath, _ = os.Executable()
|
||||
}
|
||||
|
||||
// Backup vom alten Binary
|
||||
backupPath := binaryPath + ".old"
|
||||
if err := os.Rename(binaryPath, backupPath); err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Backup fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
// Neues Binary schreiben
|
||||
if err := os.WriteFile(binaryPath, binary, 0755); err != nil {
|
||||
// Rollback
|
||||
os.Rename(backupPath, binaryPath)
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Binary schreiben fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
log.Printf("Neues Binary geschrieben: %s (%d bytes)", binaryPath, len(binary))
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{
|
||||
"message": fmt.Sprintf("Update auf %s erfolgreich, Neustart...", newVersion),
|
||||
"old_version": data["old_version"],
|
||||
"new_version": newVersion,
|
||||
}
|
||||
|
||||
// Response senden, dann Neustart
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
log.Printf("Agent-Neustart nach Update auf %s", newVersion)
|
||||
// Versuche systemd service restart, fallback zu kill + restart
|
||||
if err := exec.Command("systemctl", "restart", "rmm-agent-linux").Run(); err != nil {
|
||||
log.Printf("systemctl restart fehlgeschlagen, versuche manuelle Wiederbelebung: %v", err)
|
||||
// Als letzter Ausweg: sich selbst ersetzen
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// --- Tunnel Handlers ---
|
||||
|
||||
func (h *Handler) handleTunnelConnect(msg Message) Message {
|
||||
response := Message{Type: "response", ID: msg.ID}
|
||||
|
||||
sessionID, _ := msg.Params["session_id"].(string)
|
||||
tunnelID, _ := msg.Params["tunnel_id"].(string)
|
||||
targetHost, _ := msg.Params["target_host"].(string)
|
||||
targetPortFloat, _ := msg.Params["target_port"].(float64)
|
||||
targetPort := int(targetPortFloat)
|
||||
|
||||
if sessionID == "" || targetHost == "" || targetPort <= 0 {
|
||||
response.Status = "error"
|
||||
response.Error = "session_id, target_host und target_port erforderlich"
|
||||
return response
|
||||
}
|
||||
|
||||
if err := h.client.tunnelManager.ConnectSession(sessionID, tunnelID, targetHost, targetPort); err != nil {
|
||||
response.Status = "error"
|
||||
response.Error = fmt.Sprintf("Session-Connect fehlgeschlagen: %v", err)
|
||||
return response
|
||||
}
|
||||
|
||||
response.Status = "ok"
|
||||
response.Data = map[string]interface{}{"session_id": sessionID}
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *Handler) handleTunnelDisconnect(msg Message) {
|
||||
sessionID, _ := msg.Params["session_id"].(string)
|
||||
if sessionID != "" {
|
||||
h.client.tunnelManager.DisconnectSession(sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// SendSessionData schickt Tunnel-Daten als Binary-Message zurueck zum Backend
|
||||
func (h *Handler) SendSessionData(sessionID string, data []byte) error {
|
||||
idBytes := []byte(sessionID)
|
||||
if len(idBytes) > 255 {
|
||||
return fmt.Errorf("session_id zu lang")
|
||||
}
|
||||
|
||||
message := make([]byte, 1+len(idBytes)+len(data))
|
||||
message[0] = byte(len(idBytes))
|
||||
copy(message[1:], idBytes)
|
||||
copy(message[1+len(idBytes):], data)
|
||||
|
||||
return h.client.SendBinaryData(message)
|
||||
}
|
||||
|
||||
// runCommand fuehrt einen Shell-Befehl mit Timeout aus
|
||||
func (h *Handler) runCommand(command string, timeoutSec int) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
||||
output, err := cmd.CombinedOutput()
|
||||
return string(output), err
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TunnelSession repraesentiert eine einzelne TCP-Verbindung zu einem Ziel
|
||||
type TunnelSession struct {
|
||||
ID string
|
||||
TunnelID string
|
||||
TargetHost string
|
||||
TargetPort int
|
||||
Conn net.Conn
|
||||
Active bool
|
||||
}
|
||||
|
||||
// TunnelManager verwaltet alle Sessions auf Agent-Seite
|
||||
type TunnelManager struct {
|
||||
client *Client
|
||||
sessions map[string]*TunnelSession
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTunnelManager(client *Client) *TunnelManager {
|
||||
return &TunnelManager{
|
||||
client: client,
|
||||
sessions: make(map[string]*TunnelSession),
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectSession oeffnet eine neue TCP-Verbindung fuer eine Session
|
||||
func (tm *TunnelManager) ConnectSession(sessionID, tunnelID, targetHost string, targetPort int) error {
|
||||
target := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
||||
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Verbindung zu %s fehlgeschlagen: %w", target, err)
|
||||
}
|
||||
|
||||
session := &TunnelSession{
|
||||
ID: sessionID,
|
||||
TunnelID: tunnelID,
|
||||
TargetHost: targetHost,
|
||||
TargetPort: targetPort,
|
||||
Conn: conn,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
tm.mutex.Lock()
|
||||
tm.sessions[sessionID] = session
|
||||
tm.mutex.Unlock()
|
||||
|
||||
// Ziel -> Backend Forwarding starten
|
||||
go tm.forwardFromTarget(session)
|
||||
|
||||
log.Printf("Session %s: Verbindung zu %s hergestellt", sessionID, target)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisconnectSession schliesst eine Session
|
||||
func (tm *TunnelManager) DisconnectSession(sessionID string) {
|
||||
tm.mutex.Lock()
|
||||
session, exists := tm.sessions[sessionID]
|
||||
if !exists {
|
||||
tm.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
session.Active = false
|
||||
delete(tm.sessions, sessionID)
|
||||
tm.mutex.Unlock()
|
||||
|
||||
if session.Conn != nil {
|
||||
session.Conn.Close()
|
||||
}
|
||||
|
||||
log.Printf("Session %s: geschlossen", sessionID)
|
||||
}
|
||||
|
||||
// SendDataToSession schreibt Daten in die Ziel-Verbindung einer Session
|
||||
func (tm *TunnelManager) SendDataToSession(sessionID string, data []byte) error {
|
||||
tm.mutex.RLock()
|
||||
session, exists := tm.sessions[sessionID]
|
||||
tm.mutex.RUnlock()
|
||||
|
||||
if !exists || !session.Active {
|
||||
return fmt.Errorf("Session %s nicht aktiv", sessionID)
|
||||
}
|
||||
|
||||
_, err := session.Conn.Write(data)
|
||||
if err != nil {
|
||||
tm.DisconnectSession(sessionID)
|
||||
return fmt.Errorf("Session %s: Write fehlgeschlagen: %w", sessionID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// forwardFromTarget liest Daten vom Ziel und schickt sie als Binary-Message ans Backend
|
||||
func (tm *TunnelManager) forwardFromTarget(session *TunnelSession) {
|
||||
defer func() {
|
||||
if session.Active {
|
||||
tm.DisconnectSession(session.ID)
|
||||
}
|
||||
log.Printf("Session %s: Forwarding beendet", session.ID)
|
||||
}()
|
||||
|
||||
buffer := make([]byte, 32768)
|
||||
|
||||
for session.Active {
|
||||
session.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
|
||||
n, err := session.Conn.Read(buffer)
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
if err == io.EOF {
|
||||
log.Printf("Session %s: Ziel-Verbindung geschlossen", session.ID)
|
||||
} else if session.Active {
|
||||
log.Printf("Session %s: Read-Fehler: %v", session.ID, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
if err := tm.client.messageHandler.SendSessionData(session.ID, buffer[:n]); err != nil {
|
||||
log.Printf("Session %s: WebSocket senden fehlgeschlagen: %v", session.ID, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessBinaryMessage verarbeitet eingehende Binary-Messages vom Backend (Proxy -> Ziel)
|
||||
func (tm *TunnelManager) ProcessBinaryMessage(data []byte) error {
|
||||
sessionID, payload, err := ParseBinaryMessage(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Binary-Message parsen: %w", err)
|
||||
}
|
||||
|
||||
return tm.SendDataToSession(sessionID, payload)
|
||||
}
|
||||
|
||||
// GetActiveSessions gibt die Anzahl aktiver Sessions zurueck
|
||||
func (tm *TunnelManager) GetActiveSessions() int {
|
||||
tm.mutex.RLock()
|
||||
defer tm.mutex.RUnlock()
|
||||
return len(tm.sessions)
|
||||
}
|
||||
|
||||
func (tm *TunnelManager) generateID() string {
|
||||
bytes := make([]byte, 8)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// ParseBinaryMessage parst das Binary-Format: [id_len:1][id][data]
|
||||
func ParseBinaryMessage(data []byte) (id string, payload []byte, err error) {
|
||||
if len(data) < 1 {
|
||||
return "", nil, fmt.Errorf("Daten zu kurz")
|
||||
}
|
||||
|
||||
idLen := int(data[0])
|
||||
if len(data) < 1+idLen {
|
||||
return "", nil, fmt.Errorf("ID zu kurz")
|
||||
}
|
||||
|
||||
id = string(data[1 : 1+idLen])
|
||||
payload = data[1+idLen:]
|
||||
return id, payload, nil
|
||||
}
|
||||
Reference in New Issue
Block a user