Files
tomswallapi/internal/compiler/compiler_test.go
T
benvin f80cc2cc30 Add per-device config compiler and agent config endpoint
Project the fleet-global model through a device's bindings into a rendered,
interface-agnostic config: rules compile to saddr/daddr forward matches with no
iif/oif so they are correct under FRR/ECMP. Firewalls always enforce; routers
enforce only when their fabric opts into defense-in-depth. Referenced address
groups are emitted as named sets carrying their source (static CIDRs, dns FQDNs,
or asn numbers) so membership churns out-of-band without a rule reload. Wire
GET /devices/{name}/config to compile and serve YAML, generation-stamped. Add
portgroups/policies/settings store methods and portgroup CRUD. Pure Render is
unit-tested for enforcement gating, ASN set emission, and resolver precedence.
2026-07-19 18:38:52 +10:00

130 lines
4.4 KiB
Go

package compiler
import "testing"
import "git.unkin.net/unkin/tomswallapi/internal/model"
func baseInput() Input {
return Input{
Generation: 7,
Settings: model.Settings{AddressFamily: "inet", LogLevel: "info", IPForwarding: true, TableName: "tomswall", DefaultResolver: []string{"10.0.0.53"}},
Zones: map[string]model.Zone{
"zone-a": {Name: "zone-a", Type: "ip", Subnets: []string{"10.1.0.0/24"}},
"net": {Name: "net", Type: "ip"}, // no subnets: internet-facing
},
Groups: map[string]model.AddressGroup{
"cloudflare": {Name: "cloudflare", Type: model.GroupASN, Members: []string{"13335"}, Refresh: "24h"},
},
PortGroups: map[string]model.PortGroup{
"https": {Name: "https", Proto: "tcp", Ports: []string{"443"}},
},
Rules: []model.Rule{
{ID: 1, Action: "accept", Source: []string{"zone-a"}, Dest: []string{"net:+asn_cloudflare"}, PortGroup: "https"},
},
}
}
func TestRenderFirewallEnforcesAndEmitsSet(t *testing.T) {
in := baseInput()
in.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall}
in.Bindings = []model.Binding{{Device: "fw-a", Zone: "zone-a", Interfaces: []string{"eth1"}}}
cfg, err := Render(in)
if err != nil {
t.Fatalf("Render: %v", err)
}
if !cfg.Enforcing {
t.Fatal("firewall should enforce")
}
if cfg.Generation != 7 {
t.Errorf("generation = %d, want 7", cfg.Generation)
}
if len(cfg.Rules) != 1 {
t.Fatalf("want 1 rule, got %d", len(cfg.Rules))
}
r := cfg.Rules[0]
// Interface-agnostic: source resolves to zone-a's subnets, no iif/oif.
if len(r.Source) != 1 || r.Source[0].Zone != "zone-a" || len(r.Source[0].Subnets) != 1 || r.Source[0].Subnets[0] != "10.1.0.0/24" {
t.Errorf("unexpected source match: %+v", r.Source)
}
// Dest is the no-subnet `net` zone gated by the asn set.
if len(r.Dest) != 1 || r.Dest[0].Zone != "net" || r.Dest[0].Set != "asn_cloudflare" {
t.Errorf("unexpected dest match: %+v", r.Dest)
}
if len(r.Dest[0].Subnets) != 0 {
t.Errorf("net should carry no subnets, got %v", r.Dest[0].Subnets)
}
if r.Proto != "tcp" || len(r.Ports) != 1 || r.Ports[0] != "443" {
t.Errorf("portgroup not resolved: proto=%q ports=%v", r.Proto, r.Ports)
}
// The referenced asn group must be emitted as a set carrying its source ASNs.
if len(cfg.Sets) != 1 {
t.Fatalf("want 1 set, got %d", len(cfg.Sets))
}
set := cfg.Sets[0]
if set.Name != "asn_cloudflare" || set.Kind != model.GroupASN || len(set.ASNs) != 1 || set.ASNs[0] != "13335" {
t.Errorf("unexpected set: %+v", set)
}
if set.Members != nil {
t.Errorf("asn set should not carry inline members before expansion, got %v", set.Members)
}
// Binding surfaced for the agent.
if got := cfg.Bindings["zone-a"]; len(got) != 1 || got[0] != "eth1" {
t.Errorf("binding not surfaced: %v", cfg.Bindings)
}
}
func TestRenderTransparentRouterHasNoRules(t *testing.T) {
in := baseInput()
in.Device = model.Device{Name: "rt1", Class: model.ClassRouter, Fabric: "core"}
in.Fabric = &model.Fabric{Name: "core", EnforceOnRouters: false}
cfg, err := Render(in)
if err != nil {
t.Fatalf("Render: %v", err)
}
if cfg.Enforcing {
t.Fatal("transparent router should not enforce")
}
if len(cfg.Rules) != 0 || len(cfg.Sets) != 0 {
t.Errorf("transparent router should emit no rules/sets, got %d rules %d sets", len(cfg.Rules), len(cfg.Sets))
}
}
func TestRenderEnforcingRouter(t *testing.T) {
in := baseInput()
in.Device = model.Device{Name: "rt1", Class: model.ClassRouter, Fabric: "core"}
in.Fabric = &model.Fabric{Name: "core", EnforceOnRouters: true}
cfg, err := Render(in)
if err != nil {
t.Fatalf("Render: %v", err)
}
if !cfg.Enforcing || len(cfg.Rules) != 1 {
t.Errorf("defense-in-depth router should enforce the rule: enforcing=%v rules=%d", cfg.Enforcing, len(cfg.Rules))
}
}
func TestRenderUnknownGroupIsError(t *testing.T) {
in := baseInput()
in.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall}
in.Rules = []model.Rule{{ID: 9, Action: "accept", Source: []string{"zone-a"}, Dest: []string{"net:+nope"}}}
if _, err := Render(in); err == nil {
t.Fatal("expected error for unknown address group")
}
}
func TestEffectiveResolverPrefersDevice(t *testing.T) {
in := baseInput()
in.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall, Resolver: []string{"10.9.9.9"}}
cfg, err := Render(in)
if err != nil {
t.Fatalf("Render: %v", err)
}
if len(cfg.Resolver) != 1 || cfg.Resolver[0] != "10.9.9.9" {
t.Errorf("device resolver should win: %v", cfg.Resolver)
}
}