Files
Debian b031725dfe feat(routes): Static-Routes-Management + Live-View (Networks-Tab)
Migration 0019: static_routes (id, destination, gateway, dev, metric,
table_name, active, comment).

internal/services/staticroutes/:
  - CRUD-Repo
  - Generator schreibt /etc/edgeguard/routes.conf (pipe-format) und
    triggert `sudo systemctl restart edgeguard-routes.service`
  - LiveAll() ruft `ip -j route show table all` und parsed JSON

internal/handlers/routes.go:
  GET /api/v1/routes           — managed (DB)
  POST/PUT/DELETE              — CRUD (re-render + apply on mutate)
  GET /api/v1/routes/live      — kernel-state via ip(8)

postinst:
  - /usr/sbin/edgeguard-apply-routes (root-owned shell-script). Liest
    routes.conf, flusht `proto 250` (= edgeguard), setzt neue Routen
    mit proto 250. Andere Quellen (kernel/dhcp/manuell) bleiben
    unangetastet.
  - /etc/systemd/system/edgeguard-routes.service (Type=oneshot,
    After=network-online.target). Beim Boot automatisch via
    multi-user.target.
  - /etc/iproute2/rt_protos.d/edgeguard.conf — Symbol "edgeguard" =
    250 damit `ip route show proto edgeguard` funktioniert.
    (Debian 13 hat kein /etc/iproute2 default → .d-Pattern statt
    rt_protos-Anhängen.)
  - sudoers: edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl
    restart edgeguard-routes.service

UI: Networks-Page jetzt mit Tabs (Interfaces + Routen). Routes-Tab
hat zwei Cards:
  - Live-Routen (read-only, 30s refresh, `proto edgeguard` farblich
    hervorgehoben)
  - Verwaltete Routen (CRUD-Tabelle, Add/Edit-Modal mit destination/
    gateway/dev/metric/table/active/comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:50:26 +02:00

129 lines
3.2 KiB
Go

package handlers
import (
"context"
"errors"
"log/slog"
"strconv"
"github.com/gin-gonic/gin"
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/staticroutes"
)
// RoutesHandler exposes:
//
// GET /api/v1/routes — verwaltete Routen aus DB
// POST /api/v1/routes — anlegen
// PUT /api/v1/routes/:id — ändern
// DELETE /api/v1/routes/:id — löschen
// GET /api/v1/routes/live — Live-Routen vom Kernel (ip -j route)
type RoutesHandler struct {
Repo *staticroutes.Repo
Renderer *staticroutes.Generator
Audit *audit.Repo
NodeID string
}
func NewRoutesHandler(repo *staticroutes.Repo, gen *staticroutes.Generator,
a *audit.Repo, nodeID string) *RoutesHandler {
return &RoutesHandler{Repo: repo, Renderer: gen, Audit: a, NodeID: nodeID}
}
func (h *RoutesHandler) Register(rg *gin.RouterGroup) {
g := rg.Group("/routes")
g.GET("", h.List)
g.POST("", h.Create)
g.PUT("/:id", h.Update)
g.DELETE("/:id", h.Delete)
g.GET("/live", h.Live)
}
func (h *RoutesHandler) reload(ctx context.Context, op string) {
if h.Renderer == nil {
return
}
if err := h.Renderer.Render(ctx); err != nil {
slog.Warn("routes: render+apply failed", "op", op, "error", err)
}
}
func (h *RoutesHandler) List(c *gin.Context) {
out, err := h.Repo.List(c.Request.Context())
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, gin.H{"routes": out})
}
func (h *RoutesHandler) Create(c *gin.Context) {
var req staticroutes.Route
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err)
return
}
out, err := h.Repo.Create(c.Request.Context(), req)
if err != nil {
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "route.create", out.Destination, out, h.NodeID)
response.Created(c, out)
h.reload(c.Request.Context(), "create")
}
func (h *RoutesHandler) Update(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req staticroutes.Route
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err)
return
}
out, err := h.Repo.Update(c.Request.Context(), id, req)
if err != nil {
if errors.Is(err, staticroutes.ErrNotFound) {
response.NotFound(c, err)
return
}
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "route.update", out.Destination, out, h.NodeID)
response.OK(c, out)
h.reload(c.Request.Context(), "update")
}
func (h *RoutesHandler) Delete(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
if err := h.Repo.Delete(c.Request.Context(), id); err != nil {
if errors.Is(err, staticroutes.ErrNotFound) {
response.NotFound(c, err)
return
}
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "route.delete",
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
response.NoContent(c)
h.reload(c.Request.Context(), "delete")
}
func (h *RoutesHandler) Live(c *gin.Context) {
routes, err := staticroutes.LiveAll(c.Request.Context())
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, gin.H{"routes": routes})
}