package main import ( "archive/zip" "bytes" "crypto/sha256" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "runtime" "strings" "time" "gopkg.in/yaml.v3" ) type Config struct { BackendURL string `yaml:"backend_url"` APIKey string `yaml:"api_key"` AgentName string `yaml:"agent_name"` Insecure bool `yaml:"insecure"` } // Plattform-spezifische Pfade und Befehle var ( agentBinaryPath string updaterBinaryPath string configPath string agentIDPath string platformName string // Dateinamen im ZIP-Bundle agentZipName string updaterZipName string ) func init() { switch runtime.GOOS { case "freebsd": agentBinaryPath = "/usr/local/rmm/rmm-agent" updaterBinaryPath = "/usr/local/rmm/rmm-updater" configPath = "/usr/local/rmm/config.yaml" agentIDPath = "/usr/local/rmm/agent_id" platformName = "freebsd" agentZipName = "rmm-agent" updaterZipName = "rmm-updater-freebsd" default: // linux agentBinaryPath = "/usr/local/bin/rmm-agent" updaterBinaryPath = "/usr/local/bin/rmm-updater" configPath = "/etc/rmm/config.yaml" agentIDPath = "/etc/rmm/agent_id" platformName = "linux" agentZipName = "rmm-agent-linux" updaterZipName = "rmm-updater-linux" } } func stopAgent() error { switch runtime.GOOS { case "freebsd": return exec.Command("/usr/sbin/service", "rmm_agent", "stop").Run() default: return exec.Command("/usr/bin/systemctl", "stop", "rmm-agent").Run() } } func startAgent() error { switch runtime.GOOS { case "freebsd": return exec.Command("/usr/sbin/service", "rmm_agent", "start").Run() default: return exec.Command("/usr/bin/systemctl", "start", "rmm-agent").Run() } } func restartUpdater() { switch runtime.GOOS { case "freebsd": exec.Command("/usr/sbin/service", "rmm_updater", "restart").Run() default: exec.Command("/usr/bin/systemctl", "restart", "rmm-updater").Run() } } const ( checkInterval = 60 * time.Second logPrefix = "[rmm-updater] " ) func main() { log.SetPrefix(logPrefix) log.SetFlags(log.LstdFlags) log.Printf("RMM Updater gestartet (platform=%s, agent=%s, updater=%s)", platformName, agentBinaryPath, updaterBinaryPath) cfg, err := loadConfig() if err != nil { log.Fatalf("Config laden fehlgeschlagen: %v", err) } agentID, err := loadAgentID() if err != nil { log.Fatalf("Agent-ID laden fehlgeschlagen: %v", err) } log.Printf("Backend: %s, Agent: %s (%s)", cfg.BackendURL, cfg.AgentName, agentID) client := &http.Client{ Timeout: 120 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.Insecure}, }, } for { checkAndUpdate(client, cfg, agentID) time.Sleep(checkInterval) } } func loadConfig() (*Config, error) { data, err := os.ReadFile(configPath) if err != nil { return nil, err } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, err } cfg.Insecure = true return &cfg, nil } func loadAgentID() (string, error) { data, err := os.ReadFile(agentIDPath) if err != nil { return "", fmt.Errorf("agent_id nicht gefunden: %v", err) } id := strings.TrimSpace(string(data)) if id == "" { return "", fmt.Errorf("agent_id leer") } return id, nil } func checkAndUpdate(client *http.Client, cfg *Config, agentID string) { // Firmware-Info vom Backend holen (plattformspezifisch) url := fmt.Sprintf("%s/api/v1/firmware?platform=%s", cfg.BackendURL, platformName) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("X-API-Key", cfg.APIKey) resp, err := client.Do(req) if err != nil { log.Printf("Firmware-Info Abfrage fehlgeschlagen: %v", err) return } defer resp.Body.Close() if resp.StatusCode != 200 { return } var info struct { Available bool `json:"available"` Version string `json:"version"` Hash string `json:"hash"` Size int `json:"size"` } if err := json.NewDecoder(resp.Body).Decode(&info); err != nil || !info.Available { return } // Pruefen ob lokaler Agent schon den gleichen Hash hat if localHash, err := hashFile(agentBinaryPath); err == nil && localHash == info.Hash { return // Schon aktuell } // Update angefordert? agentURL := fmt.Sprintf("%s/api/v1/agents/%s", cfg.BackendURL, agentID) agentReq, _ := http.NewRequest("GET", agentURL, nil) agentReq.Header.Set("X-API-Key", cfg.APIKey) agentResp, err := client.Do(agentReq) if err != nil { return } defer agentResp.Body.Close() var agentInfo struct { Agent struct { UpdateRequested bool `json:"update_requested"` } `json:"agent"` } if err := json.NewDecoder(agentResp.Body).Decode(&agentInfo); err != nil { return } if !agentInfo.Agent.UpdateRequested { return } log.Printf("Update verfuegbar: v%s (%d bytes, platform=%s)", info.Version, info.Size, platformName) if err := downloadAndApply(client, cfg, agentID, info.Hash); err != nil { log.Printf("Update fehlgeschlagen: %v", err) return } log.Printf("Update auf v%s erfolgreich", info.Version) } func downloadAndApply(client *http.Client, cfg *Config, agentID, expectedHash string) error { url := fmt.Sprintf("%s/api/v1/firmware/download?platform=%s", cfg.BackendURL, platformName) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("X-API-Key", cfg.APIKey) resp, err := client.Do(req) if err != nil { return fmt.Errorf("download fehlgeschlagen: %v", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("download HTTP %d", resp.StatusCode) } bundle, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("download lesen fehlgeschlagen: %v", err) } // Hash des gesamten Bundles verifizieren hash := sha256.Sum256(bundle) hashStr := hex.EncodeToString(hash[:]) if hashStr != expectedHash { return fmt.Errorf("hash mismatch: erwartet %s, bekommen %s", expectedHash[:16], hashStr[:16]) } log.Printf("Bundle heruntergeladen (%d bytes, Hash OK)", len(bundle)) // Versuche als ZIP zu entpacken agentBinary, updaterBinary, err := extractZipBundle(bundle) if err != nil { // Fallback: Kein ZIP — einzelne Agent-Binary (Abwaertskompatibilitaet) log.Printf("Kein ZIP-Bundle, verwende als einzelne Agent-Binary") agentBinary = bundle updaterBinary = nil } // Agent stoppen log.Println("Agent wird gestoppt...") stopAgent() time.Sleep(2 * time.Second) // Agent-Binary ersetzen if err := replaceBinary(agentBinaryPath, agentBinary); err != nil { return fmt.Errorf("agent binary ersetzen fehlgeschlagen: %v", err) } log.Println("Agent-Binary aktualisiert") // Agent starten log.Println("Agent wird gestartet...") if err := startAgent(); err != nil { log.Printf("WARNUNG: Agent-Start fehlgeschlagen: %v — versuche Rollback", err) rollbackBinary(agentBinaryPath) startAgent() return fmt.Errorf("agent start fehlgeschlagen") } // Update-Flag loeschen clearURL := fmt.Sprintf("%s/api/v1/agents/%s/request-update", cfg.BackendURL, agentID) clearReq, _ := http.NewRequest("DELETE", clearURL, nil) clearReq.Header.Set("X-API-Key", cfg.APIKey) client.Do(clearReq) // Updater-Binary ersetzen (wenn im Bundle enthalten) if updaterBinary != nil { log.Println("Updater-Binary wird aktualisiert...") if err := replaceBinary(updaterBinaryPath, updaterBinary); err != nil { log.Printf("WARNUNG: Updater-Binary ersetzen fehlgeschlagen: %v", err) } else { log.Println("Updater-Binary aktualisiert, starte neu...") // Updater neustart — dieser Prozess wird beendet und neu gestartet go func() { time.Sleep(1 * time.Second) restartUpdater() }() } } return nil } func extractZipBundle(data []byte) (agentBin []byte, updaterBin []byte, err error) { reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return nil, nil, fmt.Errorf("kein gueltiges ZIP: %v", err) } for _, f := range reader.File { rc, err := f.Open() if err != nil { continue } content, err := io.ReadAll(rc) rc.Close() if err != nil { continue } switch f.Name { case agentZipName: agentBin = content log.Printf("ZIP: Agent-Binary gefunden (%s, %d bytes)", f.Name, len(content)) case updaterZipName: updaterBin = content log.Printf("ZIP: Updater-Binary gefunden (%s, %d bytes)", f.Name, len(content)) default: log.Printf("ZIP: Unbekannte Datei ignoriert: %s", f.Name) } } if agentBin == nil { return nil, nil, fmt.Errorf("agent binary nicht im ZIP gefunden (erwartet: %s)", agentZipName) } return agentBin, updaterBin, nil } func replaceBinary(path string, newContent []byte) error { backupPath := path + ".bak" // Backup erstellen if _, err := os.Stat(path); err == nil { os.Rename(path, backupPath) } // Neue Binary schreiben if err := os.WriteFile(path, newContent, 0755); err != nil { // Rollback os.Rename(backupPath, path) return err } // Backup loeschen os.Remove(backupPath) return nil } func rollbackBinary(path string) { backupPath := path + ".bak" if _, err := os.Stat(backupPath); err == nil { os.Rename(backupPath, path) } } func hashFile(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]), nil }