package config import ( "fmt" "net/netip" "slices" "strings" ) type Host struct { Zone string `yaml:"zone"` Interface string `yaml:"interface"` Addresses []string `yaml:"addresses"` Exclusions []string `yaml:"exclusions,omitempty"` Dynamic bool `yaml:"dynamic,omitempty"` Options HostOptions `yaml:"options,omitempty"` } type HostOptions struct { Broadcast bool `yaml:"broadcast,omitempty"` DestOnly bool `yaml:"destonly,omitempty"` IPSec bool `yaml:"ipsec,omitempty"` MSS int `yaml:"mss,omitempty"` NoSmurfs bool `yaml:"nosmurfs,omitempty"` RouteBack bool `yaml:"routeback,omitempty"` TCPFlags bool `yaml:"tcpflags,omitempty"` } func (c *Config) validateHosts() error { fwZone := c.FirewallZone() for i, h := range c.Hosts { if h.Zone == "" { return fmt.Errorf("host[%d]: zone required", i) } if h.Zone == fwZone { return fmt.Errorf("host[%d]: firewall zone must not be listed in hosts", i) } if _, ok := c.Zones[h.Zone]; !ok { return fmt.Errorf("host[%d]: zone %q not defined", i, h.Zone) } if h.Interface == "" { return fmt.Errorf("host[%d]: interface required", i) } ifaceFound := false for _, iface := range c.Interfaces { prefix, wild := strings.CutSuffix(iface.PhysicalName(), "+") if iface.Interface == h.Interface || iface.PhysicalName() == h.Interface || (wild && strings.HasPrefix(h.Interface, prefix)) { ifaceFound = true break } } if !ifaceFound { return fmt.Errorf("host[%d]: interface %q not defined in interfaces", i, h.Interface) } if !h.Dynamic && len(h.Addresses) == 0 { return fmt.Errorf("host[%d]: at least one address required (or set dynamic: true)", i) } for _, a := range slices.Concat(h.Addresses, h.Exclusions) { if _, err := netip.ParsePrefix(a); err != nil { if _, err := netip.ParseAddr(a); err != nil { return fmt.Errorf("host[%d]: invalid address %q", i, a) } } } } return nil }