package ws import ( "context" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "log" "os" "os/exec" "strconv" "strings" "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 "update": response = h.handleUpdate(msg) case "major_update": response = h.handleMajorUpdate(msg) case "update_check": response = h.handleUpdateCheck(msg) case "reboot": response = h.handleReboot(msg) case "backup": response = h.handleBackup(msg) case "wg_add_peer": response = h.handleWGAddPeer(msg) case "wg_delete_peer": response = h.handleWGDeletePeer(msg) case "wg_list_peers": response = h.handleWGListPeers(msg) case "tunnel_connect": response = h.handleTunnelConnect(msg) case "tunnel_disconnect": h.handleTunnelDisconnect(msg) return case "tunnel_open": response.Status = "ok" response.Data = map[string]interface{}{"message": "tunnel_open deprecated, use tunnel_connect"} case "tunnel_close": response.Status = "ok" 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 } // handleUpdateCheck prueft ob Updates verfuegbar sind func (h *Handler) handleUpdateCheck(msg Message) Message { response := Message{Type: "response", ID: msg.ID} log.Println("Update-Check gestartet...") result := map[string]interface{}{} // 1. OPNsense Core Update pruefen // opnsense-update -c gibt Exit 1 wenn Updates da sind, aber auch bei anderen Fehlern. // Nur wenn stdout tatsaechlich Update-Infos enthaelt, ist ein Update verfuegbar. coreOutput, coreErr := h.runCommand("/usr/local/sbin/opnsense-update -c", 60) coreInfo := strings.TrimSpace(coreOutput) if coreErr != nil && coreInfo != "" { // Exit 1 MIT Output = echtes Update verfuegbar result["core_update_available"] = true result["core_update_info"] = coreInfo } else { result["core_update_available"] = false result["core_update_info"] = "Kein Core-Update verfuegbar" } // Reboot-Check: Base/Kernel Version != installierte Version? baseVer, _ := h.runCommand("cat /usr/local/opnsense/version/base", 5) kernelVer, _ := h.runCommand("cat /usr/local/opnsense/version/kernel", 5) coreJson, _ := h.runCommand("cat /usr/local/opnsense/version/core", 5) baseVer = strings.TrimSpace(baseVer) kernelVer = strings.TrimSpace(kernelVer) rebootRequired := false // Pruefen ob CORE_SERIES sich von Base/Kernel unterscheidet if strings.Contains(coreJson, "CORE_NEXT") && baseVer != "" && kernelVer != "" { // Einfacher Check: wenn base/kernel neuer als core → reboot noetig coreVer := "" for _, line := range strings.Split(coreJson, "\n") { if strings.Contains(line, "CORE_PKGVERSION") { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 { coreVer = strings.Trim(strings.TrimSpace(parts[1]), "\",") } } } if coreVer != "" && baseVer != "" && !strings.HasPrefix(baseVer, strings.Split(coreVer, ".")[0]+"."+strings.Split(coreVer, ".")[1]) { // Base-Version Major.Minor unterscheidet sich von Core → Reboot rebootRequired = true } } result["reboot_required"] = rebootRequired // 2. Paket-Updates pruefen pkgOutput, _ := h.runCommand("/usr/sbin/pkg upgrade -n", 60) pendingPackages := h.parsePkgUpgrade(pkgOutput) result["package_update_available"] = len(pendingPackages) > 0 result["pending_packages"] = pendingPackages result["pending_count"] = len(pendingPackages) response.Status = "ok" response.Data = result log.Printf("Update-Check abgeschlossen: Core=%v, Packages=%d", result["core_update_available"], len(pendingPackages)) return response } // handleUpdate fuehrt ein normales Update durch (Core + Packages) func (h *Handler) handleUpdate(msg Message) Message { response := Message{Type: "response", ID: msg.ID} log.Println("Normales Update gestartet...") reboot := false if r, ok := msg.Params["reboot"].(bool); ok { reboot = r } result := map[string]interface{}{ "steps": []map[string]interface{}{}, } var steps []map[string]interface{} // Schritt 1: opnsense-update (Core + Firmware) log.Println("Update Schritt 1: opnsense-update") coreOutput, coreErr := h.runCommand("/usr/local/sbin/opnsense-update", 600) step1 := map[string]interface{}{ "step": "opnsense-update", "output": coreOutput, "ok": coreErr == nil, } if coreErr != nil { step1["error"] = coreErr.Error() } steps = append(steps, step1) // Schritt 2: pkg upgrade -y (Paket-Updates) log.Println("Update Schritt 2: pkg upgrade -y") pkgOutput, pkgErr := h.runCommand("/usr/sbin/pkg upgrade -y", 600) step2 := map[string]interface{}{ "step": "pkg upgrade", "output": pkgOutput, "ok": pkgErr == nil, } if pkgErr != nil { step2["error"] = pkgErr.Error() } steps = append(steps, step2) // Schritt 3: opnsense-update -c (Nachpruefung) log.Println("Update Schritt 3: Nachpruefung") checkOutput, checkErr := h.runCommand("/usr/local/sbin/opnsense-update -c", 60) rebootRequired := checkErr != nil // Exit 1 nach Update = Reboot noetig step3 := map[string]interface{}{ "step": "verify", "output": checkOutput, "reboot_required": rebootRequired, } steps = append(steps, step3) result["steps"] = steps result["reboot_required"] = rebootRequired result["reboot_requested"] = reboot // Gesamtstatus if coreErr == nil && pkgErr == nil { response.Status = "ok" if rebootRequired { result["message"] = "Update erfolgreich — Reboot erforderlich" } else { result["message"] = "Update erfolgreich abgeschlossen" } } else { response.Status = "ok" result["message"] = "Update mit Warnungen abgeschlossen" } // Reboot wenn gewuenscht if reboot { log.Println("Reboot nach Update angefordert...") result["reboot"] = "Reboot in 5 Sekunden..." response.Data = result // Response erst senden, dann rebooten go func() { time.Sleep(5 * time.Second) log.Println("Reboot wird ausgefuehrt...") exec.Command("/sbin/shutdown", "-r", "now").Run() }() return response } response.Data = result log.Println("Update abgeschlossen") return response } // handleMajorUpdate fuehrt ein Major-Upgrade durch // Ablauf: Phase 1: opnsense-update -r -bkp → Reboot // Phase 2 (nach Reboot): pkg-static update -f → pkg-static upgrade -y → optionaler Reboot // WICHTIG: Das neue Paket-Repository wird erst NACH dem Reboot aktiv, // daher muss pkg upgrade zwingend nach dem Reboot laufen. func (h *Handler) handleMajorUpdate(msg Message) Message { response := Message{Type: "response", ID: msg.ID} targetVersion, ok := msg.Params["version"].(string) if !ok || targetVersion == "" { response.Status = "error" response.Error = "Parameter 'version' erforderlich (z.B. '26.1')" return response } // Phase 2: Wenn phase=2, nur pkg upgrade ausfuehren (nach Reboot) phase := "1" if p, ok := msg.Params["phase"].(string); ok && p != "" { phase = p } else if p, ok := msg.Params["phase"].(float64); ok { phase = fmt.Sprintf("%.0f", p) } log.Printf("Major-Update auf Version %s, Phase %s gestartet...", targetVersion, phase) result := map[string]interface{}{ "target_version": targetVersion, "phase": phase, } var steps []map[string]interface{} hasError := false if phase == "2" { // Phase 2: Nach Reboot — Repo auf neue Version umstellen, dann pkg upgrade // Das OPNsense pkg-Repo wird vom opnsense Package kontrolliert (CORE_REPOSITORY in /usr/local/opnsense/version/core) // Nach opnsense-update -r -bkp + Reboot zeigt das Repo noch die alte Version // Wir muessen die Repo-Config manuell umschreiben repoConf := "/usr/local/etc/pkg/repos/OPNsense.conf" log.Printf("Phase 2: Repo-Config auf %s umstellen", targetVersion) // Aktuelle Series aus dem Repo-Config lesen und durch targetVersion ersetzen sedCmd := fmt.Sprintf("/usr/bin/sed -i '' 's|/[0-9][0-9]\\.[0-9]/latest|/%s/latest|g' %s", targetVersion, repoConf) sedOutput, sedErr := h.runCommand(sedCmd, 10) step0 := map[string]interface{}{ "step": "repo-switch to " + targetVersion, "output": sedOutput, "ok": sedErr == nil, } if sedErr != nil { step0["error"] = sedErr.Error() hasError = true } steps = append(steps, step0) log.Println("Phase 2: pkg-static update -f") catalogOutput, catalogErr := h.runCommand("/usr/local/sbin/pkg-static update -f", 120) step1 := map[string]interface{}{ "step": "pkg-static update -f", "output": catalogOutput, "ok": catalogErr == nil, } if catalogErr != nil { step1["error"] = catalogErr.Error() hasError = true } steps = append(steps, step1) log.Println("Phase 2: pkg-static upgrade -y") pkgOutput, pkgErr := h.runCommand("/usr/local/sbin/pkg-static upgrade -y", 900) step2 := map[string]interface{}{ "step": "pkg-static upgrade -y", "output": pkgOutput, "ok": pkgErr == nil, } if pkgErr != nil { step2["error"] = pkgErr.Error() hasError = true } steps = append(steps, step2) // Verify verOutput, _ := h.runCommand("/usr/local/sbin/opnsense-version", 10) verStep := map[string]interface{}{ "step": "verify", "output": strings.TrimSpace(verOutput), "ok": true, } steps = append(steps, verStep) result["steps"] = steps result["reboot_required"] = false if !hasError { result["message"] = fmt.Sprintf("Major-Update Phase 2 auf %s abgeschlossen — Packages aktualisiert", targetVersion) } else { result["message"] = fmt.Sprintf("Major-Update Phase 2 auf %s mit Fehlern", targetVersion) } response.Status = "ok" response.Data = result return response } // Phase 1: Base + Kernel installieren, dann Reboot reboot := true if r, ok := msg.Params["reboot"].(bool); ok { reboot = r } log.Printf("Phase 1: opnsense-update -r %s -bkp", targetVersion) releaseCmd := fmt.Sprintf("/usr/local/sbin/opnsense-update -r %s -bkp", targetVersion) releaseOutput, releaseErr := h.runCommand(releaseCmd, 900) step1 := map[string]interface{}{ "step": fmt.Sprintf("opnsense-update -r %s -bkp", targetVersion), "output": releaseOutput, "ok": releaseErr == nil, } if releaseErr != nil { step1["error"] = releaseErr.Error() hasError = true } steps = append(steps, step1) result["steps"] = steps result["reboot_required"] = true result["reboot_requested"] = reboot if !hasError { result["message"] = fmt.Sprintf("Major-Update Phase 1 auf %s abgeschlossen — Reboot erforderlich, danach Phase 2 ausfuehren", targetVersion) } else { result["message"] = fmt.Sprintf("Major-Update Phase 1 auf %s mit Fehlern", targetVersion) } response.Status = "ok" if reboot { log.Println("Reboot nach Phase 1 angefordert...") result["reboot"] = "Reboot in 5 Sekunden..." response.Data = result go func() { time.Sleep(5 * time.Second) log.Println("Reboot wird ausgefuehrt...") exec.Command("/sbin/shutdown", "-r", "now").Run() }() return response } response.Data = result log.Printf("Major-Update Phase 1 auf %s abgeschlossen (Reboot noch ausstehend)", targetVersion) 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 } // --- Backup Handler --- // handleBackup liest die OPNsense config.xml und sendet sie zurueck func (h *Handler) handleBackup(msg Message) Message { response := Message{Type: "response", ID: msg.ID} configPath := "/conf/config.xml" if p, ok := msg.Params["path"].(string); ok && p != "" { configPath = p } data, err := os.ReadFile(configPath) if err != nil { response.Status = "error" response.Error = fmt.Sprintf("Config lesen fehlgeschlagen: %v", err) return response } // SHA256-Hash berechnen hash := sha256.Sum256(data) hashStr := hex.EncodeToString(hash[:]) // Base64-kodiert senden (config.xml ist Text, aber sicherstellen dass nichts verloren geht) encoded := base64.StdEncoding.EncodeToString(data) response.Status = "ok" response.Data = map[string]interface{}{ "config": encoded, "hash": hashStr, "size": len(data), "path": configPath, "encoding": "base64", } log.Printf("Backup erstellt: %s (%d Bytes, Hash: %s)", configPath, len(data), hashStr[:12]) 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) } } // --- Helper --- // 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 } // parsePkgUpgrade parst die Ausgabe von "pkg upgrade -n" func (h *Handler) parsePkgUpgrade(output string) []map[string]string { var packages []map[string]string inUpgradeBlock := false for _, line := range strings.Split(output, "\n") { trimmed := strings.TrimSpace(line) if strings.Contains(trimmed, "packages to be UPGRADED") || strings.Contains(trimmed, "will be affected") { inUpgradeBlock = true continue } if inUpgradeBlock && strings.Contains(trimmed, "->") { parts := strings.SplitN(trimmed, ":", 2) if len(parts) == 2 { pkg := strings.TrimSpace(parts[0]) versions := strings.TrimSpace(parts[1]) arrowParts := strings.SplitN(versions, "->", 2) if len(arrowParts) == 2 { packages = append(packages, map[string]string{ "package": pkg, "current_version": strings.TrimSpace(arrowParts[0]), "new_version": strings.TrimSpace(arrowParts[1]), }) } } } if inUpgradeBlock && (trimmed == "" || strings.HasPrefix(trimmed, "Number of")) { if !strings.HasPrefix(trimmed, "Number of") { inUpgradeBlock = false } } } return packages } 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/rmm/rmm-agent" 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) exec.Command("/usr/sbin/service", "rmm_agent", "restart").Run() }() return response } // 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) }