Files
tomswall/internal/config/policy.go
T
unkinben 8d9a76c751 Add comprehensive nftables compiler with shorewall feature parity
Rewrites the compiler from ~440 to ~1700 lines covering all major shorewall
firewall features: loopback, conntrack fast-path, anti-spoof, DHCP, intra-zone,
blacklist/whitelist, conntrack notrack, tunnels (13 types), rules with sections,
DNAT/redirect, SNAT/masquerade, static NAT, policies with zone exclusions,
MSS clamping, rate limiting, connection limiting, negated addresses, ICMP type
matching, TCP RST reject, user/UID matching, mark match/set, NFQUEUE, NONAT,
and policy-level rate/conn limiting.

Adds full config types for all shorewall subsystems (mangle, accounting, maclist,
netmap, providers, tunnels, conntrack, blrules, proxyarp/ndp, routes, tc, secmarks),
shorewall migration tooling, expanded CLI commands, expression-level diff engine,
and 49 unit tests.
2026-07-01 23:56:44 +10:00

144 lines
3.8 KiB
Go

package config
import (
"fmt"
"strings"
)
type PolicyAction string
const (
PolicyAccept PolicyAction = "accept"
PolicyDrop PolicyAction = "drop"
PolicyReject PolicyAction = "reject"
PolicyContinue PolicyAction = "continue"
PolicyNone PolicyAction = "none"
PolicyQueue PolicyAction = "queue"
PolicyNFQueue PolicyAction = "nfqueue"
)
// Policy defines the default action for traffic between zones.
// Policies are evaluated in order — first match wins.
// Intra-zone traffic (zone to itself) is implicitly ACCEPTed unless
// overridden with an explicit policy or by using "all+" as source/dest.
type Policy struct {
// Source zone(s). Supports: zone name, "all", "all+" (overrides intra-zone ACCEPT),
// comma-separated zones ("loc,dmz"), or exclusions ("all!net").
Source string `yaml:"source"`
// Dest zone(s). Same syntax as Source.
Dest string `yaml:"dest"`
Action PolicyAction `yaml:"action"`
Log string `yaml:"log,omitempty"`
// Rate limit for TCP connections.
// Format: [s:|d:][name:]rate/{sec|min|hour|day}[:burst]
RateLimit string `yaml:"rate_limit,omitempty"`
// Simultaneous connection limit. Format: limit[:mask]
// mask is a VLSM prefix length to apply per-subnet limiting.
ConnLimit string `yaml:"conn_limit,omitempty"`
// NFQueue number (only used when action is nfqueue).
NFQueue int `yaml:"nfqueue,omitempty"`
}
func (c *Config) validatePolicy() error {
if len(c.Policy) == 0 {
return fmt.Errorf("no policies defined")
}
fwZone := c.FirewallZone()
for i, p := range c.Policy {
if p.Source == "" {
return fmt.Errorf("policy[%d]: source required", i)
}
if p.Dest == "" {
return fmt.Errorf("policy[%d]: dest required", i)
}
switch p.Action {
case PolicyAccept, PolicyDrop, PolicyReject, PolicyContinue, PolicyNone, PolicyQueue, PolicyNFQueue:
default:
return fmt.Errorf("policy[%d]: unknown action %q", i, p.Action)
}
if err := validatePolicyZoneRef(p.Source, c, fwZone, p.Action, "source", i); err != nil {
return err
}
if err := validatePolicyZoneRef(p.Dest, c, fwZone, p.Action, "dest", i); err != nil {
return err
}
}
return nil
}
// validatePolicyZoneRef validates a source or dest field, which can be:
// "all", "all+", "all!zone1,zone2", "zone1,zone2", "zone1,zone2+", or a single zone name.
func validatePolicyZoneRef(ref string, c *Config, fwZone string, action PolicyAction, field string, idx int) error {
if ref == "" {
return nil
}
base, exclusions := parsePolicyRef(ref)
if action == PolicyNone {
if base == "all" || base == "all+" {
return fmt.Errorf("policy[%d]: NONE may not be used with %s=%q", idx, field, ref)
}
for _, z := range splitZones(base) {
if z == fwZone {
return fmt.Errorf("policy[%d]: NONE may not be used with the firewall zone", idx)
}
}
}
if base != "all" && base != "all+" {
for _, z := range splitZones(base) {
name := strings.TrimSuffix(z, "+")
if name != fwZone {
if _, ok := c.Zones[name]; !ok {
return fmt.Errorf("policy[%d]: %s zone %q not defined", idx, field, name)
}
}
}
}
for _, ez := range exclusions {
if _, ok := c.Zones[ez]; !ok {
return fmt.Errorf("policy[%d]: excluded %s zone %q not defined", idx, field, ez)
}
}
return nil
}
// parsePolicyRef splits "all!net,dmz" into base="all" and exclusions=["net","dmz"].
func parsePolicyRef(ref string) (base string, exclusions []string) {
if idx := strings.IndexByte(ref, '!'); idx >= 0 {
base = ref[:idx]
for _, z := range strings.Split(ref[idx+1:], ",") {
z = strings.TrimSpace(z)
if z != "" {
exclusions = append(exclusions, z)
}
}
return base, exclusions
}
return ref, nil
}
func splitZones(ref string) []string {
ref = strings.TrimSuffix(ref, "+")
var zones []string
for _, z := range strings.Split(ref, ",") {
z = strings.TrimSpace(z)
if z != "" {
zones = append(zones, z)
}
}
return zones
}