internal/handlers/firewall.go: ein FirewallHandler-Struct hält alle
6 Repos + Audit-Ref. Register(authed) mountet 30 Endpoints unter
/api/v1/firewall/{address-objects,address-groups,services,
service-groups,rules,nat-rules}.
Validation:
* Address-Objects: kind=host → ParseIP, network → ParseCIDR,
range → "IP-IP", fqdn → looksLikeFQDN.
* Rules: src/dst max one of (object_id|group_id|cidr); 0 = "any".
service max one of (object|group). CIDR-Werte werden geparsed.
* NAT: kind-spezifische Pflichtfelder. dnat braucht target_addr
+ match_dport_start. snat braucht target_addr. masquerade
verbietet target_addr (Iface-IP gewinnt).
* Services: builtin-Rows können nicht editiert/gelöscht werden
(Repo-Layer enforced).
Audit-Log pro Mutation. NoContent für DELETE.
Wiring in cmd/edgeguard-api/main.go: 6 Repos + ein
NewFirewallHandler(...).Register(authed).
Renderer (nft aus allen Joins) + Frontend folgen in den nächsten
Commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
762 lines
20 KiB
Go
762 lines
20 KiB
Go
package handlers
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"net"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/firewall"
|
||
)
|
||
|
||
// FirewallHandler exposes everything under /api/v1/firewall/*:
|
||
//
|
||
// address-objects — primitive Adress-Definitionen (host/network/range/fqdn)
|
||
// address-groups — Gruppen von address-objects (mit /members ops)
|
||
// services — proto+port (Builtins lassen sich nicht editieren)
|
||
// service-groups — Gruppen von services
|
||
// rules — v2-Policy: zone × addr × service × action
|
||
// nat-rules — separate Tabelle: dnat / snat / masquerade
|
||
//
|
||
// Validation lebt im Handler (DB lässt mehr zu als die Anwendung erlauben
|
||
// will — exactly-one-of-Constraints sind in Postgres mühsam).
|
||
type FirewallHandler struct {
|
||
AddrObjects *firewall.AddressObjectsRepo
|
||
AddrGroups *firewall.AddressGroupsRepo
|
||
Services *firewall.ServicesRepo
|
||
ServiceGroups *firewall.ServiceGroupsRepo
|
||
Rules *firewall.RulesRepo
|
||
NATRules *firewall.NATRulesRepo
|
||
Audit *audit.Repo
|
||
NodeID string
|
||
}
|
||
|
||
func NewFirewallHandler(
|
||
ao *firewall.AddressObjectsRepo,
|
||
ag *firewall.AddressGroupsRepo,
|
||
sv *firewall.ServicesRepo,
|
||
sg *firewall.ServiceGroupsRepo,
|
||
rl *firewall.RulesRepo,
|
||
nat *firewall.NATRulesRepo,
|
||
a *audit.Repo,
|
||
nodeID string,
|
||
) *FirewallHandler {
|
||
return &FirewallHandler{
|
||
AddrObjects: ao, AddrGroups: ag,
|
||
Services: sv, ServiceGroups: sg,
|
||
Rules: rl, NATRules: nat,
|
||
Audit: a, NodeID: nodeID,
|
||
}
|
||
}
|
||
|
||
func (h *FirewallHandler) Register(rg *gin.RouterGroup) {
|
||
g := rg.Group("/firewall")
|
||
|
||
ao := g.Group("/address-objects")
|
||
ao.GET("", h.ListAddrObj)
|
||
ao.POST("", h.CreateAddrObj)
|
||
ao.GET("/:id", h.GetAddrObj)
|
||
ao.PUT("/:id", h.UpdateAddrObj)
|
||
ao.DELETE("/:id", h.DeleteAddrObj)
|
||
|
||
ag := g.Group("/address-groups")
|
||
ag.GET("", h.ListAddrGrp)
|
||
ag.POST("", h.CreateAddrGrp)
|
||
ag.GET("/:id", h.GetAddrGrp)
|
||
ag.PUT("/:id", h.UpdateAddrGrp)
|
||
ag.DELETE("/:id", h.DeleteAddrGrp)
|
||
|
||
sv := g.Group("/services")
|
||
sv.GET("", h.ListService)
|
||
sv.POST("", h.CreateService)
|
||
sv.GET("/:id", h.GetService)
|
||
sv.PUT("/:id", h.UpdateService)
|
||
sv.DELETE("/:id", h.DeleteService)
|
||
|
||
sg := g.Group("/service-groups")
|
||
sg.GET("", h.ListSvcGrp)
|
||
sg.POST("", h.CreateSvcGrp)
|
||
sg.GET("/:id", h.GetSvcGrp)
|
||
sg.PUT("/:id", h.UpdateSvcGrp)
|
||
sg.DELETE("/:id", h.DeleteSvcGrp)
|
||
|
||
rl := g.Group("/rules")
|
||
rl.GET("", h.ListRule)
|
||
rl.POST("", h.CreateRule)
|
||
rl.GET("/:id", h.GetRule)
|
||
rl.PUT("/:id", h.UpdateRule)
|
||
rl.DELETE("/:id", h.DeleteRule)
|
||
|
||
nat := g.Group("/nat-rules")
|
||
nat.GET("", h.ListNAT)
|
||
nat.POST("", h.CreateNAT)
|
||
nat.GET("/:id", h.GetNAT)
|
||
nat.PUT("/:id", h.UpdateNAT)
|
||
nat.DELETE("/:id", h.DeleteNAT)
|
||
}
|
||
|
||
// ── Address Objects ────────────────────────────────────────────────────
|
||
|
||
func (h *FirewallHandler) ListAddrObj(c *gin.Context) {
|
||
out, err := h.AddrObjects.List(c.Request.Context())
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, gin.H{"address_objects": out})
|
||
}
|
||
|
||
func (h *FirewallHandler) GetAddrObj(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
x, err := h.AddrObjects.Get(c.Request.Context(), id)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrAddressObjectNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, x)
|
||
}
|
||
|
||
func (h *FirewallHandler) CreateAddrObj(c *gin.Context) {
|
||
var req models.FirewallAddressObject
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
if err := validateAddrObjValue(req.Kind, req.Value); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.AddrObjects.Create(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_obj.create", req.Name, out, h.NodeID)
|
||
response.Created(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) UpdateAddrObj(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var req models.FirewallAddressObject
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
if err := validateAddrObjValue(req.Kind, req.Value); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.AddrObjects.Update(c.Request.Context(), id, req)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrAddressObjectNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_obj.update", req.Name, out, h.NodeID)
|
||
response.OK(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) DeleteAddrObj(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if err := h.AddrObjects.Delete(c.Request.Context(), id); err != nil {
|
||
if errors.Is(err, firewall.ErrAddressObjectNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = 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)
|
||
}
|
||
|
||
// ── Address Groups ─────────────────────────────────────────────────────
|
||
|
||
func (h *FirewallHandler) ListAddrGrp(c *gin.Context) {
|
||
out, err := h.AddrGroups.List(c.Request.Context())
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, gin.H{"address_groups": out})
|
||
}
|
||
|
||
func (h *FirewallHandler) GetAddrGrp(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
x, err := h.AddrGroups.Get(c.Request.Context(), id)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrAddressGroupNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, x)
|
||
}
|
||
|
||
func (h *FirewallHandler) CreateAddrGrp(c *gin.Context) {
|
||
var req models.FirewallAddressGroup
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.AddrGroups.Create(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_grp.create", req.Name, out, h.NodeID)
|
||
response.Created(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) UpdateAddrGrp(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var req models.FirewallAddressGroup
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.AddrGroups.Update(c.Request.Context(), id, req)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrAddressGroupNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.addr_grp.update", req.Name, out, h.NodeID)
|
||
response.OK(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) DeleteAddrGrp(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if err := h.AddrGroups.Delete(c.Request.Context(), id); err != nil {
|
||
if errors.Is(err, firewall.ErrAddressGroupNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = 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)
|
||
}
|
||
|
||
// ── Services ───────────────────────────────────────────────────────────
|
||
|
||
func (h *FirewallHandler) ListService(c *gin.Context) {
|
||
out, err := h.Services.List(c.Request.Context())
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, gin.H{"services": out})
|
||
}
|
||
|
||
func (h *FirewallHandler) GetService(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
x, err := h.Services.Get(c.Request.Context(), id)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrServiceNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, x)
|
||
}
|
||
|
||
func (h *FirewallHandler) CreateService(c *gin.Context) {
|
||
var req models.FirewallService
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.Services.Create(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.service.create", req.Name, out, h.NodeID)
|
||
response.Created(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) UpdateService(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var req models.FirewallService
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.Services.Update(c.Request.Context(), id, req)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrServiceNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.service.update", req.Name, out, h.NodeID)
|
||
response.OK(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) DeleteService(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if err := h.Services.Delete(c.Request.Context(), id); err != nil {
|
||
if errors.Is(err, firewall.ErrServiceNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.service.delete",
|
||
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
|
||
response.NoContent(c)
|
||
}
|
||
|
||
// ── Service Groups ─────────────────────────────────────────────────────
|
||
|
||
func (h *FirewallHandler) ListSvcGrp(c *gin.Context) {
|
||
out, err := h.ServiceGroups.List(c.Request.Context())
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, gin.H{"service_groups": out})
|
||
}
|
||
|
||
func (h *FirewallHandler) GetSvcGrp(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
x, err := h.ServiceGroups.Get(c.Request.Context(), id)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrServiceGroupNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, x)
|
||
}
|
||
|
||
func (h *FirewallHandler) CreateSvcGrp(c *gin.Context) {
|
||
var req models.FirewallServiceGroup
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.ServiceGroups.Create(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.svc_grp.create", req.Name, out, h.NodeID)
|
||
response.Created(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) UpdateSvcGrp(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var req models.FirewallServiceGroup
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.ServiceGroups.Update(c.Request.Context(), id, req)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrServiceGroupNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.svc_grp.update", req.Name, out, h.NodeID)
|
||
response.OK(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) DeleteSvcGrp(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if err := h.ServiceGroups.Delete(c.Request.Context(), id); err != nil {
|
||
if errors.Is(err, firewall.ErrServiceGroupNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = 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)
|
||
}
|
||
|
||
// ── Rules ──────────────────────────────────────────────────────────────
|
||
|
||
func (h *FirewallHandler) ListRule(c *gin.Context) {
|
||
out, err := h.Rules.List(c.Request.Context())
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, gin.H{"rules": out})
|
||
}
|
||
|
||
func (h *FirewallHandler) GetRule(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
x, err := h.Rules.Get(c.Request.Context(), id)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrRuleNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, x)
|
||
}
|
||
|
||
func (h *FirewallHandler) CreateRule(c *gin.Context) {
|
||
var req models.FirewallRule
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
if err := validateRule(req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.Rules.Create(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.rule.create", strconv.FormatInt(out.ID, 10), out, h.NodeID)
|
||
response.Created(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) UpdateRule(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var req models.FirewallRule
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
if err := validateRule(req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.Rules.Update(c.Request.Context(), id, req)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrRuleNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.rule.update", strconv.FormatInt(id, 10), out, h.NodeID)
|
||
response.OK(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) DeleteRule(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if err := h.Rules.Delete(c.Request.Context(), id); err != nil {
|
||
if errors.Is(err, firewall.ErrRuleNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.rule.delete",
|
||
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
|
||
response.NoContent(c)
|
||
}
|
||
|
||
// ── NAT Rules ──────────────────────────────────────────────────────────
|
||
|
||
func (h *FirewallHandler) ListNAT(c *gin.Context) {
|
||
out, err := h.NATRules.List(c.Request.Context())
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, gin.H{"nat_rules": out})
|
||
}
|
||
|
||
func (h *FirewallHandler) GetNAT(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
x, err := h.NATRules.Get(c.Request.Context(), id)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrNATRuleNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
response.OK(c, x)
|
||
}
|
||
|
||
func (h *FirewallHandler) CreateNAT(c *gin.Context) {
|
||
var req models.FirewallNATRule
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
if err := validateNAT(req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.NATRules.Create(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.nat.create", strconv.FormatInt(out.ID, 10), out, h.NodeID)
|
||
response.Created(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) UpdateNAT(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
var req models.FirewallNATRule
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
if err := validateNAT(req); err != nil {
|
||
response.BadRequest(c, err)
|
||
return
|
||
}
|
||
out, err := h.NATRules.Update(c.Request.Context(), id, req)
|
||
if err != nil {
|
||
if errors.Is(err, firewall.ErrNATRuleNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.nat.update", strconv.FormatInt(id, 10), out, h.NodeID)
|
||
response.OK(c, out)
|
||
}
|
||
|
||
func (h *FirewallHandler) DeleteNAT(c *gin.Context) {
|
||
id, ok := parseID(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if err := h.NATRules.Delete(c.Request.Context(), id); err != nil {
|
||
if errors.Is(err, firewall.ErrNATRuleNotFound) {
|
||
response.NotFound(c, err)
|
||
return
|
||
}
|
||
response.Internal(c, err)
|
||
return
|
||
}
|
||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "fw.nat.delete",
|
||
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
|
||
response.NoContent(c)
|
||
}
|
||
|
||
// ── Validators ─────────────────────────────────────────────────────────
|
||
|
||
func validateAddrObjValue(kind, value string) error {
|
||
switch kind {
|
||
case "host":
|
||
if net.ParseIP(value) == nil {
|
||
return fmt.Errorf("host: %q is not a valid IPv4/IPv6 address", value)
|
||
}
|
||
case "network":
|
||
if _, _, err := net.ParseCIDR(value); err != nil {
|
||
return fmt.Errorf("network: %q is not a valid CIDR (%w)", value, err)
|
||
}
|
||
case "range":
|
||
parts := strings.SplitN(value, "-", 2)
|
||
if len(parts) != 2 ||
|
||
net.ParseIP(strings.TrimSpace(parts[0])) == nil ||
|
||
net.ParseIP(strings.TrimSpace(parts[1])) == nil {
|
||
return fmt.Errorf("range: %q must be 'IP-IP'", value)
|
||
}
|
||
case "fqdn":
|
||
if !looksLikeFQDN(value) {
|
||
return fmt.Errorf("fqdn: %q does not look like a hostname", value)
|
||
}
|
||
default:
|
||
return fmt.Errorf("unknown address-object kind %q", kind)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func looksLikeFQDN(s string) bool {
|
||
s = strings.TrimSuffix(strings.TrimSpace(s), ".")
|
||
if s == "" || len(s) > 253 || !strings.Contains(s, ".") {
|
||
return false
|
||
}
|
||
for _, label := range strings.Split(s, ".") {
|
||
if len(label) == 0 || len(label) > 63 {
|
||
return false
|
||
}
|
||
for _, r := range label {
|
||
ok := r == '-' || (r >= '0' && r <= '9') ||
|
||
(r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
|
||
if !ok {
|
||
return false
|
||
}
|
||
}
|
||
if label[0] == '-' || label[len(label)-1] == '-' {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// validateRule enforces "exactly one or zero" of the three address
|
||
// pointers per side, and "exactly one or zero" service references.
|
||
func validateRule(r models.FirewallRule) error {
|
||
if r.Action == "" {
|
||
return errors.New("action is required")
|
||
}
|
||
if got := countNonNil(r.SrcAddressObjectID, r.SrcAddressGroupID, optStr(r.SrcCIDR)); got > 1 {
|
||
return errors.New("source: at most one of src_address_object_id / src_address_group_id / src_cidr")
|
||
}
|
||
if got := countNonNil(r.DstAddressObjectID, r.DstAddressGroupID, optStr(r.DstCIDR)); got > 1 {
|
||
return errors.New("destination: at most one of dst_address_object_id / dst_address_group_id / dst_cidr")
|
||
}
|
||
if r.ServiceObjectID != nil && r.ServiceGroupID != nil {
|
||
return errors.New("service: at most one of service_object_id / service_group_id")
|
||
}
|
||
if r.SrcCIDR != nil {
|
||
if _, _, err := net.ParseCIDR(*r.SrcCIDR); err != nil {
|
||
return fmt.Errorf("src_cidr: %w", err)
|
||
}
|
||
}
|
||
if r.DstCIDR != nil {
|
||
if _, _, err := net.ParseCIDR(*r.DstCIDR); err != nil {
|
||
return fmt.Errorf("dst_cidr: %w", err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateNAT(n models.FirewallNATRule) error {
|
||
switch n.Kind {
|
||
case "dnat":
|
||
if n.TargetAddr == nil || *n.TargetAddr == "" {
|
||
return errors.New("dnat: target_addr is required")
|
||
}
|
||
if n.MatchDPortStart == nil {
|
||
return errors.New("dnat: match_dport_start is required (which incoming port to forward)")
|
||
}
|
||
case "snat":
|
||
if n.TargetAddr == nil || *n.TargetAddr == "" {
|
||
return errors.New("snat: target_addr is required (the address to rewrite to)")
|
||
}
|
||
case "masquerade":
|
||
if n.OutZone == nil || *n.OutZone == "" {
|
||
return errors.New("masquerade: out_zone is required (which interface group's IP to use)")
|
||
}
|
||
if n.TargetAddr != nil && *n.TargetAddr != "" {
|
||
return errors.New("masquerade: target_addr must be empty (uses out-iface IP)")
|
||
}
|
||
default:
|
||
return fmt.Errorf("unknown NAT kind %q", n.Kind)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// countNonNil counts how many of the supplied pointers are non-nil
|
||
// (and, for *string, non-empty). Used by validateRule.
|
||
func countNonNil(args ...any) int {
|
||
n := 0
|
||
for _, a := range args {
|
||
switch v := a.(type) {
|
||
case *int64:
|
||
if v != nil {
|
||
n++
|
||
}
|
||
case *string:
|
||
if v != nil && *v != "" {
|
||
n++
|
||
}
|
||
case nil:
|
||
// skip
|
||
default:
|
||
n++
|
||
}
|
||
}
|
||
return n
|
||
}
|
||
|
||
func optStr(p *string) *string { return p }
|