Files
tomswall/internal/agent/rendered.go
T
benvin e0f54ef320 Add tomswall agent (control-plane pull mode)
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.
2026-07-20 20:05:49 +10:00

71 lines
3.5 KiB
Go

// Package agent implements `tomswall agent`: it pulls a device's compiled config
// from tomswallapi, differentially applies it, and reports the applied generation.
// It never fails closed — if the control plane is unreachable it keeps the last
// known-good config running.
package agent
// RenderedConfig is the per-device document served by tomswallapi at
// GET /api/v1/devices/{name}/config. It mirrors the control plane's compiler
// output: interface-agnostic, address-matched rules plus named sets.
type RenderedConfig struct {
Generation int64 `yaml:"generation" json:"generation"`
Device string `yaml:"device" json:"device"`
Class string `yaml:"class" json:"class"`
Enforcing bool `yaml:"enforcing" json:"enforcing"`
Settings RenderedSettings `yaml:"settings" json:"settings"`
Resolver []string `yaml:"resolver,omitempty" json:"resolver,omitempty"`
Bindings map[string][]string `yaml:"bindings,omitempty" json:"bindings,omitempty"` // zone -> interfaces
Sets []RenderedSet `yaml:"sets,omitempty" json:"sets,omitempty"`
Rules []RenderedRule `yaml:"rules,omitempty" json:"rules,omitempty"`
Policies []RenderedPolicy `yaml:"policies,omitempty" json:"policies,omitempty"`
}
type RenderedSettings struct {
AddressFamily string `yaml:"address_family" json:"address_family"`
LogLevel string `yaml:"log_level" json:"log_level"`
IPForwarding bool `yaml:"ip_forwarding" json:"ip_forwarding"`
TableName string `yaml:"table_name" json:"table_name"`
}
// RenderedSet is an address group's nftables set. Members carries the concrete
// elements the control plane knows (static CIDRs, expanded ASN prefixes); FQDNs
// are resolved on-device; ASNs are informational (already expanded into Members).
type RenderedSet struct {
Name string `yaml:"name" json:"name"`
Kind string `yaml:"kind" json:"kind"` // static | dns | asn
Members []string `yaml:"members,omitempty" json:"members,omitempty"`
FQDNs []string `yaml:"fqdns,omitempty" json:"fqdns,omitempty"`
ASNs []string `yaml:"asns,omitempty" json:"asns,omitempty"`
Refresh string `yaml:"refresh,omitempty" json:"refresh,omitempty"`
}
// RenderedMatch is one OR'd element of a rule direction: a zone's subnets AND,
// optionally, a named set to intersect with.
type RenderedMatch struct {
Zone string `yaml:"zone" json:"zone"`
Subnets []string `yaml:"subnets,omitempty" json:"subnets,omitempty"`
Set string `yaml:"set,omitempty" json:"set,omitempty"`
}
type RenderedRule struct {
Action string `yaml:"action" json:"action"`
Source []RenderedMatch `yaml:"source" json:"source"`
Dest []RenderedMatch `yaml:"dest" json:"dest"`
Proto string `yaml:"proto,omitempty" json:"proto,omitempty"`
Ports []string `yaml:"ports,omitempty" json:"ports,omitempty"`
Log string `yaml:"log,omitempty" json:"log,omitempty"`
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
type RenderedPolicy struct {
Priority int `yaml:"priority" json:"priority"`
Source string `yaml:"source" json:"source"`
Dest string `yaml:"dest" json:"dest"`
Action string `yaml:"action" json:"action"`
Log string `yaml:"log,omitempty" json:"log,omitempty"`
}
// setMembers returns the concrete address elements for a set: static/asn use
// Members; dns is resolved separately and merged in before translation.
func (s RenderedSet) staticMembers() []string { return s.Members }