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 }