protocols_test.go 20 KB

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