package config import "fmt" type RuleAction string const ( RuleAccept RuleAction = "accept" RuleDrop RuleAction = "drop" RuleReject RuleAction = "reject" RuleDNAT RuleAction = "dnat" RuleRedirect RuleAction = "redirect" RuleLog RuleAction = "log" ) type Rule struct { Action RuleAction `yaml:"action"` Source string `yaml:"source"` Dest string `yaml:"dest"` Proto string `yaml:"proto,omitempty"` DPort PortSpec `yaml:"dport,omitempty"` SPort PortSpec `yaml:"sport,omitempty"` PortGroup string `yaml:"portgroup,omitempty"` Log string `yaml:"log,omitempty"` DNATDest string `yaml:"dnat_dest,omitempty"` RateLimit string `yaml:"rate_limit,omitempty"` ConnLimit int `yaml:"conn_limit,omitempty"` Comment string `yaml:"comment,omitempty"` } // PortSpec supports single ports, ranges, and lists. // Examples: [80], [443], [80, 443], ["1024-65535"], [53, "80-90"] type PortSpec []string func (ps *PortSpec) UnmarshalYAML(unmarshal func(interface{}) error) error { var multi []interface{} if err := unmarshal(&multi); err == nil { for _, v := range multi { switch val := v.(type) { case int: *ps = append(*ps, fmt.Sprintf("%d", val)) case float64: *ps = append(*ps, fmt.Sprintf("%d", int(val))) case string: *ps = append(*ps, val) default: return fmt.Errorf("unsupported port value type %T", v) } } return nil } var single string if err := unmarshal(&single); err == nil { *ps = PortSpec{single} return nil } var num int if err := unmarshal(&num); err == nil { *ps = PortSpec{fmt.Sprintf("%d", num)} return nil } return fmt.Errorf("invalid port spec") } func (c *Config) validateRules() error { for i, r := range c.Rules { switch r.Action { case RuleAccept, RuleDrop, RuleReject, RuleDNAT, RuleRedirect, RuleLog: default: return fmt.Errorf("rule[%d]: unknown action %q", i, r.Action) } if r.Source == "" { return fmt.Errorf("rule[%d]: source required", i) } if r.Dest == "" { return fmt.Errorf("rule[%d]: dest required", i) } srcZone := zoneFromSpec(r.Source) if srcZone != "all" { if _, ok := c.Zones[srcZone]; !ok { return fmt.Errorf("rule[%d]: source zone %q not defined", i, srcZone) } } dstZone := zoneFromSpec(r.Dest) if dstZone != "all" { if _, ok := c.Zones[dstZone]; !ok { return fmt.Errorf("rule[%d]: dest zone %q not defined", i, dstZone) } } if r.PortGroup != "" { if _, ok := c.PortGroups[r.PortGroup]; !ok { return fmt.Errorf("rule[%d]: portgroup %q not defined", i, r.PortGroup) } if r.Proto != "" || len(r.DPort) > 0 { return fmt.Errorf("rule[%d]: portgroup is mutually exclusive with proto/dport", i) } } if r.Action == RuleDNAT && r.DNATDest == "" { return fmt.Errorf("rule[%d]: dnat_dest required for DNAT action", i) } } return nil } // zoneFromSpec extracts the zone name from a zone spec like "net" or "net:192.168.1.0/24". func zoneFromSpec(spec string) string { for i, c := range spec { if c == ':' { return spec[:i] } } return spec }