protocols_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  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 !strings.Contains(cfg, "RTS_BABEL") {
  268. t.Fatalf("expected RTS_BABEL in export filter for multi-hop transit:\n%s", cfg)
  269. }
  270. if _, err := exec.LookPath("bird"); err == nil {
  271. tmpFile, err := os.CreateTemp("", "bird_babel_filter_*.conf")
  272. if err != nil {
  273. t.Fatalf("Failed to create temp file: %v", err)
  274. }
  275. defer os.Remove(tmpFile.Name())
  276. if _, err := tmpFile.WriteString(cfg); err != nil {
  277. t.Fatalf("Failed to write temp file: %v", err)
  278. }
  279. tmpFile.Close()
  280. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  281. out, err := cmd.CombinedOutput()
  282. if err != nil {
  283. t.Fatalf("bird -p validation failed on babel filter config: %s\nConfig:\n%s", string(out), cfg)
  284. }
  285. }
  286. }
  287. func TestBFDAndRIPAndRAdvManagement(t *testing.T) {
  288. tmpDir, err := os.MkdirTemp("", "debtik_protocols_test_*")
  289. if err != nil {
  290. t.Fatalf("Failed to create temp dir: %v", err)
  291. }
  292. defer os.RemoveAll(tmpDir)
  293. mgr := &Manager{
  294. configPath: tmpDir + "/dynamic.json",
  295. }
  296. // 1. Save BFD instance
  297. err = mgr.SaveBFDInstance(BFDInstance{Enabled: true})
  298. if err != nil {
  299. t.Fatalf("SaveBFDInstance failed: %v", err)
  300. }
  301. bfdInst := mgr.GetBFDInstance()
  302. if !bfdInst.Enabled {
  303. t.Errorf("Expected bfdInst.Enabled to be true")
  304. }
  305. // 2. Check BFD config generation with 0 interfaces - should contain fallback interface "*"
  306. cfg := mgr.generateBirdConfig()
  307. if !strings.Contains(cfg, `protocol bfd bfd_main`) {
  308. t.Errorf("Expected protocol bfd bfd_main in config, got:\n%s", cfg)
  309. }
  310. if !strings.Contains(cfg, `interface "*"`) {
  311. t.Errorf("Expected fallback interface \"*\" in BFD config, got:\n%s", cfg)
  312. }
  313. // 3. Save BFD multihop neighbor with local address
  314. _, err = mgr.SaveBFDNeighbor(BFDNeighbor{
  315. Address: "10.0.0.1",
  316. LocalAddress: "10.0.0.2",
  317. Multihop: true,
  318. })
  319. if err != nil {
  320. t.Fatalf("SaveBFDNeighbor failed: %v", err)
  321. }
  322. // 4. Save RIP instance without explicit enabled flag
  323. ripInst, err := mgr.SaveRIPInstance(RIPInstance{
  324. Name: "rip1",
  325. Version: 2,
  326. })
  327. if err != nil {
  328. t.Fatalf("SaveRIPInstance failed: %v", err)
  329. }
  330. if !ripInst.Enabled {
  331. t.Errorf("Expected ripInst.Enabled to be true, got false")
  332. }
  333. // 5. Save RAdv instance without explicit enabled flag
  334. radvInst, err := mgr.SaveRAdvInstance(RAdvInstance{
  335. Name: "radv1",
  336. })
  337. if err != nil {
  338. t.Fatalf("SaveRAdvInstance failed: %v", err)
  339. }
  340. if !radvInst.Enabled {
  341. t.Errorf("Expected radvInst.Enabled to be true, got false")
  342. }
  343. // 6. Verify config with BIRD compiler
  344. cfgAll := mgr.generateBirdConfig()
  345. if !strings.Contains(cfgAll, "neighbor 10.0.0.1 local 10.0.0.2 multihop;") {
  346. t.Errorf("Expected multihop neighbor with local address, got:\n%s", cfgAll)
  347. }
  348. if !strings.Contains(cfgAll, `protocol rip rip_rip1_0`) {
  349. t.Errorf("Expected protocol rip in config, got:\n%s", cfgAll)
  350. }
  351. if !strings.Contains(cfgAll, `protocol radv radv_radv1_0`) {
  352. t.Errorf("Expected protocol radv in config, got:\n%s", cfgAll)
  353. }
  354. // 7. Verify OSPF fallback interface when no templates exist
  355. mgr.config.OSPFInterfaces = nil
  356. cfgOspfFallback := mgr.generateBirdConfig()
  357. if !strings.Contains(cfgOspfFallback, `interface "*"`) {
  358. t.Errorf("Expected fallback wildcard interface for OSPF area, got:\n%s", cfgOspfFallback)
  359. }
  360. if _, err := exec.LookPath("bird"); err == nil {
  361. tmpFile, err := os.CreateTemp("", "bird_all_val_*.conf")
  362. if err != nil {
  363. t.Fatalf("Failed to create temp file: %v", err)
  364. }
  365. defer os.Remove(tmpFile.Name())
  366. if _, err := tmpFile.WriteString(cfgOspfFallback); err != nil {
  367. t.Fatalf("Failed to write temp file: %v", err)
  368. }
  369. tmpFile.Close()
  370. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  371. out, err := cmd.CombinedOutput()
  372. if err != nil {
  373. t.Fatalf("bird -p validation failed on all protocols config: %s\nConfig:\n%s", string(out), cfgOspfFallback)
  374. }
  375. }
  376. }
  377. func TestRIPAndOSPFInstanceJSONUnmarshal(t *testing.T) {
  378. testCasesRIP := []struct {
  379. jsonStr string
  380. expectedVersion int
  381. }{
  382. {`{"name":"rip1","version":2}`, 2},
  383. {`{"name":"rip1","version":"2"}`, 2},
  384. {`{"name":"rip1","version":"rip2"}`, 2},
  385. {`{"name":"rip1","version":"ripv2"}`, 2},
  386. {`{"name":"ripng1","version":1}`, 1},
  387. {`{"name":"ripng1","version":"1"}`, 1},
  388. {`{"name":"ripng1","version":"ripng"}`, 1},
  389. {`{"name":"ripng1","version":"ng"}`, 1},
  390. {`{"name":"ripng1","version":"ipv6"}`, 1},
  391. {`{"name":"rip_default"}`, 2},
  392. }
  393. for idx, tc := range testCasesRIP {
  394. var inst RIPInstance
  395. if err := json.Unmarshal([]byte(tc.jsonStr), &inst); err != nil {
  396. t.Fatalf("Case %d failed to unmarshal: %v", idx, err)
  397. }
  398. if inst.Version != tc.expectedVersion {
  399. t.Errorf("Case %d: expected version %d, got %d", idx, tc.expectedVersion, inst.Version)
  400. }
  401. }
  402. testCasesOSPF := []struct {
  403. jsonStr string
  404. expectedVersion int
  405. }{
  406. {`{"name":"ospf1","version":2}`, 2},
  407. {`{"name":"ospf1","version":"2"}`, 2},
  408. {`{"name":"ospf1","version":"ospfv2"}`, 2},
  409. {`{"name":"ospf1","version":"v2"}`, 2},
  410. {`{"name":"ospf3","version":3}`, 3},
  411. {`{"name":"ospf3","version":"3"}`, 3},
  412. {`{"name":"ospf3","version":"ospfv3"}`, 3},
  413. {`{"name":"ospf3","version":"v3"}`, 3},
  414. {`{"name":"ospf3","version":"ipv6"}`, 3},
  415. {`{"name":"ospf_default"}`, 2},
  416. }
  417. for idx, tc := range testCasesOSPF {
  418. var inst OSPFInstance
  419. if err := json.Unmarshal([]byte(tc.jsonStr), &inst); err != nil {
  420. t.Fatalf("OSPF Case %d failed to unmarshal: %v", idx, err)
  421. }
  422. if inst.Version != tc.expectedVersion {
  423. t.Errorf("OSPF Case %d: expected version %d, got %d", idx, tc.expectedVersion, inst.Version)
  424. }
  425. }
  426. }
  427. func TestDynamicRoutingFiltersAllProtocols(t *testing.T) {
  428. mgr := &Manager{
  429. config: DynamicConfig{
  430. RoutingFilters: []RoutingFilterRule{
  431. {
  432. ID: "rule-in-1",
  433. Chain: "filter-in",
  434. Rule: "dst in 10.0.0.0/8",
  435. Action: "accept",
  436. Disabled: false,
  437. },
  438. {
  439. ID: "rule-out-1",
  440. Chain: "filter-out",
  441. Rule: "dst in 192.168.0.0/16",
  442. Action: "accept",
  443. Disabled: false,
  444. },
  445. },
  446. BGPInstance: BGPInstance{
  447. Enabled: true,
  448. AS: 65001,
  449. RouterID: "10.42.62.254",
  450. InFilter: "filter-in",
  451. OutFilter: "filter-out",
  452. },
  453. BGPPeers: []BGPPeer{
  454. {
  455. ID: "peer-1",
  456. Name: "DefaultPeer",
  457. RemoteAddress: "10.42.36.253",
  458. RemoteAS: 65002,
  459. },
  460. },
  461. OSPFInstances: []OSPFInstance{
  462. {
  463. ID: "ospf-1",
  464. Name: "Default",
  465. Version: 2,
  466. RouterID: "10.42.62.254",
  467. Enabled: true,
  468. InFilter: "filter-in",
  469. OutFilter: "filter-out",
  470. RedistributeStatic: true,
  471. },
  472. },
  473. OSPFAreas: []OSPFArea{
  474. {
  475. ID: "area-0",
  476. Name: "backbone",
  477. AreaID: "0.0.0.0",
  478. AreaType: "default",
  479. },
  480. },
  481. OSPFInterfaces: []OSPFInterface{
  482. {
  483. ID: "ospf-if-1",
  484. Interfaces: "eth0",
  485. Area: "backbone",
  486. },
  487. },
  488. RIPInstances: []RIPInstance{
  489. {
  490. ID: "rip-1",
  491. Name: "default",
  492. Version: 2,
  493. Enabled: true,
  494. InFilter: "filter-in",
  495. OutFilter: "filter-out",
  496. RedistributeConnected: true,
  497. },
  498. },
  499. RIPInterfaces: []RIPInterface{
  500. {
  501. ID: "rip-if-1",
  502. Instance: "default",
  503. Interface: "eth0",
  504. },
  505. },
  506. BabelInstances: []BabelInstance{
  507. {
  508. ID: "babel-1",
  509. Name: "default",
  510. RouterID: "10.42.62.254",
  511. Enabled: true,
  512. InFilter: "filter-in",
  513. OutFilter: "filter-out",
  514. RedistributeConnected: true,
  515. },
  516. },
  517. BabelInterfaces: []BabelInterface{
  518. {
  519. ID: "babel-if-1",
  520. Instance: "default",
  521. Interface: "eth0",
  522. },
  523. },
  524. EIGRPInstances: []EIGRPInstance{
  525. {
  526. ID: "eigrp-1",
  527. Name: "default",
  528. AS: 100,
  529. RouterID: "10.42.62.254",
  530. Enabled: true,
  531. InFilter: "filter-in",
  532. OutFilter: "filter-out",
  533. RedistributeConnected: true,
  534. },
  535. },
  536. },
  537. }
  538. // 1. Test BIRD Config Generation
  539. birdCfg := mgr.generateBirdConfig()
  540. // Verify BGP uses global fallback filter
  541. if !strings.Contains(birdCfg, "import filter filter_filter_in;") {
  542. t.Errorf("BGP config missing import filter filter_filter_in;\nConfig:\n%s", birdCfg)
  543. }
  544. if !strings.Contains(birdCfg, "fn_filter_filter_out();") {
  545. t.Errorf("BGP config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg)
  546. }
  547. // Verify OSPF in_filter & out_filter
  548. if !strings.Contains(birdCfg, "protocol ospf v2 ospf_Default") || !strings.Contains(birdCfg, "import filter filter_filter_in;") {
  549. t.Errorf("OSPF config missing import filter filter_filter_in;\nConfig:\n%s", birdCfg)
  550. }
  551. // Verify RIP in_filter & out_filter
  552. if !strings.Contains(birdCfg, "protocol rip rip_default") || !strings.Contains(birdCfg, "fn_filter_filter_out();") {
  553. t.Errorf("RIP config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg)
  554. }
  555. // Verify Babel in_filter & out_filter
  556. if !strings.Contains(birdCfg, "protocol babel babel_default") || !strings.Contains(birdCfg, "fn_filter_filter_out();") {
  557. t.Errorf("Babel config missing fn_filter_filter_out();\nConfig:\n%s", birdCfg)
  558. }
  559. // Verify BIRD syntax with bird -p if installed
  560. if _, err := exec.LookPath("bird"); err == nil {
  561. tmpFile, err := os.CreateTemp("", "bird_filters_val_*.conf")
  562. if err != nil {
  563. t.Fatalf("Failed to create temp file: %v", err)
  564. }
  565. defer os.Remove(tmpFile.Name())
  566. if _, err := tmpFile.WriteString(birdCfg); err != nil {
  567. t.Fatalf("Failed to write temp file: %v", err)
  568. }
  569. tmpFile.Close()
  570. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  571. out, err := cmd.CombinedOutput()
  572. if err != nil {
  573. t.Fatalf("bird -p validation failed on all protocol filters config: %s\nConfig:\n%s", string(out), birdCfg)
  574. }
  575. }
  576. // 2. Test FRR EIGRP Config Generation
  577. frrCfg := mgr.generateFRREigrpConfig()
  578. if !strings.Contains(frrCfg, "ip prefix-list pl_in_filter_in seq 10 permit 10.0.0.0/8") {
  579. t.Errorf("FRR config missing in-filter prefix list;\nConfig:\n%s", frrCfg)
  580. }
  581. if !strings.Contains(frrCfg, "ip prefix-list pl_out_filter_out seq 10 permit 192.168.0.0/16") {
  582. t.Errorf("FRR config missing out-filter prefix list;\nConfig:\n%s", frrCfg)
  583. }
  584. if !strings.Contains(frrCfg, "distribute-list prefix pl_in_filter_in in") {
  585. t.Errorf("FRR config missing distribute-list prefix in;\nConfig:\n%s", frrCfg)
  586. }
  587. if !strings.Contains(frrCfg, "distribute-list prefix pl_out_filter_out out") {
  588. t.Errorf("FRR config missing distribute-list prefix out;\nConfig:\n%s", frrCfg)
  589. }
  590. }
  591. func TestMoveRoutingFilter(t *testing.T) {
  592. mgr := &Manager{
  593. config: DynamicConfig{
  594. RoutingFilters: []RoutingFilterRule{
  595. {ID: "r1", Chain: "chain-A", RuleOrder: 10, Rule: "dst in 10.1.0.0/16"},
  596. {ID: "r2", Chain: "chain-A", RuleOrder: 20, Rule: "dst in 10.2.0.0/16"},
  597. {ID: "r3", Chain: "chain-A", RuleOrder: 30, Rule: "dst in 10.3.0.0/16"},
  598. {ID: "r-other", Chain: "chain-B", RuleOrder: 10, Rule: "dst in 172.16.0.0/12"},
  599. },
  600. },
  601. }
  602. // 1. Move r1 "up" (already at top) -> should succeed with no order change
  603. if err := mgr.MoveRoutingFilter("r1", "up"); err != nil {
  604. t.Fatalf("Move r1 up failed: %v", err)
  605. }
  606. filters := mgr.ListRoutingFilters()
  607. if filters[0].ID != "r1" || filters[0].RuleOrder != 10 {
  608. t.Errorf("Expected r1 still at order 10, got %d", filters[0].RuleOrder)
  609. }
  610. // 2. Move r1 "down" -> r2 should become order 10, r1 should become order 20
  611. if err := mgr.MoveRoutingFilter("r1", "down"); err != nil {
  612. t.Fatalf("Move r1 down failed: %v", err)
  613. }
  614. filters = mgr.ListRoutingFilters()
  615. var r1Order, r2Order int
  616. for _, f := range filters {
  617. if f.ID == "r1" {
  618. r1Order = f.RuleOrder
  619. }
  620. if f.ID == "r2" {
  621. r2Order = f.RuleOrder
  622. }
  623. }
  624. if r2Order != 10 || r1Order != 20 {
  625. t.Errorf("Expected r2 order 10 and r1 order 20, got r2=%d, r1=%d", r2Order, r1Order)
  626. }
  627. // 3. Chain B should be unaffected
  628. for _, f := range filters {
  629. if f.ID == "r-other" && f.RuleOrder != 10 {
  630. t.Errorf("Chain B rule modified unexpectedly: %d", f.RuleOrder)
  631. }
  632. }
  633. // 4. Move r3 (now at index 2 in chain-A) "up" -> should swap with r1 (at order 20)
  634. if err := mgr.MoveRoutingFilter("r3", "up"); err != nil {
  635. t.Fatalf("Move r3 up failed: %v", err)
  636. }
  637. filters = mgr.ListRoutingFilters()
  638. var r3Order int
  639. for _, f := range filters {
  640. if f.ID == "r1" {
  641. r1Order = f.RuleOrder
  642. }
  643. if f.ID == "r3" {
  644. r3Order = f.RuleOrder
  645. }
  646. }
  647. if r3Order != 20 || r1Order != 30 {
  648. t.Errorf("Expected r3 order 20 and r1 order 30, got r3=%d, r1=%d", r3Order, r1Order)
  649. }
  650. }