protocols_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. package dynamicrouting
  2. import (
  3. "encoding/json"
  4. "os"
  5. "os/exec"
  6. "strings"
  7. "testing"
  8. )
  9. func TestAllProtocolsBirdConfigGeneration(t *testing.T) {
  10. mgr := &Manager{
  11. config: DynamicConfig{
  12. BGPInstance: BGPInstance{
  13. Enabled: true,
  14. AS: 65001,
  15. RouterID: "10.42.62.254",
  16. },
  17. BGPPeers: []BGPPeer{
  18. {
  19. ID: "peer-1",
  20. Name: "Uplink",
  21. RemoteAddress: "10.42.36.253",
  22. RemoteAS: 65002,
  23. UseBFD: true,
  24. },
  25. },
  26. OSPFInstances: []OSPFInstance{
  27. {
  28. ID: "ospf-1",
  29. Name: "Default",
  30. Version: 2,
  31. RouterID: "10.42.62.254",
  32. Enabled: true,
  33. },
  34. },
  35. OSPFAreas: []OSPFArea{
  36. {
  37. ID: "area-0",
  38. Name: "Default",
  39. AreaID: "0.0.0.0",
  40. AreaType: "default",
  41. },
  42. },
  43. OSPFInterfaces: []OSPFInterface{
  44. {
  45. ID: "ospf-if-1",
  46. Interfaces: "eth0",
  47. Area: "Default",
  48. Cost: 10,
  49. UseBFD: true,
  50. },
  51. },
  52. BFDInstance: BFDInstance{
  53. Enabled: true,
  54. },
  55. BFDInterfaces: []BFDInterface{
  56. {
  57. ID: "bfd-if-1",
  58. Interface: "eth0",
  59. MinRxInterval: 100,
  60. MinTxInterval: 100,
  61. Multiplier: 3,
  62. },
  63. },
  64. BFDNeighbors: []BFDNeighbor{
  65. {
  66. ID: "bfd-nbr-1",
  67. Address: "10.42.36.253",
  68. Interface: "eth0",
  69. },
  70. },
  71. RIPInstances: []RIPInstance{
  72. {
  73. ID: "rip-1",
  74. Name: "default",
  75. Version: 2,
  76. Enabled: true,
  77. RedistributeConnected: true,
  78. RedistributeStatic: true,
  79. },
  80. },
  81. RIPInterfaces: []RIPInterface{
  82. {
  83. ID: "rip-if-1",
  84. Instance: "default",
  85. Interface: "eth0",
  86. Metric: 1,
  87. Mode: "multicast",
  88. UseBFD: true,
  89. },
  90. },
  91. BabelInstances: []BabelInstance{
  92. {
  93. ID: "babel-1",
  94. Name: "default",
  95. RouterID: "10.42.62.254",
  96. Enabled: true,
  97. RedistributeConnected: true,
  98. },
  99. },
  100. BabelInterfaces: []BabelInterface{
  101. {
  102. ID: "babel-if-1",
  103. Instance: "default",
  104. Interface: "eth0",
  105. Type: "wired",
  106. RxCost: 10,
  107. HelloInterval: 4,
  108. UpdateInterval: 16,
  109. },
  110. },
  111. RAdvInstances: []RAdvInstance{
  112. {
  113. ID: "radv-1",
  114. Name: "default",
  115. Enabled: true,
  116. },
  117. },
  118. RAdvInterfaces: []RAdvInterface{
  119. {
  120. ID: "radv-if-1",
  121. Instance: "default",
  122. Interface: "eth0",
  123. MinRAInterval: 200,
  124. MaxRAInterval: 600,
  125. DefaultLifetime: 1800,
  126. Prefixes: []string{"2001:db8:1::/64"},
  127. RDNSS: []string{"2001:4860:4860::8888"},
  128. },
  129. },
  130. },
  131. }
  132. cfg := mgr.generateBirdConfig()
  133. // Verify all protocol blocks are generated
  134. if !strings.Contains(cfg, "protocol bfd bfd_main") {
  135. t.Errorf("Expected protocol bfd bfd_main in config, got:\n%s", cfg)
  136. }
  137. if !strings.Contains(cfg, "protocol rip rip_default") {
  138. t.Errorf("Expected protocol rip in config, got:\n%s", cfg)
  139. }
  140. if !strings.Contains(cfg, "protocol babel babel_default") {
  141. t.Errorf("Expected protocol babel in config, got:\n%s", cfg)
  142. }
  143. if !strings.Contains(cfg, "protocol radv radv_default") {
  144. t.Errorf("Expected protocol radv in config, got:\n%s", cfg)
  145. }
  146. if !strings.Contains(cfg, "bfd yes;") {
  147. t.Errorf("Expected bfd yes; in peer and interface, got:\n%s", cfg)
  148. }
  149. // Validate generated config directly with BIRD 2 compiler if bird binary is available
  150. if _, err := exec.LookPath("bird"); err == nil {
  151. tmpFile, err := os.CreateTemp("", "bird_val_*.conf")
  152. if err != nil {
  153. t.Fatalf("Failed to create temp file: %v", err)
  154. }
  155. defer os.Remove(tmpFile.Name())
  156. if _, err := tmpFile.WriteString(cfg); err != nil {
  157. t.Fatalf("Failed to write temp file: %v", err)
  158. }
  159. tmpFile.Close()
  160. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  161. out, err := cmd.CombinedOutput()
  162. if err != nil {
  163. t.Fatalf("bird -p validation failed: %s\nConfig:\n%s", string(out), cfg)
  164. }
  165. }
  166. }
  167. func TestBabelFallbackInterfaceAndInstanceManagement(t *testing.T) {
  168. tmpDir, err := os.MkdirTemp("", "debtik_babel_test_*")
  169. if err != nil {
  170. t.Fatalf("Failed to create temp dir: %v", err)
  171. }
  172. defer os.RemoveAll(tmpDir)
  173. mgr := &Manager{
  174. configPath: tmpDir + "/dynamic.json",
  175. config: DynamicConfig{
  176. BabelInstances: []BabelInstance{},
  177. BabelInterfaces: []BabelInterface{},
  178. },
  179. }
  180. // 1. Save Babel instance without explicit enabled flag
  181. inst, err := mgr.SaveBabelInstance(BabelInstance{
  182. Name: "internal-Maiden2",
  183. RouterID: "192.168.132.1",
  184. })
  185. if err != nil {
  186. t.Fatalf("SaveBabelInstance failed: %v", err)
  187. }
  188. if !inst.Enabled {
  189. t.Errorf("Expected inst.Enabled to be true when Disabled is false, got false")
  190. }
  191. // 2. Generate BIRD config when no interfaces exist - should generate fallback interface "*"
  192. cfg := mgr.generateBirdConfig()
  193. if !strings.Contains(cfg, `protocol babel babel_internal_Maiden2_0`) {
  194. t.Errorf("Expected protocol babel in config, got:\n%s", cfg)
  195. }
  196. if !strings.Contains(cfg, `interface "*"`) {
  197. t.Errorf("Expected fallback interface \"*\" in config, got:\n%s", cfg)
  198. }
  199. // 3. Save Babel interface without explicit instance - should default to "internal-Maiden2"
  200. iface, err := mgr.SaveBabelInterface(BabelInterface{
  201. Interface: "eth0",
  202. RxCost: 96,
  203. })
  204. if err != nil {
  205. t.Fatalf("SaveBabelInterface failed: %v", err)
  206. }
  207. if iface.Instance != "internal-Maiden2" {
  208. t.Errorf("Expected iface.Instance to default to internal-Maiden2, got %q", iface.Instance)
  209. }
  210. // 4. Regenerate config - now should contain eth0 template
  211. cfg2 := mgr.generateBirdConfig()
  212. if !strings.Contains(cfg2, `interface "eth0"`) {
  213. t.Errorf("Expected interface \"eth0\" in config, got:\n%s", cfg2)
  214. }
  215. // 5. Validate with bird -p if bird is installed
  216. if _, err := exec.LookPath("bird"); err == nil {
  217. tmpFile, err := os.CreateTemp("", "bird_babel_val_*.conf")
  218. if err != nil {
  219. t.Fatalf("Failed to create temp file: %v", err)
  220. }
  221. defer os.Remove(tmpFile.Name())
  222. if _, err := tmpFile.WriteString(cfg2); err != nil {
  223. t.Fatalf("Failed to write temp file: %v", err)
  224. }
  225. tmpFile.Close()
  226. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  227. out, err := cmd.CombinedOutput()
  228. if err != nil {
  229. t.Fatalf("bird -p validation failed on babel config: %s\nConfig:\n%s", string(out), cfg2)
  230. }
  231. }
  232. }
  233. func TestBabelFiltersAndValidation(t *testing.T) {
  234. tmpDir, err := os.MkdirTemp("", "debtik_babel_filter_test_*")
  235. if err != nil {
  236. t.Fatalf("Failed to create temp dir: %v", err)
  237. }
  238. defer os.RemoveAll(tmpDir)
  239. mgr := &Manager{
  240. configPath: tmpDir + "/dynamic.json",
  241. config: DynamicConfig{
  242. RoutingFilters: []RoutingFilterRule{
  243. {Chain: "babel-in", RuleOrder: 10, Rule: "if (dst in 192.168.100.0/24) { accept; }"},
  244. {Chain: "babel-out", RuleOrder: 10, Rule: "if (dst in 10.0.0.0/8) { accept; }"},
  245. },
  246. BabelInstances: []BabelInstance{
  247. {
  248. Name: "test_babel",
  249. RouterID: "1.1.1.1",
  250. InFilter: "babel-in",
  251. OutFilter: "babel-out",
  252. RedistributeStatic: true,
  253. },
  254. },
  255. BabelInterfaces: []BabelInterface{
  256. {Interface: "eth0", Instance: "test_babel"},
  257. },
  258. },
  259. }
  260. cfg := mgr.generateBirdConfig()
  261. if !strings.Contains(cfg, "import filter filter_babel_in;") {
  262. t.Fatalf("expected import filter filter_babel_in in config:\n%s", cfg)
  263. }
  264. if !strings.Contains(cfg, "fn_filter_babel_out();") {
  265. t.Fatalf("expected fn_filter_babel_out() in export filter:\n%s", cfg)
  266. }
  267. if _, err := exec.LookPath("bird"); err == nil {
  268. tmpFile, err := os.CreateTemp("", "bird_babel_filter_*.conf")
  269. if err != nil {
  270. t.Fatalf("Failed to create temp file: %v", err)
  271. }
  272. defer os.Remove(tmpFile.Name())
  273. if _, err := tmpFile.WriteString(cfg); err != nil {
  274. t.Fatalf("Failed to write temp file: %v", err)
  275. }
  276. tmpFile.Close()
  277. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  278. out, err := cmd.CombinedOutput()
  279. if err != nil {
  280. t.Fatalf("bird -p validation failed on babel filter config: %s\nConfig:\n%s", string(out), cfg)
  281. }
  282. }
  283. }
  284. func TestBFDAndRIPAndRAdvManagement(t *testing.T) {
  285. tmpDir, err := os.MkdirTemp("", "debtik_protocols_test_*")
  286. if err != nil {
  287. t.Fatalf("Failed to create temp dir: %v", err)
  288. }
  289. defer os.RemoveAll(tmpDir)
  290. mgr := &Manager{
  291. configPath: tmpDir + "/dynamic.json",
  292. }
  293. // 1. Save BFD instance
  294. err = mgr.SaveBFDInstance(BFDInstance{Enabled: true})
  295. if err != nil {
  296. t.Fatalf("SaveBFDInstance failed: %v", err)
  297. }
  298. bfdInst := mgr.GetBFDInstance()
  299. if !bfdInst.Enabled {
  300. t.Errorf("Expected bfdInst.Enabled to be true")
  301. }
  302. // 2. Check BFD config generation with 0 interfaces - should contain fallback interface "*"
  303. cfg := mgr.generateBirdConfig()
  304. if !strings.Contains(cfg, `protocol bfd bfd_main`) {
  305. t.Errorf("Expected protocol bfd bfd_main in config, got:\n%s", cfg)
  306. }
  307. if !strings.Contains(cfg, `interface "*"`) {
  308. t.Errorf("Expected fallback interface \"*\" in BFD config, got:\n%s", cfg)
  309. }
  310. // 3. Save BFD multihop neighbor with local address
  311. _, err = mgr.SaveBFDNeighbor(BFDNeighbor{
  312. Address: "10.0.0.1",
  313. LocalAddress: "10.0.0.2",
  314. Multihop: true,
  315. })
  316. if err != nil {
  317. t.Fatalf("SaveBFDNeighbor failed: %v", err)
  318. }
  319. // 4. Save RIP instance without explicit enabled flag
  320. ripInst, err := mgr.SaveRIPInstance(RIPInstance{
  321. Name: "rip1",
  322. Version: 2,
  323. })
  324. if err != nil {
  325. t.Fatalf("SaveRIPInstance failed: %v", err)
  326. }
  327. if !ripInst.Enabled {
  328. t.Errorf("Expected ripInst.Enabled to be true, got false")
  329. }
  330. // 5. Save RAdv instance without explicit enabled flag
  331. radvInst, err := mgr.SaveRAdvInstance(RAdvInstance{
  332. Name: "radv1",
  333. })
  334. if err != nil {
  335. t.Fatalf("SaveRAdvInstance failed: %v", err)
  336. }
  337. if !radvInst.Enabled {
  338. t.Errorf("Expected radvInst.Enabled to be true, got false")
  339. }
  340. // 6. Verify config with BIRD compiler
  341. cfgAll := mgr.generateBirdConfig()
  342. if !strings.Contains(cfgAll, "neighbor 10.0.0.1 local 10.0.0.2 multihop;") {
  343. t.Errorf("Expected multihop neighbor with local address, got:\n%s", cfgAll)
  344. }
  345. if !strings.Contains(cfgAll, `protocol rip rip_rip1_0`) {
  346. t.Errorf("Expected protocol rip in config, got:\n%s", cfgAll)
  347. }
  348. if !strings.Contains(cfgAll, `protocol radv radv_radv1_0`) {
  349. t.Errorf("Expected protocol radv in config, got:\n%s", cfgAll)
  350. }
  351. // 7. Verify OSPF fallback interface when no templates exist
  352. mgr.config.OSPFInterfaces = nil
  353. cfgOspfFallback := mgr.generateBirdConfig()
  354. if !strings.Contains(cfgOspfFallback, `interface "*"`) {
  355. t.Errorf("Expected fallback wildcard interface for OSPF area, got:\n%s", cfgOspfFallback)
  356. }
  357. if _, err := exec.LookPath("bird"); err == nil {
  358. tmpFile, err := os.CreateTemp("", "bird_all_val_*.conf")
  359. if err != nil {
  360. t.Fatalf("Failed to create temp file: %v", err)
  361. }
  362. defer os.Remove(tmpFile.Name())
  363. if _, err := tmpFile.WriteString(cfgOspfFallback); err != nil {
  364. t.Fatalf("Failed to write temp file: %v", err)
  365. }
  366. tmpFile.Close()
  367. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  368. out, err := cmd.CombinedOutput()
  369. if err != nil {
  370. t.Fatalf("bird -p validation failed on all protocols config: %s\nConfig:\n%s", string(out), cfgOspfFallback)
  371. }
  372. }
  373. }
  374. func TestRIPAndOSPFInstanceJSONUnmarshal(t *testing.T) {
  375. testCasesRIP := []struct {
  376. jsonStr string
  377. expectedVersion int
  378. }{
  379. {`{"name":"rip1","version":2}`, 2},
  380. {`{"name":"rip1","version":"2"}`, 2},
  381. {`{"name":"rip1","version":"rip2"}`, 2},
  382. {`{"name":"rip1","version":"ripv2"}`, 2},
  383. {`{"name":"ripng1","version":1}`, 1},
  384. {`{"name":"ripng1","version":"1"}`, 1},
  385. {`{"name":"ripng1","version":"ripng"}`, 1},
  386. {`{"name":"ripng1","version":"ng"}`, 1},
  387. {`{"name":"ripng1","version":"ipv6"}`, 1},
  388. {`{"name":"rip_default"}`, 2},
  389. }
  390. for idx, tc := range testCasesRIP {
  391. var inst RIPInstance
  392. if err := json.Unmarshal([]byte(tc.jsonStr), &inst); err != nil {
  393. t.Fatalf("Case %d failed to unmarshal: %v", idx, err)
  394. }
  395. if inst.Version != tc.expectedVersion {
  396. t.Errorf("Case %d: expected version %d, got %d", idx, tc.expectedVersion, inst.Version)
  397. }
  398. }
  399. testCasesOSPF := []struct {
  400. jsonStr string
  401. expectedVersion int
  402. }{
  403. {`{"name":"ospf1","version":2}`, 2},
  404. {`{"name":"ospf1","version":"2"}`, 2},
  405. {`{"name":"ospf1","version":"ospfv2"}`, 2},
  406. {`{"name":"ospf1","version":"v2"}`, 2},
  407. {`{"name":"ospf3","version":3}`, 3},
  408. {`{"name":"ospf3","version":"3"}`, 3},
  409. {`{"name":"ospf3","version":"ospfv3"}`, 3},
  410. {`{"name":"ospf3","version":"v3"}`, 3},
  411. {`{"name":"ospf3","version":"ipv6"}`, 3},
  412. {`{"name":"ospf_default"}`, 2},
  413. }
  414. for idx, tc := range testCasesOSPF {
  415. var inst OSPFInstance
  416. if err := json.Unmarshal([]byte(tc.jsonStr), &inst); err != nil {
  417. t.Fatalf("OSPF Case %d failed to unmarshal: %v", idx, err)
  418. }
  419. if inst.Version != tc.expectedVersion {
  420. t.Errorf("OSPF Case %d: expected version %d, got %d", idx, tc.expectedVersion, inst.Version)
  421. }
  422. }
  423. }
  424. func TestDynamicRoutingFiltersAllProtocols(t *testing.T) {
  425. mgr := &Manager{
  426. config: DynamicConfig{
  427. RoutingFilters: []RoutingFilterRule{
  428. {
  429. ID: "rule-in-1",
  430. Chain: "filter-in",
  431. Rule: "dst in 10.0.0.0/8",
  432. Action: "accept",
  433. Disabled: false,
  434. },
  435. {
  436. ID: "rule-out-1",
  437. Chain: "filter-out",
  438. Rule: "dst in 192.168.0.0/16",
  439. Action: "accept",
  440. Disabled: false,
  441. },
  442. },
  443. BGPInstance: BGPInstance{
  444. Enabled: true,
  445. AS: 65001,
  446. RouterID: "10.42.62.254",
  447. InFilter: "filter-in",
  448. OutFilter: "filter-out",
  449. },
  450. BGPPeers: []BGPPeer{
  451. {
  452. ID: "peer-1",
  453. Name: "DefaultPeer",
  454. RemoteAddress: "10.42.36.253",
  455. RemoteAS: 65002,
  456. },
  457. },
  458. OSPFInstances: []OSPFInstance{
  459. {
  460. ID: "ospf-1",
  461. Name: "Default",
  462. Version: 2,
  463. RouterID: "10.42.62.254",
  464. Enabled: true,
  465. InFilter: "filter-in",
  466. OutFilter: "filter-out",
  467. RedistributeStatic: true,
  468. },
  469. },
  470. OSPFAreas: []OSPFArea{
  471. {
  472. ID: "area-0",
  473. Name: "backbone",
  474. AreaID: "0.0.0.0",
  475. AreaType: "default",
  476. },
  477. },
  478. OSPFInterfaces: []OSPFInterface{
  479. {
  480. ID: "ospf-if-1",
  481. Interfaces: "eth0",
  482. Area: "backbone",
  483. },
  484. },
  485. RIPInstances: []RIPInstance{
  486. {
  487. ID: "rip-1",
  488. Name: "default",
  489. Version: 2,
  490. Enabled: true,
  491. InFilter: "filter-in",
  492. OutFilter: "filter-out",
  493. RedistributeConnected: true,
  494. },
  495. },
  496. RIPInterfaces: []RIPInterface{
  497. {
  498. ID: "rip-if-1",
  499. Instance: "default",
  500. Interface: "eth0",
  501. },
  502. },
  503. BabelInstances: []BabelInstance{
  504. {
  505. ID: "babel-1",
  506. Name: "default",
  507. RouterID: "10.42.62.254",
  508. Enabled: true,
  509. InFilter: "filter-in",
  510. OutFilter: "filter-out",
  511. RedistributeConnected: true,
  512. },
  513. },
  514. BabelInterfaces: []BabelInterface{
  515. {
  516. ID: "babel-if-1",
  517. Instance: "default",
  518. Interface: "eth0",
  519. },
  520. },
  521. EIGRPInstances: []EIGRPInstance{
  522. {
  523. ID: "eigrp-1",
  524. Name: "default",
  525. AS: 100,
  526. RouterID: "10.42.62.254",
  527. Enabled: true,
  528. InFilter: "filter-in",
  529. OutFilter: "filter-out",
  530. RedistributeConnected: true,
  531. },
  532. },
  533. },
  534. }
  535. // 1. Test BIRD Config Generation
  536. birdCfg := mgr.generateBirdConfig()
  537. // Verify BGP uses global fallback filter
  538. if !strings.Contains(birdCfg, "import filter filter_filter_in;") {
  539. t.Errorf("BGP config missing import filter filter_filter_in;\nConfig:\n%s", birdCfg)
  540. }
  541. if !strings.Contains(birdCfg, "fn_filter_filter_out();") {
  542. t.Errorf("BGP config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg)
  543. }
  544. // Verify OSPF in_filter & out_filter
  545. if !strings.Contains(birdCfg, "protocol ospf v2 ospf_Default") || !strings.Contains(birdCfg, "import filter filter_filter_in;") {
  546. t.Errorf("OSPF config missing import filter filter_filter_in;\nConfig:\n%s", birdCfg)
  547. }
  548. // Verify RIP in_filter & out_filter
  549. if !strings.Contains(birdCfg, "protocol rip rip_default") || !strings.Contains(birdCfg, "fn_filter_filter_out();") {
  550. t.Errorf("RIP config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg)
  551. }
  552. // Verify Babel in_filter & out_filter
  553. if !strings.Contains(birdCfg, "protocol babel babel_default") || !strings.Contains(birdCfg, "fn_filter_filter_out();") {
  554. t.Errorf("Babel config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg)
  555. }
  556. // Verify BIRD syntax with bird -p if installed
  557. if _, err := exec.LookPath("bird"); err == nil {
  558. tmpFile, err := os.CreateTemp("", "bird_filters_val_*.conf")
  559. if err != nil {
  560. t.Fatalf("Failed to create temp file: %v", err)
  561. }
  562. defer os.Remove(tmpFile.Name())
  563. if _, err := tmpFile.WriteString(birdCfg); err != nil {
  564. t.Fatalf("Failed to write temp file: %v", err)
  565. }
  566. tmpFile.Close()
  567. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  568. out, err := cmd.CombinedOutput()
  569. if err != nil {
  570. t.Fatalf("bird -p validation failed on all protocol filters config: %s\nConfig:\n%s", string(out), birdCfg)
  571. }
  572. }
  573. // 2. Test FRR EIGRP Config Generation
  574. frrCfg := mgr.generateFRREigrpConfig()
  575. if !strings.Contains(frrCfg, "ip prefix-list pl_in_filter_in seq 10 permit 10.0.0.0/8") {
  576. t.Errorf("FRR config missing in-filter prefix list;\nConfig:\n%s", frrCfg)
  577. }
  578. if !strings.Contains(frrCfg, "ip prefix-list pl_out_filter_out seq 10 permit 192.168.0.0/16") {
  579. t.Errorf("FRR config missing out-filter prefix list;\nConfig:\n%s", frrCfg)
  580. }
  581. if !strings.Contains(frrCfg, "distribute-list prefix pl_in_filter_in in") {
  582. t.Errorf("FRR config missing distribute-list prefix in;\nConfig:\n%s", frrCfg)
  583. }
  584. if !strings.Contains(frrCfg, "distribute-list prefix pl_out_filter_out out") {
  585. t.Errorf("FRR config missing distribute-list prefix out;\nConfig:\n%s", frrCfg)
  586. }
  587. }
  588. func TestMoveRoutingFilter(t *testing.T) {
  589. mgr := &Manager{
  590. config: DynamicConfig{
  591. RoutingFilters: []RoutingFilterRule{
  592. {ID: "r1", Chain: "chain-A", RuleOrder: 10, Rule: "dst in 10.1.0.0/16"},
  593. {ID: "r2", Chain: "chain-A", RuleOrder: 20, Rule: "dst in 10.2.0.0/16"},
  594. {ID: "r3", Chain: "chain-A", RuleOrder: 30, Rule: "dst in 10.3.0.0/16"},
  595. {ID: "r-other", Chain: "chain-B", RuleOrder: 10, Rule: "dst in 172.16.0.0/12"},
  596. },
  597. },
  598. }
  599. // 1. Move r1 "up" (already at top) -> should succeed with no order change
  600. if err := mgr.MoveRoutingFilter("r1", "up"); err != nil {
  601. t.Fatalf("Move r1 up failed: %v", err)
  602. }
  603. filters := mgr.ListRoutingFilters()
  604. if filters[0].ID != "r1" || filters[0].RuleOrder != 10 {
  605. t.Errorf("Expected r1 still at order 10, got %d", filters[0].RuleOrder)
  606. }
  607. // 2. Move r1 "down" -> r2 should become order 10, r1 should become order 20
  608. if err := mgr.MoveRoutingFilter("r1", "down"); err != nil {
  609. t.Fatalf("Move r1 down failed: %v", err)
  610. }
  611. filters = mgr.ListRoutingFilters()
  612. var r1Order, r2Order int
  613. for _, f := range filters {
  614. if f.ID == "r1" {
  615. r1Order = f.RuleOrder
  616. }
  617. if f.ID == "r2" {
  618. r2Order = f.RuleOrder
  619. }
  620. }
  621. if r2Order != 10 || r1Order != 20 {
  622. t.Errorf("Expected r2 order 10 and r1 order 20, got r2=%d, r1=%d", r2Order, r1Order)
  623. }
  624. // 3. Chain B should be unaffected
  625. for _, f := range filters {
  626. if f.ID == "r-other" && f.RuleOrder != 10 {
  627. t.Errorf("Chain B rule modified unexpectedly: %d", f.RuleOrder)
  628. }
  629. }
  630. // 4. Move r3 (now at index 2 in chain-A) "up" -> should swap with r1 (at order 20)
  631. if err := mgr.MoveRoutingFilter("r3", "up"); err != nil {
  632. t.Fatalf("Move r3 up failed: %v", err)
  633. }
  634. filters = mgr.ListRoutingFilters()
  635. var r3Order int
  636. for _, f := range filters {
  637. if f.ID == "r1" {
  638. r1Order = f.RuleOrder
  639. }
  640. if f.ID == "r3" {
  641. r3Order = f.RuleOrder
  642. }
  643. }
  644. if r3Order != 20 || r1Order != 30 {
  645. t.Errorf("Expected r3 order 20 and r1 order 30, got r3=%d, r1=%d", r3Order, r1Order)
  646. }
  647. }