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>
129 lines
3.4 KiB
Go
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
|
|
}
|