feat(fw): Renderer-Rewrite + auto-apply + Anti-Lockout

internal/firewall/firewall.go komplett neu: joint zone-iface-mapping
(network_interfaces.role), address objects + groups (members
expandiert), services + groups, rules, nat-rules. Output: einheitliche
View mit Legs (rule × service cross-product) damit das Template kein
sub-template/dict braucht.

Template:
* Anti-Lockout-Block am input-chain-Top (SSH+443 immer erlaubt,
  KANN nicht von Custom-Rules overruled werden — User-Wunsch).
* Rules: pro Leg eine nft-Zeile mit iif/oif sets, ip saddr/daddr,
  proto+dport, optional log-prefix.
* prerouting_nat: iteriert dnat-Rules.
* postrouting_nat: snat + masquerade.

Auto-apply: FirewallHandler bekommt einen Reloader-Hook der nach
jedem POST/PUT/DELETE aufgerufen wird. main.go injected
firewall.New(pool).Render — schreibt + sudo nft -f.

Sudoers (/etc/sudoers.d/edgeguard): NOPASSWD für 'nft -f
/etc/edgeguard/nftables.d/ruleset.nft'. configgen.ReloadService
nutzt jetzt sudo (haproxy reload klappte vorher nicht aus dem
edgeguard-User).

Frontend (Sweep): style={{ marginBottom: 16 }} → className="mb-16"
in allen 7 Firewall-Tabs — User-Feedback "globales CSS statt inline".

Live auf 89.163.205.6: nft list table inet edgeguard zeigt
Anti-Lockout + Baseline + Cluster-Peer-Set + (jetzt noch leere)
Custom-Rules-Sektion. render-config postinst-mäßig sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-10 13:34:06 +02:00
parent e2bdce9271
commit 1b2c0d7411
13 changed files with 542 additions and 161 deletions

View File

@@ -1,8 +1,10 @@
package handlers
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"strconv"
"strings"
@@ -35,6 +37,13 @@ type FirewallHandler struct {
NATRules *firewall.NATRulesRepo
Audit *audit.Repo
NodeID string
// Reloader regenerates and applies the nft ruleset. Called after
// each mutation so the kernel ruleset is always in sync with the
// DB. Errors are logged but don't fail the API call (the row is
// already committed). Wire to internal/firewall.Generator.Render
// in main.go; pass nil during tests.
Reloader func(ctx context.Context) error
}
func NewFirewallHandler(
@@ -46,12 +55,27 @@ func NewFirewallHandler(
nat *firewall.NATRulesRepo,
a *audit.Repo,
nodeID string,
reloader func(ctx context.Context) error,
) *FirewallHandler {
return &FirewallHandler{
AddrObjects: ao, AddrGroups: ag,
Services: sv, ServiceGroups: sg,
Rules: rl, NATRules: nat,
Audit: a, NodeID: nodeID,
Reloader: reloader,
}
}
// reload runs the configured Reloader (if any). Errors don't fail
// the surrounding API call — the DB row is already committed and
// the operator can re-trigger via `edgeguard-ctl render-config`.
func (h *FirewallHandler) reload(ctx context.Context, op string) {
if h.Reloader == nil {
return
}
if err := h.Reloader(ctx); err != nil {
slog.Warn("firewall: nft reload failed",
"op", op, "error", err)
}
}
@@ -145,7 +169,7 @@ func (h *FirewallHandler) CreateAddrObj(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_obj.create", req.Name, out, h.NodeID)
response.Created(c, out)
response.Created(c, out); h.reload(c.Request.Context(), "create")
}
func (h *FirewallHandler) UpdateAddrObj(c *gin.Context) {
@@ -172,7 +196,7 @@ func (h *FirewallHandler) UpdateAddrObj(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_obj.update", req.Name, out, h.NodeID)
response.OK(c, out)
response.OK(c, out); h.reload(c.Request.Context(), "update")
}
func (h *FirewallHandler) DeleteAddrObj(c *gin.Context) {
@@ -190,7 +214,7 @@ func (h *FirewallHandler) DeleteAddrObj(c *gin.Context) {
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_obj.delete",
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
response.NoContent(c)
response.NoContent(c); h.reload(c.Request.Context(), "delete")
}
// ── Address Groups ─────────────────────────────────────────────────────
@@ -233,7 +257,7 @@ func (h *FirewallHandler) CreateAddrGrp(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_grp.create", req.Name, out, h.NodeID)
response.Created(c, out)
response.Created(c, out); h.reload(c.Request.Context(), "create")
}
func (h *FirewallHandler) UpdateAddrGrp(c *gin.Context) {
@@ -256,7 +280,7 @@ func (h *FirewallHandler) UpdateAddrGrp(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_grp.update", req.Name, out, h.NodeID)
response.OK(c, out)
response.OK(c, out); h.reload(c.Request.Context(), "update")
}
func (h *FirewallHandler) DeleteAddrGrp(c *gin.Context) {
@@ -274,7 +298,7 @@ func (h *FirewallHandler) DeleteAddrGrp(c *gin.Context) {
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_grp.delete",
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
response.NoContent(c)
response.NoContent(c); h.reload(c.Request.Context(), "delete")
}
// ── Services ───────────────────────────────────────────────────────────
@@ -317,7 +341,7 @@ func (h *FirewallHandler) CreateService(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.service.create", req.Name, out, h.NodeID)
response.Created(c, out)
response.Created(c, out); h.reload(c.Request.Context(), "create")
}
func (h *FirewallHandler) UpdateService(c *gin.Context) {
@@ -340,7 +364,7 @@ func (h *FirewallHandler) UpdateService(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.service.update", req.Name, out, h.NodeID)
response.OK(c, out)
response.OK(c, out); h.reload(c.Request.Context(), "update")
}
func (h *FirewallHandler) DeleteService(c *gin.Context) {
@@ -358,7 +382,7 @@ func (h *FirewallHandler) DeleteService(c *gin.Context) {
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.service.delete",
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
response.NoContent(c)
response.NoContent(c); h.reload(c.Request.Context(), "delete")
}
// ── Service Groups ─────────────────────────────────────────────────────
@@ -401,7 +425,7 @@ func (h *FirewallHandler) CreateSvcGrp(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.svc_grp.create", req.Name, out, h.NodeID)
response.Created(c, out)
response.Created(c, out); h.reload(c.Request.Context(), "create")
}
func (h *FirewallHandler) UpdateSvcGrp(c *gin.Context) {
@@ -424,7 +448,7 @@ func (h *FirewallHandler) UpdateSvcGrp(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.svc_grp.update", req.Name, out, h.NodeID)
response.OK(c, out)
response.OK(c, out); h.reload(c.Request.Context(), "update")
}
func (h *FirewallHandler) DeleteSvcGrp(c *gin.Context) {
@@ -442,7 +466,7 @@ func (h *FirewallHandler) DeleteSvcGrp(c *gin.Context) {
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.svc_grp.delete",
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
response.NoContent(c)
response.NoContent(c); h.reload(c.Request.Context(), "delete")
}
// ── Rules ──────────────────────────────────────────────────────────────
@@ -489,7 +513,7 @@ func (h *FirewallHandler) CreateRule(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.rule.create", strconv.FormatInt(out.ID, 10), out, h.NodeID)
response.Created(c, out)
response.Created(c, out); h.reload(c.Request.Context(), "create")
}
func (h *FirewallHandler) UpdateRule(c *gin.Context) {
@@ -516,7 +540,7 @@ func (h *FirewallHandler) UpdateRule(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.rule.update", strconv.FormatInt(id, 10), out, h.NodeID)
response.OK(c, out)
response.OK(c, out); h.reload(c.Request.Context(), "update")
}
func (h *FirewallHandler) DeleteRule(c *gin.Context) {
@@ -534,7 +558,7 @@ func (h *FirewallHandler) DeleteRule(c *gin.Context) {
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.rule.delete",
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
response.NoContent(c)
response.NoContent(c); h.reload(c.Request.Context(), "delete")
}
// ── NAT Rules ──────────────────────────────────────────────────────────
@@ -581,7 +605,7 @@ func (h *FirewallHandler) CreateNAT(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.nat.create", strconv.FormatInt(out.ID, 10), out, h.NodeID)
response.Created(c, out)
response.Created(c, out); h.reload(c.Request.Context(), "create")
}
func (h *FirewallHandler) UpdateNAT(c *gin.Context) {
@@ -608,7 +632,7 @@ func (h *FirewallHandler) UpdateNAT(c *gin.Context) {
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.nat.update", strconv.FormatInt(id, 10), out, h.NodeID)
response.OK(c, out)
response.OK(c, out); h.reload(c.Request.Context(), "update")
}
func (h *FirewallHandler) DeleteNAT(c *gin.Context) {
@@ -626,7 +650,7 @@ func (h *FirewallHandler) DeleteNAT(c *gin.Context) {
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.nat.delete",
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
response.NoContent(c)
response.NoContent(c); h.reload(c.Request.Context(), "delete")
}
// ── Validators ─────────────────────────────────────────────────────────