Agent-Updater, Multi-Plattform Firmware, Frontend Firmware-Seite, Agent-Tab, Zertifikate-Redesign

Updater:
- Separater Go-Daemon (rmm-updater) als eigener rc.d Service
- Pollt alle 60s: Firmware vorhanden + Hash unterschiedlich + Flag gesetzt
- Download -> SHA256 Verify -> Agent Stop -> Replace -> Agent Start
- Automatisches Rollback bei Fehler
- Deployed und getestet auf allen 3 Firewalls (.1, .19, .33)

Backend:
- Firmware-DB multi-plattform (freebsd/linux/windows)
- update_requested Flag pro Agent in agents Tabelle
- Heartbeat-Response liefert update_available wenn Flag gesetzt
- Neue Endpoints: request-update, request-update-all, firmware/download
- Auth: JWT + CombinedAuth Middleware (API-Key OR JWT)
- Customers CRUD + Agent-Zuordnung
- Users CRUD + Passwort-Aenderung

Frontend:
- Firmware-Seite: Multi-Plattform Upload, Agent-Liste mit Update-Trigger
- Agent-Tab im AgentPanel: Update-Button, nuetzliche Befehle, Agent-Info
- Zertifikate-Tab redesigned: Karten-Layout mit Restlaufzeit und Farbbalken
- Sidebar: Firmware-Eintrag hinzugefuegt

Plugin:
- install.sh erweitert: Installiert Agent + Updater + rc.d Services
- rmm_updater rc.d Service

README:
- Architektur-Diagramm erweitert (Frontend + Updater)
- Auth, Customers, Users, Firmware API dokumentiert
- Updater-Flow dokumentiert
- Frontend-Seiten und Deploy beschrieben
- DB-Schema aktualisiert (neue Tabellen)
- Dateistruktur erweitert (updater/, frontend/, firmware.go, auth.go)
This commit is contained in:
cynfo3000
2026-02-28 22:43:03 +01:00
parent 616a0cb6ac
commit e74cdce0ba
26 changed files with 2857 additions and 241 deletions
+2 -2
View File
@@ -8,7 +8,7 @@ import (
)
func CollectOPNsenseVersion() string {
out, err := exec.Command("opnsense-version").Output()
out, err := exec.Command("/usr/local/sbin/opnsense-version").Output()
if err != nil {
return ""
}
@@ -18,7 +18,7 @@ func CollectOPNsenseVersion() string {
}
func CollectFreeBSDVersion() string {
out, err := exec.Command("freebsd-version").Output()
out, err := exec.Command("/bin/freebsd-version").Output()
if err != nil {
return sysctlGet("kern.osrelease")
}
+6 -2
View File
@@ -15,6 +15,8 @@ import (
"github.com/cynfo/rmm-agent/ws"
)
var Version = "dev"
func main() {
cfgPath := flag.String("config", "config.yaml", "Pfad zur Konfigurationsdatei")
insecure := flag.Bool("insecure", false, "TLS-Zertifikatpruefung deaktivieren")
@@ -29,7 +31,7 @@ func main() {
cfg.Insecure = true
}
log.Printf("RMM Agent startet: %s", cfg.AgentName)
log.Printf("RMM Agent v%s startet: %s", Version, cfg.AgentName)
log.Printf("Backend: %s, Intervall: %ds", cfg.BackendURL, cfg.IntervalSeconds)
c := client.New(cfg.BackendURL, cfg.APIKey, cfg.Insecure)
@@ -57,6 +59,7 @@ func main() {
"hostname": hostname,
"ip": getLocalIP(),
"opnsense_version": collector.CollectOPNsenseVersion(),
"agent_version": Version,
}
if agentID != "" {
regReq["agent_id"] = agentID
@@ -124,7 +127,8 @@ func sendHeartbeat(c *client.Client, agentID string) error {
cronJobs := collector.CollectCron()
req := map[string]interface{}{
"agent_id": agentID,
"agent_id": agentID,
"agent_version": Version,
"system_data": map[string]interface{}{
"agent_id": agentID,
"hostname": collector.CollectHostname(),
+28 -7
View File
@@ -99,7 +99,6 @@ func (c *Client) reconnectLoop() {
select {
case <-time.After(backoff):
// Exponential backoff mit Maximum
backoff = time.Duration(float64(backoff) * 1.5)
if backoff > maxBackoff {
backoff = maxBackoff
@@ -110,14 +109,38 @@ func (c *Client) reconnectLoop() {
continue
}
// Verbindung erfolgreich - backoff zurücksetzen
backoff = c.reconnectInterval
connStart := time.Now()
log.Println("WebSocket-Verbindung hergestellt")
// Ping/Pong und Message-Handling starten
c.runConnection()
log.Println("WebSocket-Verbindung getrennt")
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
}
}
}
@@ -206,9 +229,7 @@ func (c *Client) runConnection() {
go c.handleMessage(msg)
case err := <-errChan:
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("WebSocket-Fehler: %v", err)
}
log.Printf("WebSocket-Lesefehler: %v", err)
return
}
}
+85
View File
@@ -58,6 +58,8 @@ func (h *Handler) HandleCommand(msg Message) {
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)
@@ -558,6 +560,89 @@ func (h *Handler) parsePkgUpgrade(output string) []map[string]string {
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)