Initial commit: RMM Agent + Backend

- Go Agent (FreeBSD/amd64) fuer OPNsense
- Go Backend (Linux/amd64) mit REST API + SQLite
- Collector: Hardware, CPU, Memory, Disks, Network, Services,
  WireGuard, DHCP (KEA/ISC/dnsmasq), Routes, Gateways,
  Certificates, Plugins, Updates
- Persistente Agent-ID
- TLS + API-Key Auth
- WebSocket-Infrastruktur (WIP): bidirektionaler Command-Kanal,
  TCP-Tunnel fuer Remote-Zugriff auf Firewall-WebUI
- Lastenheft und README
This commit is contained in:
cynfo3000
2026-02-28 07:38:14 +01:00
commit 6120d0cffa
35 changed files with 3992 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
package api
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"strings"
"time"
"github.com/cynfo/rmm-backend/db"
"github.com/cynfo/rmm-backend/models"
)
type Handler struct {
db *db.Database
}
func NewHandler(database *db.Database) *Handler {
return &Handler{db: database}
}
// SetupRoutes - Alle API-Routes registrieren
func (h *Handler) SetupRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /api/v1/agent/register", h.registerAgent)
mux.HandleFunc("POST /api/v1/agent/heartbeat", h.heartbeat)
mux.HandleFunc("GET /api/v1/agents", h.listAgents)
mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent)
mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData)
mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent)
}
func generateID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
// POST /api/v1/agent/register
func (h *Handler) registerAgent(w http.ResponseWriter, r *http.Request) {
var req models.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
return
}
if req.Name == "" || req.Hostname == "" {
writeError(w, http.StatusBadRequest, "Name und Hostname sind erforderlich")
return
}
// Agent-ID bestimmen: vom Agent mitgeliefert > nach Hostname suchen > neu generieren
agentID := req.AgentID
isExisting := false
if agentID != "" {
// Pruefen ob die ID schon in der DB existiert
if existing, _, err := h.db.GetAgent(agentID); err == nil && existing != nil {
isExisting = true
}
}
if agentID == "" {
// Nach bestehendem Agent mit gleichem Hostname suchen
if existing, err := h.db.GetAgentByHostname(req.Hostname); err == nil && existing != nil {
agentID = existing.ID
isExisting = true
}
}
if agentID == "" {
agentID = generateID()
}
agent := &models.Agent{
ID: agentID,
Name: req.Name,
Hostname: req.Hostname,
IP: req.IP,
OPNsenseVersion: req.OPNsenseVersion,
RegisteredAt: time.Now().UTC(),
}
if err := h.db.RegisterAgent(agent); err != nil {
log.Printf("Fehler bei Registrierung: %v", err)
writeError(w, http.StatusInternalServerError, "Registrierung fehlgeschlagen")
return
}
action := "registriert"
if isExisting {
action = "re-registriert"
}
log.Printf("Agent %s: %s (%s)", action, agent.Name, agent.ID)
writeJSON(w, http.StatusCreated, models.RegisterResponse{
ID: agent.ID,
Message: "Agent erfolgreich registriert",
})
}
// POST /api/v1/agent/heartbeat
func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) {
var req models.HeartbeatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
return
}
if req.AgentID == "" {
writeError(w, http.StatusBadRequest, "agent_id erforderlich")
return
}
if err := h.db.SaveSystemData(req.AgentID, &req.SystemData); err != nil {
if strings.Contains(err.Error(), "nicht gefunden") {
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
return
}
log.Printf("Fehler bei Heartbeat: %v", err)
writeError(w, http.StatusInternalServerError, "Heartbeat fehlgeschlagen")
return
}
writeJSON(w, http.StatusOK, models.HeartbeatResponse{Message: "OK"})
}
// GET /api/v1/agents
func (h *Handler) listAgents(w http.ResponseWriter, r *http.Request) {
agents, err := h.db.GetAgents()
if err != nil {
log.Printf("Fehler beim Laden der Agents: %v", err)
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
if agents == nil {
agents = []models.Agent{}
}
writeJSON(w, http.StatusOK, agents)
}
// GET /api/v1/agents/{id}
func (h *Handler) getAgent(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
agent, sysData, err := h.db.GetAgent(id)
if err != nil {
log.Printf("Fehler: %v", err)
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
if agent == nil {
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
return
}
result := map[string]interface{}{
"agent": agent,
"system_data": sysData,
}
writeJSON(w, http.StatusOK, result)
}
// GET /api/v1/agents/{id}/system
func (h *Handler) getSystemData(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
data, err := h.db.GetSystemData(id)
if err != nil {
log.Printf("Fehler: %v", err)
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
if data == nil {
writeError(w, http.StatusNotFound, "Keine Systemdaten vorhanden")
return
}
writeJSON(w, http.StatusOK, data)
}
// DELETE /api/v1/agents/{id}
func (h *Handler) deleteAgent(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
deleted, err := h.db.DeleteAgent(id)
if err != nil {
log.Printf("Fehler beim Loeschen: %v", err)
writeError(w, http.StatusInternalServerError, "Loeschen fehlgeschlagen")
return
}
if !deleted {
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
return
}
log.Printf("Agent geloescht: %s", id)
writeJSON(w, http.StatusOK, map[string]string{"message": "Agent geloescht"})
}
+33
View File
@@ -0,0 +1,33 @@
package api
import (
"log"
"net/http"
"time"
)
// APIKeyAuth - Middleware fuer API-Key Authentifizierung
func APIKeyAuth(validKeys []string, next http.Handler) http.Handler {
keySet := make(map[string]bool, len(validKeys))
for _, k := range validKeys {
keySet[k] = true
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if key == "" || !keySet[key] {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Logging - Request-Logging Middleware
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
+49
View File
@@ -0,0 +1,49 @@
package config
import (
"os"
"gopkg.in/yaml.v3"
)
type Config struct {
ListenAddr string `yaml:"listen_addr"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
APIKeys []string `yaml:"api_keys"`
DBPath string `yaml:"db_path"`
}
func Load(path string) (*Config, error) {
cfg := &Config{
ListenAddr: ":8443",
TLSCert: "certs/server.crt",
TLSKey: "certs/server.key",
DBPath: "rmm.db",
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, err
}
// Env overrides
if v := os.Getenv("RMM_LISTEN_ADDR"); v != "" {
cfg.ListenAddr = v
}
if v := os.Getenv("RMM_TLS_CERT"); v != "" {
cfg.TLSCert = v
}
if v := os.Getenv("RMM_TLS_KEY"); v != "" {
cfg.TLSKey = v
}
if v := os.Getenv("RMM_DB_PATH"); v != "" {
cfg.DBPath = v
}
return cfg, nil
}
+235
View File
@@ -0,0 +1,235 @@
package db
import (
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/cynfo/rmm-backend/models"
_ "modernc.org/sqlite"
)
type Database struct {
db *sql.DB
}
func New(dbPath string) (*Database, error) {
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("Datenbank oeffnen: %w", err)
}
// WAL-Modus fuer bessere Performance
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
return nil, err
}
d := &Database{db: db}
if err := d.migrate(); err != nil {
return nil, fmt.Errorf("Migration fehlgeschlagen: %w", err)
}
return d, nil
}
func (d *Database) migrate() error {
schema := `
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
hostname TEXT NOT NULL,
ip TEXT NOT NULL,
opnsense_version TEXT NOT NULL DEFAULT '',
registered_at TEXT NOT NULL,
last_heartbeat TEXT
);
CREATE TABLE IF NOT EXISTS system_data (
agent_id TEXT PRIMARY KEY,
data_json TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
);
`
_, err := d.db.Exec(schema)
return err
}
func (d *Database) Close() error {
return d.db.Close()
}
// RegisterAgent - Neuen Agent registrieren oder bestehenden aktualisieren
func (d *Database) RegisterAgent(agent *models.Agent) error {
_, err := d.db.Exec(`
INSERT INTO agents (id, name, hostname, ip, opnsense_version, registered_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name=excluded.name,
hostname=excluded.hostname,
ip=excluded.ip,
opnsense_version=excluded.opnsense_version
`, agent.ID, agent.Name, agent.Hostname, agent.IP, agent.OPNsenseVersion, agent.RegisteredAt.UTC().Format(time.RFC3339))
return err
}
// SaveSystemData - Systemdaten speichern und Heartbeat aktualisieren
func (d *Database) SaveSystemData(agentID string, data *models.SystemData) error {
now := time.Now().UTC().Format(time.RFC3339)
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("JSON Marshal: %w", err)
}
tx, err := d.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Heartbeat Zeitstempel aktualisieren
res, err := tx.Exec("UPDATE agents SET last_heartbeat = ? WHERE id = ?", now, agentID)
if err != nil {
return err
}
rows, _ := res.RowsAffected()
if rows == 0 {
return fmt.Errorf("Agent %s nicht gefunden", agentID)
}
// Systemdaten upsert
_, err = tx.Exec(`
INSERT INTO system_data (agent_id, data_json, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(agent_id) DO UPDATE SET
data_json=excluded.data_json,
updated_at=excluded.updated_at
`, agentID, string(jsonData), now)
if err != nil {
return err
}
return tx.Commit()
}
// GetAgents - Alle Agents auflisten
func (d *Database) GetAgents() ([]models.Agent, error) {
rows, err := d.db.Query("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents ORDER BY name")
if err != nil {
return nil, err
}
defer rows.Close()
var agents []models.Agent
for rows.Next() {
var a models.Agent
var regAt string
var lastHB sql.NullString
if err := rows.Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &regAt, &lastHB); err != nil {
return nil, err
}
a.RegisteredAt, _ = time.Parse(time.RFC3339, regAt)
if lastHB.Valid {
t, _ := time.Parse(time.RFC3339, lastHB.String)
a.LastHeartbeat = &t
}
agents = append(agents, a)
}
return agents, rows.Err()
}
// GetAgent - Einzelnen Agent mit Systemdaten
func (d *Database) GetAgentByHostname(hostname string) (*models.Agent, error) {
var a models.Agent
var regAt string
var lastHB sql.NullString
err := d.db.QueryRow("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents WHERE hostname = ? LIMIT 1", hostname).
Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &regAt, &lastHB)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
a.RegisteredAt, _ = time.Parse(time.RFC3339, regAt)
if lastHB.Valid {
t, _ := time.Parse(time.RFC3339, lastHB.String)
a.LastHeartbeat = &t
}
return &a, nil
}
func (d *Database) GetAgent(id string) (*models.Agent, *models.SystemData, error) {
var a models.Agent
var regAt string
var lastHB sql.NullString
err := d.db.QueryRow("SELECT id, name, hostname, ip, opnsense_version, registered_at, last_heartbeat FROM agents WHERE id = ?", id).
Scan(&a.ID, &a.Name, &a.Hostname, &a.IP, &a.OPNsenseVersion, &regAt, &lastHB)
if err == sql.ErrNoRows {
return nil, nil, nil
}
if err != nil {
return nil, nil, err
}
a.RegisteredAt, _ = time.Parse(time.RFC3339, regAt)
if lastHB.Valid {
t, _ := time.Parse(time.RFC3339, lastHB.String)
a.LastHeartbeat = &t
}
// Systemdaten laden
var dataJSON sql.NullString
err = d.db.QueryRow("SELECT data_json FROM system_data WHERE agent_id = ?", id).Scan(&dataJSON)
if err != nil && err != sql.ErrNoRows {
return nil, nil, err
}
var sysData *models.SystemData
if dataJSON.Valid {
sysData = &models.SystemData{}
if err := json.Unmarshal([]byte(dataJSON.String), sysData); err != nil {
return &a, nil, nil
}
}
return &a, sysData, nil
}
// GetSystemData - Nur Systemdaten eines Agents
func (d *Database) GetSystemData(agentID string) (*models.SystemData, error) {
var dataJSON string
err := d.db.QueryRow("SELECT data_json FROM system_data WHERE agent_id = ?", agentID).Scan(&dataJSON)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
var data models.SystemData
if err := json.Unmarshal([]byte(dataJSON), &data); err != nil {
return nil, err
}
return &data, nil
}
// DeleteAgent - Agent und zugehoerige Daten loeschen
func (d *Database) DeleteAgent(id string) (bool, error) {
// Foreign Key Cascade aktivieren
d.db.Exec("PRAGMA foreign_keys = ON")
res, err := d.db.Exec("DELETE FROM agents WHERE id = ?", id)
if err != nil {
return false, err
}
rows, _ := res.RowsAffected()
// Sicherheitshalber auch system_data loeschen (falls kein CASCADE)
d.db.Exec("DELETE FROM system_data WHERE agent_id = ?", id)
return rows > 0, nil
}
+25
View File
@@ -0,0 +1,25 @@
module github.com/cynfo/rmm-backend
go 1.22
require (
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.29.1
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.16.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.41.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)
+45
View File
@@ -0,0 +1,45 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA=
modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+132
View File
@@ -0,0 +1,132 @@
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/cynfo/rmm-backend/api"
"github.com/cynfo/rmm-backend/config"
"github.com/cynfo/rmm-backend/db"
)
func main() {
cfgPath := "config.yaml"
if len(os.Args) > 1 {
cfgPath = os.Args[1]
}
cfg, err := config.Load(cfgPath)
if err != nil {
log.Fatalf("Konfiguration laden fehlgeschlagen: %v", err)
}
// TLS-Zertifikate pruefen/generieren
if err := ensureTLS(cfg.TLSCert, cfg.TLSKey); err != nil {
log.Fatalf("TLS-Setup fehlgeschlagen: %v", err)
}
// Datenbank initialisieren
database, err := db.New(cfg.DBPath)
if err != nil {
log.Fatalf("Datenbank-Fehler: %v", err)
}
defer database.Close()
// Router aufbauen
mux := http.NewServeMux()
handler := api.NewHandler(database)
handler.SetupRoutes(mux)
// Middleware-Chain: Logging -> Auth -> Handler
var chain http.Handler = mux
chain = api.APIKeyAuth(cfg.APIKeys, chain)
chain = api.Logging(chain)
log.Printf("RMM Backend startet auf %s (TLS)", cfg.ListenAddr)
log.Printf("API-Keys konfiguriert: %d", len(cfg.APIKeys))
if err := http.ListenAndServeTLS(cfg.ListenAddr, cfg.TLSCert, cfg.TLSKey, chain); err != nil {
log.Fatalf("Server-Fehler: %v", err)
}
}
// ensureTLS generiert self-signed Zertifikate wenn keine vorhanden
func ensureTLS(certPath, keyPath string) error {
if _, err := os.Stat(certPath); err == nil {
if _, err := os.Stat(keyPath); err == nil {
log.Println("TLS-Zertifikate gefunden")
return nil
}
}
log.Println("Generiere self-signed TLS-Zertifikat...")
// Verzeichnis erstellen
if dir := filepath.Dir(certPath); dir != "" && dir != "." {
os.MkdirAll(dir, 0755)
}
if dir := filepath.Dir(keyPath); dir != "" && dir != "." {
os.MkdirAll(dir, 0755)
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return fmt.Errorf("Key generieren: %w", err)
}
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
template := x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
Organization: []string{"RMM Backend"},
CommonName: "rmm-backend",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour * 10), // 10 Jahre
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("192.168.85.13")},
DNSNames: []string{"localhost", "rmm-backend"},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
return fmt.Errorf("Zertifikat erstellen: %w", err)
}
certFile, err := os.Create(certPath)
if err != nil {
return err
}
defer certFile.Close()
pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyBytes, err := x509.MarshalECPrivateKey(key)
if err != nil {
return err
}
keyFile, err := os.OpenFile(keyPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer keyFile.Close()
pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
log.Printf("TLS-Zertifikat generiert: %s, %s", certPath, keyPath)
return nil
}
+29
View File
@@ -0,0 +1,29 @@
package models
import "time"
// Agent repraesentiert einen registrierten OPNsense-Agent
type Agent struct {
ID string `json:"id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
IP string `json:"ip"`
OPNsenseVersion string `json:"opnsense_version"`
RegisteredAt time.Time `json:"registered_at"`
LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"`
}
// RegisterRequest - Agent-Registrierung
type RegisterRequest struct {
AgentID string `json:"agent_id,omitempty"`
Name string `json:"name"`
Hostname string `json:"hostname"`
IP string `json:"ip"`
OPNsenseVersion string `json:"opnsense_version"`
}
// RegisterResponse
type RegisterResponse struct {
ID string `json:"id"`
Message string `json:"message"`
}
+164
View File
@@ -0,0 +1,164 @@
package models
// SystemData - Alle Systemdaten die der Agent sendet
type SystemData struct {
AgentID string `json:"agent_id"`
Hostname string `json:"hostname"`
OPNsenseVersion string `json:"opnsense_version"`
FreeBSDVersion string `json:"freebsd_version"`
UptimeSeconds int64 `json:"uptime_seconds"`
Hardware HardwareInfo `json:"hardware"`
CPU CPUInfo `json:"cpu"`
Memory MemoryInfo `json:"memory"`
Disks []DiskInfo `json:"disks"`
NetworkInterfaces []NetworkInterface `json:"network_interfaces"`
Services []ServiceInfo `json:"services"`
WireGuard []WireGuardTunnel `json:"wireguard,omitempty"`
DHCP *DHCPInfo `json:"dhcp,omitempty"`
Routes []Route `json:"routes,omitempty"`
Gateways []GatewayInfo `json:"gateways,omitempty"`
Certificates []CertificateInfo `json:"certificates,omitempty"`
Plugins []PluginInfo `json:"plugins,omitempty"`
Updates *UpdateInfo `json:"updates,omitempty"`
}
type WireGuardTunnel struct {
Interface string `json:"interface"`
PublicKey string `json:"public_key"`
ListenPort int `json:"listen_port"`
Peers []WireGuardPeer `json:"peers"`
}
type WireGuardPeer struct {
PublicKey string `json:"public_key"`
Endpoint string `json:"endpoint"`
AllowedIPs []string `json:"allowed_ips"`
LatestHandshake int64 `json:"latest_handshake_epoch"`
HandshakeAge string `json:"handshake_age"`
TransferRx int64 `json:"transfer_rx_bytes"`
TransferTx int64 `json:"transfer_tx_bytes"`
Keepalive int `json:"persistent_keepalive"`
Status string `json:"status"`
}
type DHCPInfo struct {
Server string `json:"server"`
Leases []DHCPLease `json:"leases"`
}
type DHCPLease struct {
IP string `json:"ip"`
MAC string `json:"mac"`
Hostname string `json:"hostname"`
Start string `json:"start,omitempty"`
End string `json:"end,omitempty"`
Status string `json:"status"`
Pool string `json:"pool,omitempty"`
}
type HardwareInfo struct {
Manufacturer string `json:"manufacturer"`
Model string `json:"model"`
Serial string `json:"serial"`
BIOSVersion string `json:"bios_version"`
}
type CPUInfo struct {
Model string `json:"model"`
Cores int `json:"cores"`
Threads int `json:"threads"`
FreqMHz int `json:"freq_mhz"`
UsagePercent float64 `json:"usage_percent"`
}
type MemoryInfo struct {
TotalBytes int64 `json:"total_bytes"`
UsedBytes int64 `json:"used_bytes"`
FreeBytes int64 `json:"free_bytes"`
}
type DiskInfo struct {
Filesystem string `json:"filesystem"`
TotalBytes int64 `json:"total_bytes"`
UsedBytes int64 `json:"used_bytes"`
FreeBytes int64 `json:"free_bytes"`
MountPoint string `json:"mount_point"`
}
type NetworkInterface struct {
Name string `json:"name"`
Role string `json:"role"`
IP string `json:"ip"`
MAC string `json:"mac"`
Status string `json:"status"`
RxBytes int64 `json:"rx_bytes"`
TxBytes int64 `json:"tx_bytes"`
}
type ServiceInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
}
type Route struct {
Destination string `json:"destination"`
Gateway string `json:"gateway"`
Flags string `json:"flags"`
Interface string `json:"interface"`
}
type GatewayInfo struct {
Name string `json:"name"`
Address string `json:"address"`
Status string `json:"status"`
Loss string `json:"loss,omitempty"`
Delay string `json:"delay,omitempty"`
Stddev string `json:"stddev,omitempty"`
Monitor string `json:"monitor,omitempty"`
}
type CertificateInfo struct {
Name string `json:"name"`
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore string `json:"not_before"`
NotAfter string `json:"not_after"`
DaysLeft int `json:"days_left"`
Serial string `json:"serial"`
SAN string `json:"san,omitempty"`
KeyUsage string `json:"key_usage,omitempty"`
IsCA bool `json:"is_ca"`
Status string `json:"status"`
Source string `json:"source"`
}
type PluginInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
}
type UpdateInfo struct {
UpdateAvailable bool `json:"update_available"`
PendingCount int `json:"pending_count"`
Updates []PendingUpdateInfo `json:"updates"`
OPNsenseUpdate string `json:"opnsense_update,omitempty"`
}
type PendingUpdateInfo struct {
Package string `json:"package"`
CurrentVer string `json:"current_version"`
NewVer string `json:"new_version"`
}
// HeartbeatRequest
type HeartbeatRequest struct {
AgentID string `json:"agent_id"`
SystemData SystemData `json:"system_data"`
}
// HeartbeatResponse
type HeartbeatResponse struct {
Message string `json:"message"`
}