- 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
261 lines
6.9 KiB
Go
261 lines
6.9 KiB
Go
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
|
|
} |