8d9a76c751
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.
144 lines
3.8 KiB
Go
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
|
|
}
|