2a3eb3b04d
Spiritual successor to shorewall — manages nftables directly via google/nftables. Reads a single YAML config covering zones, interfaces, hosts, policy, rules, snat, and named portgroups. Computes differential changes against the running nftables state and applies them atomically. Supports detecting and purging rules added outside of tomswall.
122 lines
3.1 KiB
Go
122 lines
3.1 KiB
Go
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
|
|
}
|