manager_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. package routing
  2. import (
  3. "encoding/json"
  4. "os"
  5. "path/filepath"
  6. "testing"
  7. "debtik/internal/sdwan"
  8. )
  9. func TestDeduplicationOnLoad(t *testing.T) {
  10. tmpDir := t.TempDir()
  11. storePath := filepath.Join(tmpDir, "static_routes.json")
  12. // Create test JSON with duplicate routes
  13. routes := []RouteItem{
  14. {
  15. ID: "route-1",
  16. DstAddress: "10.0.0.0/8",
  17. Gateway: "10.42.36.254",
  18. Interface: "AWMN-Internal",
  19. RoutingTable: "main",
  20. Distance: 1,
  21. Comment: "Original",
  22. },
  23. {
  24. ID: "route-2",
  25. DstAddress: "10.0.0.0/8",
  26. Gateway: "10.42.36.254",
  27. Interface: "AWMN-Internal",
  28. RoutingTable: "main",
  29. Distance: 1,
  30. Comment: "Duplicate",
  31. },
  32. {
  33. ID: "route-3",
  34. DstAddress: "192.168.1.0/24",
  35. Gateway: "192.168.1.1",
  36. RoutingTable: "main",
  37. Distance: 1,
  38. },
  39. }
  40. data, err := json.Marshal(routes)
  41. if err != nil {
  42. t.Fatalf("failed to marshal routes: %v", err)
  43. }
  44. if err := os.WriteFile(storePath, data, 0600); err != nil {
  45. t.Fatalf("failed to write test file: %v", err)
  46. }
  47. mgr := &RoutingManager{
  48. staticRoutes: make([]RouteItem, 0),
  49. }
  50. // Read and deduplicate using the exact logic from load()
  51. seenIDs := make(map[string]bool)
  52. seenKeys := make(map[string]bool)
  53. var dedupedRev []RouteItem
  54. hasDups := false
  55. var loadedRoutes []RouteItem
  56. _ = json.Unmarshal(data, &loadedRoutes)
  57. for i := len(loadedRoutes) - 1; i >= 0; i-- {
  58. r := loadedRoutes[i]
  59. normDst := normalizeDst(r.DstAddress)
  60. tbl := r.RoutingTable
  61. if tbl == "" {
  62. tbl = "main"
  63. }
  64. routeKey := normDst + "|" + tbl + "|" + r.Gateway + "|" + r.Interface + "|1"
  65. if seenIDs[r.ID] || seenKeys[routeKey] {
  66. hasDups = true
  67. continue
  68. }
  69. seenIDs[r.ID] = true
  70. seenKeys[routeKey] = true
  71. dedupedRev = append(dedupedRev, r)
  72. }
  73. var deduped []RouteItem
  74. for i := len(dedupedRev) - 1; i >= 0; i-- {
  75. deduped = append(deduped, dedupedRev[i])
  76. }
  77. mgr.staticRoutes = deduped
  78. if !hasDups {
  79. t.Errorf("expected duplicates to be detected")
  80. }
  81. if len(mgr.staticRoutes) != 2 {
  82. t.Fatalf("expected 2 routes after dedup, got %d", len(mgr.staticRoutes))
  83. }
  84. // The latest entry for 10.0.0.0/8 should have been preserved
  85. if mgr.staticRoutes[0].ID != "route-2" || mgr.staticRoutes[0].Comment != "Duplicate" {
  86. t.Errorf("expected route-2 (latest) to be kept, got %+v", mgr.staticRoutes[0])
  87. }
  88. }
  89. func TestSaveRouteUpdateNoDuplicates(t *testing.T) {
  90. mgr := &RoutingManager{
  91. staticRoutes: []RouteItem{
  92. {
  93. ID: "route-100",
  94. DstAddress: "10.0.0.0/8",
  95. Gateway: "10.42.36.254",
  96. Interface: "AWMN-Internal",
  97. RoutingTable: "main",
  98. Distance: 1,
  99. Comment: "Original AWMN Route",
  100. },
  101. },
  102. }
  103. // 1. Edit using matching ID
  104. editReq1 := RouteItem{
  105. ID: "route-100",
  106. DstAddress: "10.0.0.0/8",
  107. Gateway: "10.42.36.253",
  108. Interface: "AWMN-Internal",
  109. RoutingTable: "main",
  110. Distance: 1,
  111. Comment: "Updated Gateway",
  112. }
  113. // Test findIndex logic
  114. foundIndex := -1
  115. for i, existing := range mgr.staticRoutes {
  116. if existing.ID == editReq1.ID {
  117. foundIndex = i
  118. break
  119. }
  120. }
  121. if foundIndex != 0 {
  122. t.Fatalf("expected foundIndex 0, got %d", foundIndex)
  123. }
  124. mgr.staticRoutes[foundIndex] = editReq1
  125. if len(mgr.staticRoutes) != 1 {
  126. t.Fatalf("expected 1 route, got %d (duplicate created!)", len(mgr.staticRoutes))
  127. }
  128. if mgr.staticRoutes[0].Gateway != "10.42.36.253" {
  129. t.Errorf("expected gateway 10.42.36.253, got %s", mgr.staticRoutes[0].Gateway)
  130. }
  131. // 2. Edit where UI sent kr-... ID (kernel route adoption/edit) but with OldDstAddress and OldGateway
  132. editReq2 := RouteItem{
  133. ID: "kr-254-10.0.0.0_8-10.42.36.253",
  134. OldDstAddress: "10.0.0.0/8",
  135. OldGateway: "10.42.36.253",
  136. OldRoutingTable: "main",
  137. OldInterface: "AWMN-Internal",
  138. DstAddress: "10.0.0.0/8",
  139. Gateway: "10.42.36.252",
  140. Interface: "AWMN-Internal",
  141. RoutingTable: "main",
  142. Distance: 1,
  143. CheckGateway: "internet",
  144. CheckTarget: "10.22.11.4",
  145. }
  146. foundIndex = -1
  147. // Match by OldDstAddress / OldGateway
  148. normOldDst := normalizeDst(editReq2.OldDstAddress)
  149. for i, existing := range mgr.staticRoutes {
  150. if normalizeDst(existing.DstAddress) == normOldDst && existing.RoutingTable == editReq2.OldRoutingTable {
  151. if existing.Gateway == editReq2.OldGateway || existing.Interface == editReq2.OldGateway {
  152. foundIndex = i
  153. break
  154. }
  155. }
  156. }
  157. if foundIndex != 0 {
  158. t.Fatalf("expected foundIndex 0 from OldDst/OldGw match, got %d", foundIndex)
  159. }
  160. // Preserve ID
  161. editReq2.ID = mgr.staticRoutes[foundIndex].ID
  162. mgr.staticRoutes[foundIndex] = editReq2
  163. if len(mgr.staticRoutes) != 1 {
  164. t.Fatalf("expected 1 route, got %d (duplicate created!)", len(mgr.staticRoutes))
  165. }
  166. if mgr.staticRoutes[0].ID != "route-100" {
  167. t.Errorf("expected ID route-100 preserved, got %s", mgr.staticRoutes[0].ID)
  168. }
  169. if mgr.staticRoutes[0].Gateway != "10.42.36.252" {
  170. t.Errorf("expected gateway 10.42.36.252, got %s", mgr.staticRoutes[0].Gateway)
  171. }
  172. if mgr.staticRoutes[0].CheckTarget != "10.22.11.4" {
  173. t.Errorf("expected check_target 10.22.11.4, got %s", mgr.staticRoutes[0].CheckTarget)
  174. }
  175. }
  176. func TestDeleteRouteMatchingKrID(t *testing.T) {
  177. mgr := &RoutingManager{
  178. staticRoutes: []RouteItem{
  179. {
  180. ID: "route-100",
  181. DstAddress: "10.0.0.0/8",
  182. Gateway: "10.42.36.254",
  183. Interface: "AWMN-Internal",
  184. RoutingTable: "main",
  185. Distance: 1,
  186. },
  187. {
  188. ID: "route-200",
  189. DstAddress: "0.0.0.0/0",
  190. Gateway: "sdwan0",
  191. Interface: "sdwan0",
  192. RoutingTable: "main",
  193. Distance: 1,
  194. },
  195. },
  196. }
  197. // Simulating a live route from kernel corresponding to route-100
  198. liveRoute := RouteItem{
  199. ID: "kr-254-10.0.0.0_8-10.42.36.254",
  200. DstAddress: "10.0.0.0/8",
  201. Gateway: "10.42.36.254",
  202. Interface: "AWMN-Internal",
  203. RoutingTable: "main",
  204. Distance: 1,
  205. }
  206. // If user clicks delete on kr-254-10.0.0.0_8-10.42.36.254
  207. idToDelete := liveRoute.ID
  208. targetIdx := -1
  209. for i, r := range mgr.staticRoutes {
  210. if r.ID == idToDelete {
  211. targetIdx = i
  212. break
  213. }
  214. }
  215. if targetIdx == -1 {
  216. // Fallback to matching live route against static routes
  217. normLrDst := normalizeDst(liveRoute.DstAddress)
  218. for i, sr := range mgr.staticRoutes {
  219. gwMatches := sr.Gateway == "" || liveRoute.Gateway == sr.Gateway || liveRoute.Interface == sr.Gateway
  220. if normalizeDst(sr.DstAddress) == normLrDst && sr.RoutingTable == liveRoute.RoutingTable && gwMatches {
  221. targetIdx = i
  222. break
  223. }
  224. }
  225. }
  226. if targetIdx != 0 {
  227. t.Fatalf("expected targetIdx 0, got %d", targetIdx)
  228. }
  229. // Remove target
  230. mgr.staticRoutes = append(mgr.staticRoutes[:targetIdx], mgr.staticRoutes[targetIdx+1:]...)
  231. if len(mgr.staticRoutes) != 1 {
  232. t.Fatalf("expected 1 route remaining, got %d", len(mgr.staticRoutes))
  233. }
  234. if mgr.staticRoutes[0].ID != "route-200" {
  235. t.Errorf("expected route-200 to remain, got %s", mgr.staticRoutes[0].ID)
  236. }
  237. }
  238. func TestNormalizeDst(t *testing.T) {
  239. if normalizeDst("default") != "default" {
  240. t.Errorf("expected default, got %s", normalizeDst("default"))
  241. }
  242. if normalizeDst("0.0.0.0/0") != "default" {
  243. t.Errorf("expected default, got %s", normalizeDst("0.0.0.0/0"))
  244. }
  245. if normalizeDst("") != "default" {
  246. t.Errorf("expected default, got %s", normalizeDst(""))
  247. }
  248. if normalizeDst("10.0.0.0/8") != "10.0.0.0/8" {
  249. t.Errorf("expected 10.0.0.0/8, got %s", normalizeDst("10.0.0.0/8"))
  250. }
  251. }
  252. func TestSaveAndDeleteRouteLifecycle(t *testing.T) {
  253. tmpDir := t.TempDir()
  254. origStore := StaticRoutesStorePath
  255. defer func() { StaticRoutesStorePath = origStore }()
  256. StaticRoutesStorePath = filepath.Join(tmpDir, "static_routes.json")
  257. mgr := &RoutingManager{
  258. staticRoutes: make([]RouteItem, 0),
  259. }
  260. // 1. Save new route with empty ID
  261. r1 := RouteItem{
  262. DstAddress: "198.51.100.0/24",
  263. Gateway: "blackhole",
  264. Action: "blackhole",
  265. RoutingTable: "main",
  266. Distance: 10,
  267. Comment: "Initial route",
  268. }
  269. if err := mgr.SaveRoute(&r1); err != nil {
  270. t.Fatalf("expected nil error on SaveRoute, got %v", err)
  271. }
  272. if r1.ID == "" {
  273. t.Fatalf("expected SaveRoute to populate r1.ID, got empty")
  274. }
  275. if len(mgr.staticRoutes) != 1 {
  276. t.Fatalf("expected 1 static route in manager, got %d", len(mgr.staticRoutes))
  277. }
  278. savedID := r1.ID
  279. // 2. Edit existing route with populated ID
  280. r1Edit := RouteItem{
  281. ID: savedID,
  282. DstAddress: "198.51.100.0/24",
  283. Gateway: "blackhole",
  284. Action: "blackhole",
  285. RoutingTable: "main",
  286. Distance: 20,
  287. Comment: "Updated route",
  288. }
  289. if err := mgr.SaveRoute(&r1Edit); err != nil {
  290. t.Fatalf("expected nil error on editing SaveRoute, got %v", err)
  291. }
  292. if len(mgr.staticRoutes) != 1 {
  293. t.Fatalf("expected still 1 static route after edit, got %d", len(mgr.staticRoutes))
  294. }
  295. if mgr.staticRoutes[0].ID != savedID {
  296. t.Errorf("expected ID %s preserved, got %s", savedID, mgr.staticRoutes[0].ID)
  297. }
  298. if mgr.staticRoutes[0].Distance != 20 {
  299. t.Errorf("expected distance 20, got %d", mgr.staticRoutes[0].Distance)
  300. }
  301. if mgr.staticRoutes[0].Comment != "Updated route" {
  302. t.Errorf("expected comment 'Updated route', got %s", mgr.staticRoutes[0].Comment)
  303. }
  304. // 3. Delete route
  305. delErr := mgr.DeleteRoute(savedID)
  306. if delErr != nil {
  307. t.Fatalf("expected nil error on DeleteRoute, got %v", delErr)
  308. }
  309. if len(mgr.staticRoutes) != 0 {
  310. t.Fatalf("expected 0 static routes after delete, got %d", len(mgr.staticRoutes))
  311. }
  312. }
  313. func TestMultipathMixedNexthopFlags(t *testing.T) {
  314. tmpDir := t.TempDir()
  315. origStore := StaticRoutesStorePath
  316. defer func() { StaticRoutesStorePath = origStore }()
  317. StaticRoutesStorePath = filepath.Join(tmpDir, "static_routes.json")
  318. mgr := &RoutingManager{
  319. staticRoutes: make([]RouteItem, 0),
  320. }
  321. // Route 1: via IP with device (hasVia = true)
  322. r1 := RouteItem{
  323. DstAddress: "198.51.100.0/24",
  324. Gateway: "100.68.0.1",
  325. Interface: "Fibergrid",
  326. RoutingTable: "main",
  327. Distance: 10,
  328. }
  329. if err := mgr.SaveRoute(&r1); err != nil {
  330. t.Fatalf("failed to save route 1: %v", err)
  331. }
  332. // Route 2: device only without gateway IP (hasVia = false, e.g. sdwan0-Internet)
  333. r2 := RouteItem{
  334. DstAddress: "198.51.100.0/24",
  335. Gateway: "sdwan0-Internet",
  336. Interface: "sdwan0-Internet",
  337. RoutingTable: "main",
  338. Distance: 10,
  339. }
  340. // This previously threw "Error: Invalid flags for nexthop - PERVASIVE and ONLINK can not be set."
  341. if err := mgr.SaveRoute(&r2); err != nil {
  342. t.Fatalf("failed to save multipath route 2: %v", err)
  343. }
  344. if len(mgr.staticRoutes) != 2 {
  345. t.Fatalf("expected 2 routes in multipath group, got %d", len(mgr.staticRoutes))
  346. }
  347. // Cleanup
  348. _ = mgr.DeleteRoute(r1.ID)
  349. _ = mgr.DeleteRoute(r2.ID)
  350. }
  351. func TestSDWANRouteGroupResolution(t *testing.T) {
  352. tmpDir := t.TempDir()
  353. sdwanPath := filepath.Join(tmpDir, "sdwan.json")
  354. sdwanMgr := sdwan.NewManager(sdwanPath)
  355. defer sdwanMgr.Stop()
  356. // 1. Create failover group
  357. group := &sdwan.SDWANGroup{
  358. ID: "sdwan-grp-test",
  359. Name: "sdwan-test",
  360. Mode: sdwan.ModeFailover,
  361. Members: []*sdwan.SDWANMember{
  362. {
  363. ID: "mem-1",
  364. Interface: "Fibergrid",
  365. Gateway: "100.68.0.1",
  366. Priority: 1,
  367. Status: sdwan.StatusHealthy,
  368. },
  369. {
  370. ID: "mem-2",
  371. Interface: "wgx-Space",
  372. Gateway: "",
  373. Priority: 2,
  374. Status: sdwan.StatusHealthy,
  375. },
  376. },
  377. }
  378. if err := sdwanMgr.Save(group); err != nil {
  379. t.Fatalf("failed to save sdwan test group: %v", err)
  380. }
  381. // 2. Test resolution when primary is healthy
  382. nhList, ok := sdwan.ResolveGroupForRouting("sdwan-test")
  383. if !ok || len(nhList) != 1 {
  384. t.Fatalf("expected 1 resolved nexthop, got %d (ok=%v)", len(nhList), ok)
  385. }
  386. if nhList[0].Interface != "Fibergrid" || nhList[0].Gateway != "100.68.0.1" {
  387. t.Fatalf("expected Fibergrid via 100.68.0.1, got %s via %s", nhList[0].Interface, nhList[0].Gateway)
  388. }
  389. // 3. Mark primary as DOWN and verify automatic failover to wgx-Space
  390. group.Members[0].Status = sdwan.StatusDown
  391. if err := sdwanMgr.Save(group); err != nil {
  392. t.Fatalf("failed to update sdwan group: %v", err)
  393. }
  394. nhListAfter, ok := sdwan.ResolveGroupForRouting("sdwan-test")
  395. if !ok || len(nhListAfter) != 1 {
  396. t.Fatalf("expected 1 resolved nexthop after failover, got %d", len(nhListAfter))
  397. }
  398. if nhListAfter[0].Interface != "wgx-Space" {
  399. t.Fatalf("expected failover to wgx-Space, got %s", nhListAfter[0].Interface)
  400. }
  401. // 4. Test RoutingManager saving a static route mapped to SD-WAN group
  402. routesPath := filepath.Join(tmpDir, "static_routes.json")
  403. rulesPath := filepath.Join(tmpDir, "policy_rules.json")
  404. customPath := filepath.Join(tmpDir, "custom_tables.json")
  405. StaticRoutesStorePath = routesPath
  406. PolicyRulesStorePath = rulesPath
  407. CustomTablesStorePath = customPath
  408. mgr := NewRoutingManager()
  409. defer mgr.Stop()
  410. sdwanRoute := RouteItem{
  411. DstAddress: "198.51.200.0/24",
  412. Interface: "sdwan-test",
  413. RoutingTable: "main",
  414. Distance: 5,
  415. }
  416. if err := mgr.SaveRoute(&sdwanRoute); err != nil {
  417. t.Fatalf("failed to save static route pointing to SD-WAN group: %v", err)
  418. }
  419. // Verify route is present and active
  420. routes := mgr.ListRoutes()
  421. found := false
  422. for _, r := range routes {
  423. if r.DstAddress == "198.51.200.0/24" {
  424. found = true
  425. if r.Interface != "sdwan-test" {
  426. t.Errorf("expected route interface sdwan-test, got %s", r.Interface)
  427. }
  428. break
  429. }
  430. }
  431. if !found {
  432. t.Fatalf("SD-WAN static route not found in ListRoutes")
  433. }
  434. _ = mgr.DeleteRoute(sdwanRoute.ID)
  435. }