| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- package routing
- import (
- "encoding/json"
- "os"
- "path/filepath"
- "testing"
- )
- func TestDeduplicationOnLoad(t *testing.T) {
- tmpDir := t.TempDir()
- storePath := filepath.Join(tmpDir, "static_routes.json")
- // Create test JSON with duplicate routes
- routes := []RouteItem{
- {
- ID: "route-1",
- DstAddress: "10.0.0.0/8",
- Gateway: "10.42.36.254",
- Interface: "AWMN-Internal",
- RoutingTable: "main",
- Distance: 1,
- Comment: "Original",
- },
- {
- ID: "route-2",
- DstAddress: "10.0.0.0/8",
- Gateway: "10.42.36.254",
- Interface: "AWMN-Internal",
- RoutingTable: "main",
- Distance: 1,
- Comment: "Duplicate",
- },
- {
- ID: "route-3",
- DstAddress: "192.168.1.0/24",
- Gateway: "192.168.1.1",
- RoutingTable: "main",
- Distance: 1,
- },
- }
- data, err := json.Marshal(routes)
- if err != nil {
- t.Fatalf("failed to marshal routes: %v", err)
- }
- if err := os.WriteFile(storePath, data, 0600); err != nil {
- t.Fatalf("failed to write test file: %v", err)
- }
- mgr := &RoutingManager{
- staticRoutes: make([]RouteItem, 0),
- }
- // Read and deduplicate using the exact logic from load()
- seenIDs := make(map[string]bool)
- seenKeys := make(map[string]bool)
- var dedupedRev []RouteItem
- hasDups := false
- var loadedRoutes []RouteItem
- _ = json.Unmarshal(data, &loadedRoutes)
- for i := len(loadedRoutes) - 1; i >= 0; i-- {
- r := loadedRoutes[i]
- normDst := normalizeDst(r.DstAddress)
- tbl := r.RoutingTable
- if tbl == "" {
- tbl = "main"
- }
- routeKey := normDst + "|" + tbl + "|" + r.Gateway + "|" + r.Interface + "|1"
- if seenIDs[r.ID] || seenKeys[routeKey] {
- hasDups = true
- continue
- }
- seenIDs[r.ID] = true
- seenKeys[routeKey] = true
- dedupedRev = append(dedupedRev, r)
- }
- var deduped []RouteItem
- for i := len(dedupedRev) - 1; i >= 0; i-- {
- deduped = append(deduped, dedupedRev[i])
- }
- mgr.staticRoutes = deduped
- if !hasDups {
- t.Errorf("expected duplicates to be detected")
- }
- if len(mgr.staticRoutes) != 2 {
- t.Fatalf("expected 2 routes after dedup, got %d", len(mgr.staticRoutes))
- }
- // The latest entry for 10.0.0.0/8 should have been preserved
- if mgr.staticRoutes[0].ID != "route-2" || mgr.staticRoutes[0].Comment != "Duplicate" {
- t.Errorf("expected route-2 (latest) to be kept, got %+v", mgr.staticRoutes[0])
- }
- }
- func TestSaveRouteUpdateNoDuplicates(t *testing.T) {
- mgr := &RoutingManager{
- staticRoutes: []RouteItem{
- {
- ID: "route-100",
- DstAddress: "10.0.0.0/8",
- Gateway: "10.42.36.254",
- Interface: "AWMN-Internal",
- RoutingTable: "main",
- Distance: 1,
- Comment: "Original AWMN Route",
- },
- },
- }
- // 1. Edit using matching ID
- editReq1 := RouteItem{
- ID: "route-100",
- DstAddress: "10.0.0.0/8",
- Gateway: "10.42.36.253",
- Interface: "AWMN-Internal",
- RoutingTable: "main",
- Distance: 1,
- Comment: "Updated Gateway",
- }
- // Test findIndex logic
- foundIndex := -1
- for i, existing := range mgr.staticRoutes {
- if existing.ID == editReq1.ID {
- foundIndex = i
- break
- }
- }
- if foundIndex != 0 {
- t.Fatalf("expected foundIndex 0, got %d", foundIndex)
- }
- mgr.staticRoutes[foundIndex] = editReq1
- if len(mgr.staticRoutes) != 1 {
- t.Fatalf("expected 1 route, got %d (duplicate created!)", len(mgr.staticRoutes))
- }
- if mgr.staticRoutes[0].Gateway != "10.42.36.253" {
- t.Errorf("expected gateway 10.42.36.253, got %s", mgr.staticRoutes[0].Gateway)
- }
- // 2. Edit where UI sent kr-... ID (kernel route adoption/edit) but with OldDstAddress and OldGateway
- editReq2 := RouteItem{
- ID: "kr-254-10.0.0.0_8-10.42.36.253",
- OldDstAddress: "10.0.0.0/8",
- OldGateway: "10.42.36.253",
- OldRoutingTable: "main",
- OldInterface: "AWMN-Internal",
- DstAddress: "10.0.0.0/8",
- Gateway: "10.42.36.252",
- Interface: "AWMN-Internal",
- RoutingTable: "main",
- Distance: 1,
- CheckGateway: "internet",
- CheckTarget: "10.22.11.4",
- }
- foundIndex = -1
- // Match by OldDstAddress / OldGateway
- normOldDst := normalizeDst(editReq2.OldDstAddress)
- for i, existing := range mgr.staticRoutes {
- if normalizeDst(existing.DstAddress) == normOldDst && existing.RoutingTable == editReq2.OldRoutingTable {
- if existing.Gateway == editReq2.OldGateway || existing.Interface == editReq2.OldGateway {
- foundIndex = i
- break
- }
- }
- }
- if foundIndex != 0 {
- t.Fatalf("expected foundIndex 0 from OldDst/OldGw match, got %d", foundIndex)
- }
- // Preserve ID
- editReq2.ID = mgr.staticRoutes[foundIndex].ID
- mgr.staticRoutes[foundIndex] = editReq2
- if len(mgr.staticRoutes) != 1 {
- t.Fatalf("expected 1 route, got %d (duplicate created!)", len(mgr.staticRoutes))
- }
- if mgr.staticRoutes[0].ID != "route-100" {
- t.Errorf("expected ID route-100 preserved, got %s", mgr.staticRoutes[0].ID)
- }
- if mgr.staticRoutes[0].Gateway != "10.42.36.252" {
- t.Errorf("expected gateway 10.42.36.252, got %s", mgr.staticRoutes[0].Gateway)
- }
- if mgr.staticRoutes[0].CheckTarget != "10.22.11.4" {
- t.Errorf("expected check_target 10.22.11.4, got %s", mgr.staticRoutes[0].CheckTarget)
- }
- }
- func TestDeleteRouteMatchingKrID(t *testing.T) {
- mgr := &RoutingManager{
- staticRoutes: []RouteItem{
- {
- ID: "route-100",
- DstAddress: "10.0.0.0/8",
- Gateway: "10.42.36.254",
- Interface: "AWMN-Internal",
- RoutingTable: "main",
- Distance: 1,
- },
- {
- ID: "route-200",
- DstAddress: "0.0.0.0/0",
- Gateway: "sdwan0",
- Interface: "sdwan0",
- RoutingTable: "main",
- Distance: 1,
- },
- },
- }
- // Simulating a live route from kernel corresponding to route-100
- liveRoute := RouteItem{
- ID: "kr-254-10.0.0.0_8-10.42.36.254",
- DstAddress: "10.0.0.0/8",
- Gateway: "10.42.36.254",
- Interface: "AWMN-Internal",
- RoutingTable: "main",
- Distance: 1,
- }
- // If user clicks delete on kr-254-10.0.0.0_8-10.42.36.254
- idToDelete := liveRoute.ID
- targetIdx := -1
- for i, r := range mgr.staticRoutes {
- if r.ID == idToDelete {
- targetIdx = i
- break
- }
- }
- if targetIdx == -1 {
- // Fallback to matching live route against static routes
- normLrDst := normalizeDst(liveRoute.DstAddress)
- for i, sr := range mgr.staticRoutes {
- gwMatches := sr.Gateway == "" || liveRoute.Gateway == sr.Gateway || liveRoute.Interface == sr.Gateway
- if normalizeDst(sr.DstAddress) == normLrDst && sr.RoutingTable == liveRoute.RoutingTable && gwMatches {
- targetIdx = i
- break
- }
- }
- }
- if targetIdx != 0 {
- t.Fatalf("expected targetIdx 0, got %d", targetIdx)
- }
- // Remove target
- mgr.staticRoutes = append(mgr.staticRoutes[:targetIdx], mgr.staticRoutes[targetIdx+1:]...)
- if len(mgr.staticRoutes) != 1 {
- t.Fatalf("expected 1 route remaining, got %d", len(mgr.staticRoutes))
- }
- if mgr.staticRoutes[0].ID != "route-200" {
- t.Errorf("expected route-200 to remain, got %s", mgr.staticRoutes[0].ID)
- }
- }
- func TestNormalizeDst(t *testing.T) {
- if normalizeDst("default") != "default" {
- t.Errorf("expected default, got %s", normalizeDst("default"))
- }
- if normalizeDst("0.0.0.0/0") != "default" {
- t.Errorf("expected default, got %s", normalizeDst("0.0.0.0/0"))
- }
- if normalizeDst("") != "default" {
- t.Errorf("expected default, got %s", normalizeDst(""))
- }
- if normalizeDst("10.0.0.0/8") != "10.0.0.0/8" {
- t.Errorf("expected 10.0.0.0/8, got %s", normalizeDst("10.0.0.0/8"))
- }
- }
- func TestSaveAndDeleteRouteLifecycle(t *testing.T) {
- tmpDir := t.TempDir()
- origStore := StaticRoutesStorePath
- defer func() { StaticRoutesStorePath = origStore }()
- StaticRoutesStorePath = filepath.Join(tmpDir, "static_routes.json")
- mgr := &RoutingManager{
- staticRoutes: make([]RouteItem, 0),
- }
- // 1. Save new route with empty ID
- r1 := RouteItem{
- DstAddress: "198.51.100.0/24",
- Gateway: "blackhole",
- Action: "blackhole",
- RoutingTable: "main",
- Distance: 10,
- Comment: "Initial route",
- }
- if err := mgr.SaveRoute(&r1); err != nil {
- t.Fatalf("expected nil error on SaveRoute, got %v", err)
- }
- if r1.ID == "" {
- t.Fatalf("expected SaveRoute to populate r1.ID, got empty")
- }
- if len(mgr.staticRoutes) != 1 {
- t.Fatalf("expected 1 static route in manager, got %d", len(mgr.staticRoutes))
- }
- savedID := r1.ID
- // 2. Edit existing route with populated ID
- r1Edit := RouteItem{
- ID: savedID,
- DstAddress: "198.51.100.0/24",
- Gateway: "blackhole",
- Action: "blackhole",
- RoutingTable: "main",
- Distance: 20,
- Comment: "Updated route",
- }
- if err := mgr.SaveRoute(&r1Edit); err != nil {
- t.Fatalf("expected nil error on editing SaveRoute, got %v", err)
- }
- if len(mgr.staticRoutes) != 1 {
- t.Fatalf("expected still 1 static route after edit, got %d", len(mgr.staticRoutes))
- }
- if mgr.staticRoutes[0].ID != savedID {
- t.Errorf("expected ID %s preserved, got %s", savedID, mgr.staticRoutes[0].ID)
- }
- if mgr.staticRoutes[0].Distance != 20 {
- t.Errorf("expected distance 20, got %d", mgr.staticRoutes[0].Distance)
- }
- if mgr.staticRoutes[0].Comment != "Updated route" {
- t.Errorf("expected comment 'Updated route', got %s", mgr.staticRoutes[0].Comment)
- }
- // 3. Delete route
- delErr := mgr.DeleteRoute(savedID)
- if delErr != nil {
- t.Fatalf("expected nil error on DeleteRoute, got %v", delErr)
- }
- if len(mgr.staticRoutes) != 0 {
- t.Fatalf("expected 0 static routes after delete, got %d", len(mgr.staticRoutes))
- }
- }
- func TestMultipathMixedNexthopFlags(t *testing.T) {
- tmpDir := t.TempDir()
- origStore := StaticRoutesStorePath
- defer func() { StaticRoutesStorePath = origStore }()
- StaticRoutesStorePath = filepath.Join(tmpDir, "static_routes.json")
- mgr := &RoutingManager{
- staticRoutes: make([]RouteItem, 0),
- }
- // Route 1: via IP with device (hasVia = true)
- r1 := RouteItem{
- DstAddress: "198.51.100.0/24",
- Gateway: "100.68.0.1",
- Interface: "Fibergrid",
- RoutingTable: "main",
- Distance: 10,
- }
- if err := mgr.SaveRoute(&r1); err != nil {
- t.Fatalf("failed to save route 1: %v", err)
- }
- // Route 2: device only without gateway IP (hasVia = false, e.g. sdwan0-Internet)
- r2 := RouteItem{
- DstAddress: "198.51.100.0/24",
- Gateway: "sdwan0-Internet",
- Interface: "sdwan0-Internet",
- RoutingTable: "main",
- Distance: 10,
- }
- // This previously threw "Error: Invalid flags for nexthop - PERVASIVE and ONLINK can not be set."
- if err := mgr.SaveRoute(&r2); err != nil {
- t.Fatalf("failed to save multipath route 2: %v", err)
- }
- if len(mgr.staticRoutes) != 2 {
- t.Fatalf("expected 2 routes in multipath group, got %d", len(mgr.staticRoutes))
- }
- // Cleanup
- _ = mgr.DeleteRoute(r1.ID)
- _ = mgr.DeleteRoute(r2.ID)
- }
|