Render the NAT tier into per-device configs
The compiler now projects the global NAT intents through each device's bindings into the rendered config: - snat/masquerade: renders on a device that binds the egress zone (and, when the source is a zone, that zone too), resolving the egress interface — this auto-scopes masquerade to edge devices. A literal-CIDR source needs only the egress binding. - netmap: renders on the device its anchor (device:zone|interface) names, resolving a zone anchor to its bound interface. - 1:1 nat: renders on the device it is bound to. Adds RenderedSNAT/RenderedNetmap/RenderedNAT to the rendered config, fetches the tiers in Compile, and unit-tests binding-scoping (edge vs interior/other device).
This commit is contained in:
@@ -141,6 +141,61 @@ func TestReportedFIBDoesNotLimitRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func natInput() Input {
|
||||
return Input{
|
||||
Zones: map[string]model.Zone{
|
||||
"loc": {Name: "loc", Subnets: []string{"10.1.0.0/24"}},
|
||||
"net": {Name: "net"},
|
||||
},
|
||||
SNAT: []model.SNATRule{{ID: 1, Action: "masquerade", Source: "loc", Egress: "net"}},
|
||||
Netmap: []model.NetmapRule{{ID: 1, Type: "dnat", FromNet: "10.0.0.0/24", ToNet: "192.168.1.0/24", Anchor: "fw-a:net"}},
|
||||
NAT: []model.NATRule{{ID: 1, Device: "fw-a", External: "203.0.113.10", Internal: "10.1.0.10", Interface: "eth0"}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderNATScopesToBindings(t *testing.T) {
|
||||
// fw-a binds both loc and net (an edge) → masquerade + its netmap + its nat.
|
||||
edge := natInput()
|
||||
edge.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall}
|
||||
edge.Bindings = []model.Binding{
|
||||
{Device: "fw-a", Zone: "loc", Interfaces: []string{"eth1"}},
|
||||
{Device: "fw-a", Zone: "net", Interfaces: []string{"eth0"}},
|
||||
}
|
||||
cfg, err := Render(edge)
|
||||
if err != nil {
|
||||
t.Fatalf("Render: %v", err)
|
||||
}
|
||||
if len(cfg.SNAT) != 1 || cfg.SNAT[0].Action != "masquerade" ||
|
||||
len(cfg.SNAT[0].Source) != 1 || cfg.SNAT[0].Source[0] != "10.1.0.0/24" ||
|
||||
len(cfg.SNAT[0].Egress) != 1 || cfg.SNAT[0].Egress[0] != "eth0" {
|
||||
t.Errorf("edge masquerade not rendered correctly: %+v", cfg.SNAT)
|
||||
}
|
||||
if len(cfg.Netmap) != 1 || cfg.Netmap[0].Interface != "eth0" {
|
||||
t.Errorf("netmap anchor not resolved to eth0: %+v", cfg.Netmap)
|
||||
}
|
||||
if len(cfg.NAT) != 1 || cfg.NAT[0].External != "203.0.113.10" {
|
||||
t.Errorf("1:1 nat not rendered on its device: %+v", cfg.NAT)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderNATSkipsNonEgressAndOtherDevices(t *testing.T) {
|
||||
// rt1 binds only net (not loc): masquerade requires both, so it's skipped;
|
||||
// the netmap/nat are anchored/bound to fw-a, so they don't render here either.
|
||||
interior := natInput()
|
||||
interior.Device = model.Device{Name: "rt1", Class: model.ClassRouter}
|
||||
interior.Bindings = []model.Binding{{Device: "rt1", Zone: "net", Interfaces: []string{"eth0"}}}
|
||||
cfg, err := Render(interior)
|
||||
if err != nil {
|
||||
t.Fatalf("Render: %v", err)
|
||||
}
|
||||
if len(cfg.SNAT) != 0 {
|
||||
t.Errorf("masquerade should not render without the source-zone binding: %+v", cfg.SNAT)
|
||||
}
|
||||
if len(cfg.Netmap) != 0 || len(cfg.NAT) != 0 {
|
||||
t.Errorf("netmap/nat must not render on a device they aren't bound to: %+v %+v", cfg.Netmap, cfg.NAT)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveResolverPrefersDevice(t *testing.T) {
|
||||
in := baseInput()
|
||||
in.Device = model.Device{Name: "fw-a", Class: model.ClassFirewall, Resolver: []string{"10.9.9.9"}}
|
||||
|
||||
Reference in New Issue
Block a user