e0f54ef320
Add `tomswall agent`: it pulls this device's compiled config from tomswallapi, differentially applies it, and reports the applied generation. It caches the last known-good config and, when the control plane is unreachable, keeps applying that cache — it never fails closed. - internal/agent: rendered-config types, HTTP client (fetch + status report), on-disk cache, on-device DNS resolver for dns sets (honors the device's configured resolver, fail-safe on lookup failure), and the pull-apply-report loop behind a mockable Applier. - Translate the interface-agnostic, address-matched rendered model into native tomswall config using the "all:<cidr>" any-interface source/dest form, reusing the existing differential engine. Named-set members are inlined as concrete addresses (native nft set references are a tracked follow-up). - cmd/tomswall: wire the `agent` subcommand (flags + TOMSWALL_* env, --once). - Unit tests: translation, cache, and the don't-fail-closed fallback loop. - Add DESIGN.md documenting the control-plane architecture.
99 lines
3.1 KiB
Go
99 lines
3.1 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type Interface struct {
|
|
Zone string `yaml:"zone,omitempty"`
|
|
Interface string `yaml:"interface"`
|
|
Options InterfaceOptions `yaml:"options,omitempty"`
|
|
}
|
|
|
|
type InterfaceOptions struct {
|
|
// Rule generation options
|
|
DHCP bool `yaml:"dhcp,omitempty"`
|
|
TCPFlags *bool `yaml:"tcpflags,omitempty"`
|
|
NoSmurfs bool `yaml:"nosmurfs,omitempty"`
|
|
RouteBack *bool `yaml:"routeback,omitempty"`
|
|
Bridge bool `yaml:"bridge,omitempty"`
|
|
DestOnly bool `yaml:"destonly,omitempty"`
|
|
Unmanaged bool `yaml:"unmanaged,omitempty"`
|
|
Upnp bool `yaml:"upnp,omitempty"`
|
|
|
|
// Startup behavior
|
|
Optional bool `yaml:"optional,omitempty"`
|
|
Required bool `yaml:"required,omitempty"`
|
|
Wait int `yaml:"wait,omitempty"`
|
|
|
|
// Logical-to-physical mapping
|
|
Physical string `yaml:"physical,omitempty"`
|
|
|
|
// TCP MSS clamping for forwarded SYN packets
|
|
MSS int `yaml:"mss,omitempty"`
|
|
|
|
// Limit zone to specific networks
|
|
Nets []string `yaml:"nets,omitempty"`
|
|
|
|
// Sysctl adjustments (applied on startup)
|
|
RouteFilter *int `yaml:"routefilter,omitempty"`
|
|
LogMartians *bool `yaml:"logmartians,omitempty"`
|
|
ArpFilter *bool `yaml:"arp_filter,omitempty"`
|
|
ArpIgnore *int `yaml:"arp_ignore,omitempty"`
|
|
ProxyArp *bool `yaml:"proxyarp,omitempty"`
|
|
SourceRoute *bool `yaml:"sourceroute,omitempty"`
|
|
|
|
// IPv6: controls acceptance of Router Advertisements (0/1/2)
|
|
AcceptRA *int `yaml:"accept_ra,omitempty"`
|
|
}
|
|
|
|
// PhysicalName returns the actual interface name (physical if set, else logical).
|
|
func (iface *Interface) PhysicalName() string {
|
|
if iface.Options.Physical != "" {
|
|
return iface.Options.Physical
|
|
}
|
|
return iface.Interface
|
|
}
|
|
|
|
// IsWildcard returns true if the interface matches multiple devices (e.g. "ppp+").
|
|
func (iface *Interface) IsWildcard() bool {
|
|
return strings.HasSuffix(iface.PhysicalName(), "+")
|
|
}
|
|
|
|
func (c *Config) validateInterfaces() error {
|
|
seen := make(map[string]bool)
|
|
fwZone := c.FirewallZone()
|
|
|
|
for i, iface := range c.Interfaces {
|
|
if iface.Interface == "" {
|
|
return fmt.Errorf("interface[%d]: interface name required", i)
|
|
}
|
|
if strings.Contains(iface.Interface, ":") {
|
|
return fmt.Errorf("interface[%d] %q: virtual interfaces (e.g. eth0:0) not supported; use the physical option instead", i, iface.Interface)
|
|
}
|
|
|
|
if iface.Zone != "" {
|
|
if iface.Zone == fwZone {
|
|
return fmt.Errorf("interface[%d] %q: firewall zone must not be listed in interfaces", i, iface.Interface)
|
|
}
|
|
if _, ok := c.Zones[iface.Zone]; !ok {
|
|
return fmt.Errorf("interface[%d] %q: zone %q not defined", i, iface.Interface, iface.Zone)
|
|
}
|
|
}
|
|
|
|
if iface.Options.Unmanaged && iface.Zone != "" {
|
|
return fmt.Errorf("interface[%d] %q: unmanaged interfaces must have an empty zone", i, iface.Interface)
|
|
}
|
|
if iface.Options.Optional && iface.Options.Required {
|
|
return fmt.Errorf("interface[%d] %q: optional and required are mutually exclusive", i, iface.Interface)
|
|
}
|
|
|
|
if seen[iface.Interface] {
|
|
return fmt.Errorf("interface[%d]: duplicate interface %q", i, iface.Interface)
|
|
}
|
|
seen[iface.Interface] = true
|
|
}
|
|
return nil
|
|
}
|