protocols_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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 TestBFDAndRIPAndRAdvManagement(t *testing.T) {
  234. tmpDir, err := os.MkdirTemp("", "debtik_protocols_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. }
  242. // 1. Save BFD instance
  243. err = mgr.SaveBFDInstance(BFDInstance{Enabled: true})
  244. if err != nil {
  245. t.Fatalf("SaveBFDInstance failed: %v", err)
  246. }
  247. bfdInst := mgr.GetBFDInstance()
  248. if !bfdInst.Enabled {
  249. t.Errorf("Expected bfdInst.Enabled to be true")
  250. }
  251. // 2. Check BFD config generation with 0 interfaces - should contain fallback interface "*"
  252. cfg := mgr.generateBirdConfig()
  253. if !strings.Contains(cfg, `protocol bfd bfd_main`) {
  254. t.Errorf("Expected protocol bfd bfd_main in config, got:\n%s", cfg)
  255. }
  256. if !strings.Contains(cfg, `interface "*"`) {
  257. t.Errorf("Expected fallback interface \"*\" in BFD config, got:\n%s", cfg)
  258. }
  259. // 3. Save BFD multihop neighbor with local address
  260. _, err = mgr.SaveBFDNeighbor(BFDNeighbor{
  261. Address: "10.0.0.1",
  262. LocalAddress: "10.0.0.2",
  263. Multihop: true,
  264. })
  265. if err != nil {
  266. t.Fatalf("SaveBFDNeighbor failed: %v", err)
  267. }
  268. // 4. Save RIP instance without explicit enabled flag
  269. ripInst, err := mgr.SaveRIPInstance(RIPInstance{
  270. Name: "rip1",
  271. Version: 2,
  272. })
  273. if err != nil {
  274. t.Fatalf("SaveRIPInstance failed: %v", err)
  275. }
  276. if !ripInst.Enabled {
  277. t.Errorf("Expected ripInst.Enabled to be true, got false")
  278. }
  279. // 5. Save RAdv instance without explicit enabled flag
  280. radvInst, err := mgr.SaveRAdvInstance(RAdvInstance{
  281. Name: "radv1",
  282. })
  283. if err != nil {
  284. t.Fatalf("SaveRAdvInstance failed: %v", err)
  285. }
  286. if !radvInst.Enabled {
  287. t.Errorf("Expected radvInst.Enabled to be true, got false")
  288. }
  289. // 6. Verify config with BIRD compiler
  290. cfgAll := mgr.generateBirdConfig()
  291. if !strings.Contains(cfgAll, "neighbor 10.0.0.1 local 10.0.0.2 multihop;") {
  292. t.Errorf("Expected multihop neighbor with local address, got:\n%s", cfgAll)
  293. }
  294. if !strings.Contains(cfgAll, `protocol rip rip_rip1_0`) {
  295. t.Errorf("Expected protocol rip in config, got:\n%s", cfgAll)
  296. }
  297. if !strings.Contains(cfgAll, `protocol radv radv_radv1_0`) {
  298. t.Errorf("Expected protocol radv in config, got:\n%s", cfgAll)
  299. }
  300. // 7. Verify OSPF fallback interface when no templates exist
  301. mgr.config.OSPFInterfaces = nil
  302. cfgOspfFallback := mgr.generateBirdConfig()
  303. if !strings.Contains(cfgOspfFallback, `interface "*"`) {
  304. t.Errorf("Expected fallback wildcard interface for OSPF area, got:\n%s", cfgOspfFallback)
  305. }
  306. if _, err := exec.LookPath("bird"); err == nil {
  307. tmpFile, err := os.CreateTemp("", "bird_all_val_*.conf")
  308. if err != nil {
  309. t.Fatalf("Failed to create temp file: %v", err)
  310. }
  311. defer os.Remove(tmpFile.Name())
  312. if _, err := tmpFile.WriteString(cfgOspfFallback); err != nil {
  313. t.Fatalf("Failed to write temp file: %v", err)
  314. }
  315. tmpFile.Close()
  316. cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
  317. out, err := cmd.CombinedOutput()
  318. if err != nil {
  319. t.Fatalf("bird -p validation failed on all protocols config: %s\nConfig:\n%s", string(out), cfgOspfFallback)
  320. }
  321. }
  322. }
  323. func TestRIPAndOSPFInstanceJSONUnmarshal(t *testing.T) {
  324. testCasesRIP := []struct {
  325. jsonStr string
  326. expectedVersion int
  327. }{
  328. {`{"name":"rip1","version":2}`, 2},
  329. {`{"name":"rip1","version":"2"}`, 2},
  330. {`{"name":"rip1","version":"rip2"}`, 2},
  331. {`{"name":"rip1","version":"ripv2"}`, 2},
  332. {`{"name":"ripng1","version":1}`, 1},
  333. {`{"name":"ripng1","version":"1"}`, 1},
  334. {`{"name":"ripng1","version":"ripng"}`, 1},
  335. {`{"name":"ripng1","version":"ng"}`, 1},
  336. {`{"name":"ripng1","version":"ipv6"}`, 1},
  337. {`{"name":"rip_default"}`, 2},
  338. }
  339. for idx, tc := range testCasesRIP {
  340. var inst RIPInstance
  341. if err := json.Unmarshal([]byte(tc.jsonStr), &inst); err != nil {
  342. t.Fatalf("Case %d failed to unmarshal: %v", idx, err)
  343. }
  344. if inst.Version != tc.expectedVersion {
  345. t.Errorf("Case %d: expected version %d, got %d", idx, tc.expectedVersion, inst.Version)
  346. }
  347. }
  348. testCasesOSPF := []struct {
  349. jsonStr string
  350. expectedVersion int
  351. }{
  352. {`{"name":"ospf1","version":2}`, 2},
  353. {`{"name":"ospf1","version":"2"}`, 2},
  354. {`{"name":"ospf1","version":"ospfv2"}`, 2},
  355. {`{"name":"ospf1","version":"v2"}`, 2},
  356. {`{"name":"ospf3","version":3}`, 3},
  357. {`{"name":"ospf3","version":"3"}`, 3},
  358. {`{"name":"ospf3","version":"ospfv3"}`, 3},
  359. {`{"name":"ospf3","version":"v3"}`, 3},
  360. {`{"name":"ospf3","version":"ipv6"}`, 3},
  361. {`{"name":"ospf_default"}`, 2},
  362. }
  363. for idx, tc := range testCasesOSPF {
  364. var inst OSPFInstance
  365. if err := json.Unmarshal([]byte(tc.jsonStr), &inst); err != nil {
  366. t.Fatalf("OSPF Case %d failed to unmarshal: %v", idx, err)
  367. }
  368. if inst.Version != tc.expectedVersion {
  369. t.Errorf("OSPF Case %d: expected version %d, got %d", idx, tc.expectedVersion, inst.Version)
  370. }
  371. }
  372. }