sdwan_test.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package sdwan
  2. import (
  3. "net"
  4. "os"
  5. "path/filepath"
  6. "testing"
  7. "time"
  8. "github.com/vishvananda/netlink"
  9. )
  10. func TestSDWANManagerBasics(t *testing.T) {
  11. tmpDir := t.TempDir()
  12. storePath := filepath.Join(tmpDir, "sdwan.json")
  13. mgr := NewManager(storePath)
  14. defer mgr.Stop()
  15. if mgr == nil {
  16. t.Fatal("expected non-nil manager")
  17. }
  18. // 1. Create SD-WAN group
  19. group := &SDWANGroup{
  20. Name: "sdwan0",
  21. Mode: ModeWeighted,
  22. ProbeInterval: 3,
  23. AutoNAT: true,
  24. Comment: "Primary Internet SD-WAN",
  25. }
  26. if err := mgr.Save(group); err != nil {
  27. t.Fatalf("failed to save group: %v", err)
  28. }
  29. // Verify group is retrievable by Name and ID
  30. got, ok := mgr.Get("sdwan0")
  31. if !ok || got == nil {
  32. t.Fatal("expected to find sdwan0 by name")
  33. }
  34. if got.ID == "" {
  35. t.Fatal("expected auto-generated group ID")
  36. }
  37. // 2. Add members
  38. mem1 := &SDWANMember{
  39. Interface: "eth0",
  40. Gateway: "192.168.1.1",
  41. Weight: 70,
  42. TargetProbe: "1.1.1.1",
  43. }
  44. if err := mgr.AddMember("sdwan0", mem1); err != nil {
  45. t.Fatalf("failed adding member 1: %v", err)
  46. }
  47. mem2 := &SDWANMember{
  48. Interface: "eth1",
  49. Gateway: "192.168.2.1",
  50. Weight: 30,
  51. TargetProbe: "8.8.8.8",
  52. }
  53. if err := mgr.AddMember("sdwan0", mem2); err != nil {
  54. t.Fatalf("failed adding member 2: %v", err)
  55. }
  56. // Test duplicate member rejection
  57. if err := mgr.AddMember("sdwan0", &SDWANMember{Interface: "eth0"}); err == nil {
  58. t.Fatal("expected error on duplicate member interface")
  59. }
  60. // 3. Test Interface Resolution for Firewall
  61. resolved := mgr.ResolveGroupInterfaces("sdwan0")
  62. if len(resolved) != 2 || resolved[0] != "eth0" || resolved[1] != "eth1" {
  63. t.Fatalf("expected [eth0, eth1], got %v", resolved)
  64. }
  65. allGroups := mgr.ResolveAllSDWANGroups()
  66. if len(allGroups["sdwan0"]) != 2 {
  67. t.Fatalf("expected sdwan0 in ResolveAllSDWANGroups, got %v", allGroups)
  68. }
  69. // 4. Update Member
  70. mem1.Weight = 80
  71. if err := mgr.UpdateMember("sdwan0", mem1); err != nil {
  72. t.Fatalf("failed updating member: %v", err)
  73. }
  74. updated, _ := mgr.Get("sdwan0")
  75. if updated.Members[0].Weight != 80 {
  76. t.Fatalf("expected weight 80, got %d", updated.Members[0].Weight)
  77. }
  78. // 5. Telemetry Summary
  79. summary := mgr.GetSummary()
  80. if summary.TotalGroups != 1 || summary.ActiveGroups != 1 || summary.TotalMembers != 2 {
  81. t.Fatalf("unexpected summary: %+v", summary)
  82. }
  83. // 6. Delete Member
  84. if err := mgr.DeleteMember("sdwan0", "eth1"); err != nil {
  85. t.Fatalf("failed deleting member: %v", err)
  86. }
  87. resolvedAfterDelete := mgr.ResolveGroupInterfaces("sdwan0")
  88. if len(resolvedAfterDelete) != 1 || resolvedAfterDelete[0] != "eth0" {
  89. t.Fatalf("expected [eth0], got %v", resolvedAfterDelete)
  90. }
  91. // 7. Delete Group
  92. if err := mgr.Delete("sdwan0"); err != nil {
  93. t.Fatalf("failed deleting group: %v", err)
  94. }
  95. if len(mgr.List()) != 0 {
  96. t.Fatal("expected 0 groups after delete")
  97. }
  98. }
  99. func TestHealthMetricsCalculation(t *testing.T) {
  100. monitor := NewHealthMonitor(nil)
  101. mem := &SDWANMember{
  102. Interface: "eth0",
  103. MaxLatency: 100,
  104. MaxLoss: 10,
  105. }
  106. samples := []probeSample{
  107. {latency: 20.0, success: true, t: time.Now()},
  108. {latency: 25.0, success: true, t: time.Now()},
  109. {latency: 30.0, success: true, t: time.Now()},
  110. {latency: 0.0, success: false, t: time.Now()},
  111. }
  112. monitor.calculateMetrics(mem, samples)
  113. // 1 failure out of 4 = 25% loss
  114. if mem.PacketLoss != 25.0 {
  115. t.Errorf("expected 25.0%% loss, got %.1f", mem.PacketLoss)
  116. }
  117. // Average of 20, 25, 30 is 25.0 ms
  118. if mem.LiveLatency != 25.0 {
  119. t.Errorf("expected 25.0ms latency, got %.1f", mem.LiveLatency)
  120. }
  121. if mem.Jitter <= 0 {
  122. t.Errorf("expected positive jitter, got %.1f", mem.Jitter)
  123. }
  124. }
  125. func TestHealthHysteresisAndThresholds(t *testing.T) {
  126. mem := &SDWANMember{
  127. Interface: "eth0",
  128. MaxLatency: 250,
  129. MaxLoss: 20,
  130. Status: StatusHealthy,
  131. }
  132. // 1-4 fails: should NOT transition to StatusDown
  133. for i := 1; i <= 4; i++ {
  134. mem.ConsecutiveFails = i
  135. mem.ConsecutiveSuccesses = 0
  136. if mem.ConsecutiveFails >= 5 {
  137. mem.Status = StatusDown
  138. }
  139. if mem.Status == StatusDown {
  140. t.Fatalf("member became DOWN prematurely at %d fails", i)
  141. }
  142. }
  143. // 5th fail: transitions to StatusDown
  144. mem.ConsecutiveFails = 5
  145. if mem.ConsecutiveFails >= 5 {
  146. mem.Status = StatusDown
  147. }
  148. if mem.Status != StatusDown {
  149. t.Fatalf("expected StatusDown after 5 fails, got %s", mem.Status)
  150. }
  151. // 1st recovery success: hysteresis must keep it StatusDown
  152. prevStatus := mem.Status
  153. mem.ConsecutiveSuccesses = 1
  154. mem.ConsecutiveFails = 0
  155. if prevStatus == StatusDown && mem.ConsecutiveSuccesses < 2 {
  156. mem.Status = StatusDown
  157. }
  158. if mem.Status != StatusDown {
  159. t.Fatalf("expected StatusDown during 1st success cycle (hysteresis), got %s", mem.Status)
  160. }
  161. // 2nd recovery success: now allowed to recover
  162. mem.ConsecutiveSuccesses = 2
  163. if prevStatus == StatusDown && mem.ConsecutiveSuccesses < 2 {
  164. mem.Status = StatusDown
  165. } else {
  166. mem.Status = StatusHealthy
  167. }
  168. if mem.Status != StatusHealthy {
  169. t.Fatalf("expected StatusHealthy after 2 consecutive successes, got %s", mem.Status)
  170. }
  171. }
  172. func TestSDWANVirtualDeviceLifecycle(t *testing.T) {
  173. tmpDir := t.TempDir()
  174. storePath := filepath.Join(tmpDir, "sdwan.json")
  175. mgr := NewManager(storePath)
  176. defer mgr.Stop()
  177. group := &SDWANGroup{
  178. Name: "sdwantest0",
  179. Mode: ModeFailover,
  180. AutoNAT: false,
  181. Members: []*SDWANMember{
  182. {Interface: "lo", Gateway: "127.0.0.1", Weight: 100},
  183. },
  184. }
  185. if err := mgr.Save(group); err != nil {
  186. t.Fatalf("failed saving group: %v", err)
  187. }
  188. // Verify virtual device exists in kernel
  189. link, err := netlink.LinkByName("sdwantest0")
  190. if err != nil {
  191. t.Fatalf("expected sdwantest0 virtual device to be created in kernel: %v", err)
  192. }
  193. if link.Attrs().Flags&net.FlagUp == 0 {
  194. t.Fatalf("expected sdwantest0 to be UP")
  195. }
  196. // Delete group and verify virtual device is removed
  197. if err := mgr.Delete("sdwantest0"); err != nil {
  198. t.Fatalf("failed to delete group: %v", err)
  199. }
  200. if _, err := netlink.LinkByName("sdwantest0"); err == nil {
  201. t.Fatalf("expected sdwantest0 virtual device to be removed from kernel after group delete")
  202. }
  203. }
  204. func TestStartupResolverHonorsFailover(t *testing.T) {
  205. path := filepath.Join(t.TempDir(), "groups.json")
  206. content := `[{"id":"g1","name":"wan","mode":"failover","members":[{"interface":"primary","gateway":"192.0.2.1","priority":1,"status":"HEALTHY"},{"interface":"backup","gateway":"192.0.2.2","priority":2,"status":"HEALTHY"}]}]`
  207. if err := os.WriteFile(path, []byte(content), 0600); err != nil {
  208. t.Fatal(err)
  209. }
  210. hops, ok := resolveGroupFromFile(path, "wan")
  211. if !ok || len(hops) != 1 || hops[0].Interface != "primary" || hops[0].Gateway != "192.0.2.1" {
  212. t.Fatalf("startup failover resolution: %+v %v", hops, ok)
  213. }
  214. }