Files
edgeguard-native/internal/firewall/firewall.go
Debian e517783c42 feat(fw): Migration 0010 — Firewall-v2-Schema (Fortigate-Style)
Phase-1 firewall_rules (chain/match_expr raw nft) → Fortigate-Niveau:
* firewall_address_objects (host/network/range/fqdn)
* firewall_address_groups + members junction
* firewall_services (proto+port range, builtin-Flag)
* firewall_service_groups + members junction
* firewall_rules komplett umgebaut: src_zone+addr/group/cidr,
  dst_zone+addr/group/cidr, service_object_id ODER service_group_id,
  action accept|drop|reject, log-Flag, priority+enabled
* firewall_nat_rules (kind=dnat|snat|masquerade) als separate Tabelle

Zonen kommen aus network_interfaces.role (wan|lan|dmz|mgmt|cluster
+ pseudo-Zone 'any').

Builtin-Inserts: 18 Standard-Services (HTTP/HTTPS/SSH/DNS/SMTP-Familie/
DBs/RDP/WireGuard/Ping) plus 5 Service-Groups (Web, Mail-Submit,
Mail-Receive, DNS, Ping).

Renderer (internal/firewall/firewall.go) lässt firewall_rules-Query
für jetzt aus — Template fällt auf baseline + cluster-peer-set
zurück. Volle Render-Logik mit den neuen Joins kommt mit Task #44.

Models + Repos + Handlers + Frontend folgen in den nächsten Commits.

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

129 lines
3.4 KiB
Go

// Package firewall renders /etc/edgeguard/nftables.d/ruleset.nft from
// the relational state in PG (firewall_rules + ha_nodes).
//
// The base ruleset is hard-coded in the template (default-deny input,
// stateful baseline, SSH rate-limit, public :80 / :443, peer mTLS on
// :8443 from cluster IPs). Operator-defined rows in firewall_rules
// land at the bottom of input/forward/output.
//
// Reload uses `nft -f <path>` (atomic ruleset replace) — there is no
// systemctl reload for nftables.
package firewall
import (
"bytes"
"context"
_ "embed"
"fmt"
"net"
"os/exec"
"path/filepath"
"text/template"
"github.com/jackc/pgx/v5/pgxpool"
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
)
//go:embed ruleset.nft.tpl
var rulesTpl string
var tpl = template.Must(template.New("ruleset").Parse(rulesTpl))
type Generator struct {
Pool *pgxpool.Pool
OutputPath string
SkipReload bool
}
func New(pool *pgxpool.Pool) *Generator { return &Generator{Pool: pool} }
func (g *Generator) Name() string { return "nftables" }
func (g *Generator) Render(ctx context.Context) error {
view, err := g.loadView(ctx)
if err != nil {
return fmt.Errorf("nftables: load state: %w", err)
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, view); err != nil {
return fmt.Errorf("nftables: render template: %w", err)
}
out := g.OutputPath
if out == "" {
out = filepath.Join(configgen.EtcEdgeguard, "nftables.d", "ruleset.nft")
}
if err := configgen.AtomicWrite(out, buf.Bytes(), 0o644); err != nil {
return fmt.Errorf("nftables: write: %w", err)
}
if g.SkipReload {
return nil
}
if err := exec.Command("nft", "-f", out).Run(); err != nil {
return fmt.Errorf("nftables: nft -f %s: %w", out, err)
}
return nil
}
type View struct {
PeerIPv4 []string
PeerIPv6 []string
// Custom rules grouped by chain — the template iterates each
// section independently so input/forward/output stay separate.
CustomRulesInput []Rule
CustomRulesForward []Rule
CustomRulesOutput []Rule
}
type Rule struct {
MatchExpr string
Action string
Comment string
}
func (g *Generator) loadView(ctx context.Context) (*View, error) {
view := &View{}
// Peer IPs from ha_nodes — splits IPv4 vs IPv6 so the template
// can populate the right named set without runtime branching.
peerRows, err := g.Pool.Query(ctx,
`SELECT public_ip, internal_ip FROM ha_nodes`)
if err != nil {
return nil, fmt.Errorf("query ha_nodes: %w", err)
}
defer peerRows.Close()
for peerRows.Next() {
var pub, internal *string
if err := peerRows.Scan(&pub, &internal); err != nil {
return nil, err
}
for _, ip := range []*string{pub, internal} {
if ip == nil {
continue
}
parsed := net.ParseIP(*ip)
if parsed == nil {
continue
}
if parsed.To4() != nil {
view.PeerIPv4 = append(view.PeerIPv4, parsed.String())
} else {
view.PeerIPv6 = append(view.PeerIPv6, parsed.String())
}
}
}
if err := peerRows.Err(); err != nil {
return nil, err
}
// Migration 0010 hat firewall_rules komplett umgebaut (Fortigate-
// Style mit address objects + service refs). Phase-2-Renderer
// kannte das alte chain/match_expr-Schema. Bis Task #44 die
// Render-Logik mit den neuen Joins ersetzt, geben wir hier
// keine custom-Rules aus — Output ist nur baseline + cluster set.
// Sicher, weil baseline default-deny ist; v2-Rules kommen mit
// dem nächsten Renderer-Patch.
return view, nil
}