package dynamicrouting import ( "encoding/json" "os" "os/exec" "strings" "testing" ) func TestAllProtocolsBirdConfigGeneration(t *testing.T) { mgr := &Manager{ config: DynamicConfig{ BGPInstance: BGPInstance{ Enabled: true, AS: 65001, RouterID: "10.42.62.254", }, BGPPeers: []BGPPeer{ { ID: "peer-1", Name: "Uplink", RemoteAddress: "10.42.36.253", RemoteAS: 65002, UseBFD: true, }, }, OSPFInstances: []OSPFInstance{ { ID: "ospf-1", Name: "Default", Version: 2, RouterID: "10.42.62.254", Enabled: true, }, }, OSPFAreas: []OSPFArea{ { ID: "area-0", Name: "Default", AreaID: "0.0.0.0", AreaType: "default", }, }, OSPFInterfaces: []OSPFInterface{ { ID: "ospf-if-1", Interfaces: "eth0", Area: "Default", Cost: 10, UseBFD: true, }, }, BFDInstance: BFDInstance{ Enabled: true, }, BFDInterfaces: []BFDInterface{ { ID: "bfd-if-1", Interface: "eth0", MinRxInterval: 100, MinTxInterval: 100, Multiplier: 3, }, }, BFDNeighbors: []BFDNeighbor{ { ID: "bfd-nbr-1", Address: "10.42.36.253", Interface: "eth0", }, }, RIPInstances: []RIPInstance{ { ID: "rip-1", Name: "default", Version: 2, Enabled: true, RedistributeConnected: true, RedistributeStatic: true, }, }, RIPInterfaces: []RIPInterface{ { ID: "rip-if-1", Instance: "default", Interface: "eth0", Metric: 1, Mode: "multicast", UseBFD: true, }, }, BabelInstances: []BabelInstance{ { ID: "babel-1", Name: "default", RouterID: "10.42.62.254", Enabled: true, RedistributeConnected: true, }, }, BabelInterfaces: []BabelInterface{ { ID: "babel-if-1", Instance: "default", Interface: "eth0", Type: "wired", RxCost: 10, HelloInterval: 4, UpdateInterval: 16, }, }, RAdvInstances: []RAdvInstance{ { ID: "radv-1", Name: "default", Enabled: true, }, }, RAdvInterfaces: []RAdvInterface{ { ID: "radv-if-1", Instance: "default", Interface: "eth0", MinRAInterval: 200, MaxRAInterval: 600, DefaultLifetime: 1800, Prefixes: []string{"2001:db8:1::/64"}, RDNSS: []string{"2001:4860:4860::8888"}, }, }, }, } cfg := mgr.generateBirdConfig() // Verify all protocol blocks are generated if !strings.Contains(cfg, "protocol bfd bfd_main") { t.Errorf("Expected protocol bfd bfd_main in config, got:\n%s", cfg) } if !strings.Contains(cfg, "protocol rip rip_default") { t.Errorf("Expected protocol rip in config, got:\n%s", cfg) } if !strings.Contains(cfg, "protocol babel babel_default") { t.Errorf("Expected protocol babel in config, got:\n%s", cfg) } if !strings.Contains(cfg, "protocol radv radv_default") { t.Errorf("Expected protocol radv in config, got:\n%s", cfg) } if !strings.Contains(cfg, "bfd yes;") { t.Errorf("Expected bfd yes; in peer and interface, got:\n%s", cfg) } // Validate generated config directly with BIRD 2 compiler if bird binary is available if _, err := exec.LookPath("bird"); err == nil { tmpFile, err := os.CreateTemp("", "bird_val_*.conf") if err != nil { t.Fatalf("Failed to create temp file: %v", err) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(cfg); err != nil { t.Fatalf("Failed to write temp file: %v", err) } tmpFile.Close() cmd := exec.Command("bird", "-p", "-c", tmpFile.Name()) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("bird -p validation failed: %s\nConfig:\n%s", string(out), cfg) } } } func TestBabelFallbackInterfaceAndInstanceManagement(t *testing.T) { tmpDir, err := os.MkdirTemp("", "debtik_babel_test_*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) mgr := &Manager{ configPath: tmpDir + "/dynamic.json", config: DynamicConfig{ BabelInstances: []BabelInstance{}, BabelInterfaces: []BabelInterface{}, }, } // 1. Save Babel instance without explicit enabled flag inst, err := mgr.SaveBabelInstance(BabelInstance{ Name: "internal-Maiden2", RouterID: "192.168.132.1", }) if err != nil { t.Fatalf("SaveBabelInstance failed: %v", err) } if !inst.Enabled { t.Errorf("Expected inst.Enabled to be true when Disabled is false, got false") } // 2. Generate BIRD config when no interfaces exist - should generate fallback interface "*" cfg := mgr.generateBirdConfig() if !strings.Contains(cfg, `protocol babel babel_internal_Maiden2_0`) { t.Errorf("Expected protocol babel in config, got:\n%s", cfg) } if !strings.Contains(cfg, `interface "*"`) { t.Errorf("Expected fallback interface \"*\" in config, got:\n%s", cfg) } // 3. Save Babel interface without explicit instance - should default to "internal-Maiden2" iface, err := mgr.SaveBabelInterface(BabelInterface{ Interface: "eth0", RxCost: 96, }) if err != nil { t.Fatalf("SaveBabelInterface failed: %v", err) } if iface.Instance != "internal-Maiden2" { t.Errorf("Expected iface.Instance to default to internal-Maiden2, got %q", iface.Instance) } // 4. Regenerate config - now should contain eth0 template cfg2 := mgr.generateBirdConfig() if !strings.Contains(cfg2, `interface "eth0"`) { t.Errorf("Expected interface \"eth0\" in config, got:\n%s", cfg2) } // 5. Validate with bird -p if bird is installed if _, err := exec.LookPath("bird"); err == nil { tmpFile, err := os.CreateTemp("", "bird_babel_val_*.conf") if err != nil { t.Fatalf("Failed to create temp file: %v", err) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(cfg2); err != nil { t.Fatalf("Failed to write temp file: %v", err) } tmpFile.Close() cmd := exec.Command("bird", "-p", "-c", tmpFile.Name()) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("bird -p validation failed on babel config: %s\nConfig:\n%s", string(out), cfg2) } } } func TestBabelFiltersAndValidation(t *testing.T) { tmpDir, err := os.MkdirTemp("", "debtik_babel_filter_test_*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) mgr := &Manager{ configPath: tmpDir + "/dynamic.json", config: DynamicConfig{ RoutingFilters: []RoutingFilterRule{ {Chain: "babel-in", RuleOrder: 10, Rule: "if (dst in 192.168.100.0/24) { accept; }"}, {Chain: "babel-out", RuleOrder: 10, Rule: "if (dst in 10.0.0.0/8) { accept; }"}, }, BabelInstances: []BabelInstance{ { Name: "test_babel", RouterID: "1.1.1.1", InFilter: "babel-in", OutFilter: "babel-out", RedistributeStatic: true, }, }, BabelInterfaces: []BabelInterface{ {Interface: "eth0", Instance: "test_babel"}, }, }, } cfg := mgr.generateBirdConfig() if !strings.Contains(cfg, "import filter filter_babel_in;") { t.Fatalf("expected import filter filter_babel_in in config:\n%s", cfg) } if !strings.Contains(cfg, "fn_filter_babel_out();") { t.Fatalf("expected fn_filter_babel_out() in export filter:\n%s", cfg) } if _, err := exec.LookPath("bird"); err == nil { tmpFile, err := os.CreateTemp("", "bird_babel_filter_*.conf") if err != nil { t.Fatalf("Failed to create temp file: %v", err) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(cfg); err != nil { t.Fatalf("Failed to write temp file: %v", err) } tmpFile.Close() cmd := exec.Command("bird", "-p", "-c", tmpFile.Name()) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("bird -p validation failed on babel filter config: %s\nConfig:\n%s", string(out), cfg) } } } func TestBFDAndRIPAndRAdvManagement(t *testing.T) { tmpDir, err := os.MkdirTemp("", "debtik_protocols_test_*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) mgr := &Manager{ configPath: tmpDir + "/dynamic.json", } // 1. Save BFD instance err = mgr.SaveBFDInstance(BFDInstance{Enabled: true}) if err != nil { t.Fatalf("SaveBFDInstance failed: %v", err) } bfdInst := mgr.GetBFDInstance() if !bfdInst.Enabled { t.Errorf("Expected bfdInst.Enabled to be true") } // 2. Check BFD config generation with 0 interfaces - should contain fallback interface "*" cfg := mgr.generateBirdConfig() if !strings.Contains(cfg, `protocol bfd bfd_main`) { t.Errorf("Expected protocol bfd bfd_main in config, got:\n%s", cfg) } if !strings.Contains(cfg, `interface "*"`) { t.Errorf("Expected fallback interface \"*\" in BFD config, got:\n%s", cfg) } // 3. Save BFD multihop neighbor with local address _, err = mgr.SaveBFDNeighbor(BFDNeighbor{ Address: "10.0.0.1", LocalAddress: "10.0.0.2", Multihop: true, }) if err != nil { t.Fatalf("SaveBFDNeighbor failed: %v", err) } // 4. Save RIP instance without explicit enabled flag ripInst, err := mgr.SaveRIPInstance(RIPInstance{ Name: "rip1", Version: 2, }) if err != nil { t.Fatalf("SaveRIPInstance failed: %v", err) } if !ripInst.Enabled { t.Errorf("Expected ripInst.Enabled to be true, got false") } // 5. Save RAdv instance without explicit enabled flag radvInst, err := mgr.SaveRAdvInstance(RAdvInstance{ Name: "radv1", }) if err != nil { t.Fatalf("SaveRAdvInstance failed: %v", err) } if !radvInst.Enabled { t.Errorf("Expected radvInst.Enabled to be true, got false") } // 6. Verify config with BIRD compiler cfgAll := mgr.generateBirdConfig() if !strings.Contains(cfgAll, "neighbor 10.0.0.1 local 10.0.0.2 multihop;") { t.Errorf("Expected multihop neighbor with local address, got:\n%s", cfgAll) } if !strings.Contains(cfgAll, `protocol rip rip_rip1_0`) { t.Errorf("Expected protocol rip in config, got:\n%s", cfgAll) } if !strings.Contains(cfgAll, `protocol radv radv_radv1_0`) { t.Errorf("Expected protocol radv in config, got:\n%s", cfgAll) } // 7. Verify OSPF fallback interface when no templates exist mgr.config.OSPFInterfaces = nil cfgOspfFallback := mgr.generateBirdConfig() if !strings.Contains(cfgOspfFallback, `interface "*"`) { t.Errorf("Expected fallback wildcard interface for OSPF area, got:\n%s", cfgOspfFallback) } if _, err := exec.LookPath("bird"); err == nil { tmpFile, err := os.CreateTemp("", "bird_all_val_*.conf") if err != nil { t.Fatalf("Failed to create temp file: %v", err) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(cfgOspfFallback); err != nil { t.Fatalf("Failed to write temp file: %v", err) } tmpFile.Close() cmd := exec.Command("bird", "-p", "-c", tmpFile.Name()) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("bird -p validation failed on all protocols config: %s\nConfig:\n%s", string(out), cfgOspfFallback) } } } func TestRIPAndOSPFInstanceJSONUnmarshal(t *testing.T) { testCasesRIP := []struct { jsonStr string expectedVersion int }{ {`{"name":"rip1","version":2}`, 2}, {`{"name":"rip1","version":"2"}`, 2}, {`{"name":"rip1","version":"rip2"}`, 2}, {`{"name":"rip1","version":"ripv2"}`, 2}, {`{"name":"ripng1","version":1}`, 1}, {`{"name":"ripng1","version":"1"}`, 1}, {`{"name":"ripng1","version":"ripng"}`, 1}, {`{"name":"ripng1","version":"ng"}`, 1}, {`{"name":"ripng1","version":"ipv6"}`, 1}, {`{"name":"rip_default"}`, 2}, } for idx, tc := range testCasesRIP { var inst RIPInstance if err := json.Unmarshal([]byte(tc.jsonStr), &inst); err != nil { t.Fatalf("Case %d failed to unmarshal: %v", idx, err) } if inst.Version != tc.expectedVersion { t.Errorf("Case %d: expected version %d, got %d", idx, tc.expectedVersion, inst.Version) } } testCasesOSPF := []struct { jsonStr string expectedVersion int }{ {`{"name":"ospf1","version":2}`, 2}, {`{"name":"ospf1","version":"2"}`, 2}, {`{"name":"ospf1","version":"ospfv2"}`, 2}, {`{"name":"ospf1","version":"v2"}`, 2}, {`{"name":"ospf3","version":3}`, 3}, {`{"name":"ospf3","version":"3"}`, 3}, {`{"name":"ospf3","version":"ospfv3"}`, 3}, {`{"name":"ospf3","version":"v3"}`, 3}, {`{"name":"ospf3","version":"ipv6"}`, 3}, {`{"name":"ospf_default"}`, 2}, } for idx, tc := range testCasesOSPF { var inst OSPFInstance if err := json.Unmarshal([]byte(tc.jsonStr), &inst); err != nil { t.Fatalf("OSPF Case %d failed to unmarshal: %v", idx, err) } if inst.Version != tc.expectedVersion { t.Errorf("OSPF Case %d: expected version %d, got %d", idx, tc.expectedVersion, inst.Version) } } } func TestDynamicRoutingFiltersAllProtocols(t *testing.T) { mgr := &Manager{ config: DynamicConfig{ RoutingFilters: []RoutingFilterRule{ { ID: "rule-in-1", Chain: "filter-in", Rule: "dst in 10.0.0.0/8", Action: "accept", Disabled: false, }, { ID: "rule-out-1", Chain: "filter-out", Rule: "dst in 192.168.0.0/16", Action: "accept", Disabled: false, }, }, BGPInstance: BGPInstance{ Enabled: true, AS: 65001, RouterID: "10.42.62.254", InFilter: "filter-in", OutFilter: "filter-out", }, BGPPeers: []BGPPeer{ { ID: "peer-1", Name: "DefaultPeer", RemoteAddress: "10.42.36.253", RemoteAS: 65002, }, }, OSPFInstances: []OSPFInstance{ { ID: "ospf-1", Name: "Default", Version: 2, RouterID: "10.42.62.254", Enabled: true, InFilter: "filter-in", OutFilter: "filter-out", RedistributeStatic: true, }, }, OSPFAreas: []OSPFArea{ { ID: "area-0", Name: "backbone", AreaID: "0.0.0.0", AreaType: "default", }, }, OSPFInterfaces: []OSPFInterface{ { ID: "ospf-if-1", Interfaces: "eth0", Area: "backbone", }, }, RIPInstances: []RIPInstance{ { ID: "rip-1", Name: "default", Version: 2, Enabled: true, InFilter: "filter-in", OutFilter: "filter-out", RedistributeConnected: true, }, }, RIPInterfaces: []RIPInterface{ { ID: "rip-if-1", Instance: "default", Interface: "eth0", }, }, BabelInstances: []BabelInstance{ { ID: "babel-1", Name: "default", RouterID: "10.42.62.254", Enabled: true, InFilter: "filter-in", OutFilter: "filter-out", RedistributeConnected: true, }, }, BabelInterfaces: []BabelInterface{ { ID: "babel-if-1", Instance: "default", Interface: "eth0", }, }, EIGRPInstances: []EIGRPInstance{ { ID: "eigrp-1", Name: "default", AS: 100, RouterID: "10.42.62.254", Enabled: true, InFilter: "filter-in", OutFilter: "filter-out", RedistributeConnected: true, }, }, }, } // 1. Test BIRD Config Generation birdCfg := mgr.generateBirdConfig() // Verify BGP uses global fallback filter if !strings.Contains(birdCfg, "import filter filter_filter_in;") { t.Errorf("BGP config missing import filter filter_filter_in;\nConfig:\n%s", birdCfg) } if !strings.Contains(birdCfg, "fn_filter_filter_out();") { t.Errorf("BGP config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg) } // Verify OSPF in_filter & out_filter if !strings.Contains(birdCfg, "protocol ospf v2 ospf_Default") || !strings.Contains(birdCfg, "import filter filter_filter_in;") { t.Errorf("OSPF config missing import filter filter_filter_in;\nConfig:\n%s", birdCfg) } // Verify RIP in_filter & out_filter if !strings.Contains(birdCfg, "protocol rip rip_default") || !strings.Contains(birdCfg, "fn_filter_filter_out();") { t.Errorf("RIP config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg) } // Verify Babel in_filter & out_filter if !strings.Contains(birdCfg, "protocol babel babel_default") || !strings.Contains(birdCfg, "fn_filter_filter_out();") { t.Errorf("Babel config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg) } // Verify BIRD syntax with bird -p if installed if _, err := exec.LookPath("bird"); err == nil { tmpFile, err := os.CreateTemp("", "bird_filters_val_*.conf") if err != nil { t.Fatalf("Failed to create temp file: %v", err) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(birdCfg); err != nil { t.Fatalf("Failed to write temp file: %v", err) } tmpFile.Close() cmd := exec.Command("bird", "-p", "-c", tmpFile.Name()) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("bird -p validation failed on all protocol filters config: %s\nConfig:\n%s", string(out), birdCfg) } } // 2. Test FRR EIGRP Config Generation frrCfg := mgr.generateFRREigrpConfig() if !strings.Contains(frrCfg, "ip prefix-list pl_in_filter_in seq 10 permit 10.0.0.0/8") { t.Errorf("FRR config missing in-filter prefix list;\nConfig:\n%s", frrCfg) } if !strings.Contains(frrCfg, "ip prefix-list pl_out_filter_out seq 10 permit 192.168.0.0/16") { t.Errorf("FRR config missing out-filter prefix list;\nConfig:\n%s", frrCfg) } if !strings.Contains(frrCfg, "distribute-list prefix pl_in_filter_in in") { t.Errorf("FRR config missing distribute-list prefix in;\nConfig:\n%s", frrCfg) } if !strings.Contains(frrCfg, "distribute-list prefix pl_out_filter_out out") { t.Errorf("FRR config missing distribute-list prefix out;\nConfig:\n%s", frrCfg) } } func TestMoveRoutingFilter(t *testing.T) { mgr := &Manager{ config: DynamicConfig{ RoutingFilters: []RoutingFilterRule{ {ID: "r1", Chain: "chain-A", RuleOrder: 10, Rule: "dst in 10.1.0.0/16"}, {ID: "r2", Chain: "chain-A", RuleOrder: 20, Rule: "dst in 10.2.0.0/16"}, {ID: "r3", Chain: "chain-A", RuleOrder: 30, Rule: "dst in 10.3.0.0/16"}, {ID: "r-other", Chain: "chain-B", RuleOrder: 10, Rule: "dst in 172.16.0.0/12"}, }, }, } // 1. Move r1 "up" (already at top) -> should succeed with no order change if err := mgr.MoveRoutingFilter("r1", "up"); err != nil { t.Fatalf("Move r1 up failed: %v", err) } filters := mgr.ListRoutingFilters() if filters[0].ID != "r1" || filters[0].RuleOrder != 10 { t.Errorf("Expected r1 still at order 10, got %d", filters[0].RuleOrder) } // 2. Move r1 "down" -> r2 should become order 10, r1 should become order 20 if err := mgr.MoveRoutingFilter("r1", "down"); err != nil { t.Fatalf("Move r1 down failed: %v", err) } filters = mgr.ListRoutingFilters() var r1Order, r2Order int for _, f := range filters { if f.ID == "r1" { r1Order = f.RuleOrder } if f.ID == "r2" { r2Order = f.RuleOrder } } if r2Order != 10 || r1Order != 20 { t.Errorf("Expected r2 order 10 and r1 order 20, got r2=%d, r1=%d", r2Order, r1Order) } // 3. Chain B should be unaffected for _, f := range filters { if f.ID == "r-other" && f.RuleOrder != 10 { t.Errorf("Chain B rule modified unexpectedly: %d", f.RuleOrder) } } // 4. Move r3 (now at index 2 in chain-A) "up" -> should swap with r1 (at order 20) if err := mgr.MoveRoutingFilter("r3", "up"); err != nil { t.Fatalf("Move r3 up failed: %v", err) } filters = mgr.ListRoutingFilters() var r3Order int for _, f := range filters { if f.ID == "r1" { r1Order = f.RuleOrder } if f.ID == "r3" { r3Order = f.RuleOrder } } if r3Order != 20 || r1Order != 30 { t.Errorf("Expected r3 order 20 and r1 order 30, got r3=%d, r1=%d", r3Order, r1Order) } }