route_change_test.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package routing
  2. import (
  3. "encoding/json"
  4. "os"
  5. "os/exec"
  6. "path/filepath"
  7. "strings"
  8. "testing"
  9. )
  10. func useTempRouteStores(t *testing.T) {
  11. t.Helper()
  12. oldStatic, oldPolicy, oldTables := StaticRoutesStorePath, PolicyRulesStorePath, CustomTablesStorePath
  13. dir := t.TempDir()
  14. StaticRoutesStorePath = filepath.Join(dir, "static.json")
  15. PolicyRulesStorePath = filepath.Join(dir, "rules.json")
  16. CustomTablesStorePath = filepath.Join(dir, "tables.json")
  17. t.Cleanup(func() {
  18. StaticRoutesStorePath, PolicyRulesStorePath, CustomTablesStorePath = oldStatic, oldPolicy, oldTables
  19. })
  20. }
  21. func TestRouteEnableFailureRestoresStoredAndMemoryState(t *testing.T) {
  22. useTempRouteStores(t)
  23. bin := t.TempDir()
  24. t.Setenv("PATH", bin)
  25. script := "#!/bin/sh\nif [ \"$2\" = del ]; then echo 'RTNETLINK answers: No such process' >&2; exit 2; fi\necho 'permission denied' >&2; exit 2\n"
  26. if err := os.WriteFile(filepath.Join(bin, "ip"), []byte(script), 0755); err != nil {
  27. t.Fatal(err)
  28. }
  29. old := RouteItem{ID: "r1", DstAddress: "198.51.100.0/24", Gateway: "192.0.2.1", Interface: "eth0", RoutingTable: "main", Distance: 10, Disabled: true, Active: false}
  30. m := &RoutingManager{staticRoutes: []RouteItem{old}}
  31. if err := m.save(); err != nil {
  32. t.Fatal(err)
  33. }
  34. err := m.SetRouteState("r1", false)
  35. if err == nil || !strings.Contains(err.Error(), "applying route change") {
  36. t.Fatalf("expected apply error: %v", err)
  37. }
  38. if !m.staticRoutes[0].Disabled || m.staticRoutes[0].Active {
  39. t.Fatalf("memory not restored: %+v", m.staticRoutes[0])
  40. }
  41. data, err := os.ReadFile(StaticRoutesStorePath)
  42. if err != nil {
  43. t.Fatal(err)
  44. }
  45. var persisted []RouteItem
  46. if err = json.Unmarshal(data, &persisted); err != nil {
  47. t.Fatal(err)
  48. }
  49. if len(persisted) != 1 || !persisted[0].Disabled {
  50. t.Fatalf("storage not restored: %+v", persisted)
  51. }
  52. }
  53. func TestRouteDeletionNeverUsesUnqualifiedFallback(t *testing.T) {
  54. useTempRouteStores(t)
  55. bin := t.TempDir()
  56. t.Setenv("PATH", bin)
  57. logPath := filepath.Join(t.TempDir(), "commands")
  58. t.Setenv("DEBTIK_TEST_IP_LOG", logPath)
  59. script := "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$DEBTIK_TEST_IP_LOG\"\necho 'RTNETLINK answers: No such process' >&2\nexit 2\n"
  60. if err := os.WriteFile(filepath.Join(bin, "ip"), []byte(script), 0755); err != nil {
  61. t.Fatal(err)
  62. }
  63. r := RouteItem{ID: "r1", DstAddress: "198.51.100.0/24", Gateway: "192.0.2.1", Interface: "eth0", RoutingTable: "100", Distance: 10, Active: true}
  64. m := &RoutingManager{staticRoutes: []RouteItem{r}}
  65. if err := m.save(); err != nil {
  66. t.Fatal(err)
  67. }
  68. if err := m.DeleteRoute("r1"); err != nil {
  69. t.Fatal(err)
  70. }
  71. data, err := os.ReadFile(logPath)
  72. if err != nil {
  73. t.Fatal(err)
  74. }
  75. lines := strings.Split(strings.TrimSpace(string(data)), "\n")
  76. if len(lines) != 2 {
  77. t.Fatalf("unexpected commands: %s", data)
  78. }
  79. for _, line := range lines {
  80. if !strings.Contains(line, "metric 10 table 100 proto static") {
  81. t.Fatalf("unsafe deletion: %s", line)
  82. }
  83. }
  84. }
  85. func TestIsolatedRouteGroupDeletionKeepsOtherMetric(t *testing.T) {
  86. if os.Getenv("DEBTIK_TEST_ISOLATED_NETNS") != "1" {
  87. t.Skip("requires a dedicated network namespace")
  88. }
  89. ip := func(args ...string) {
  90. t.Helper()
  91. if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
  92. t.Fatalf("ip %v: %s: %v", args, out, err)
  93. }
  94. }
  95. ip("link", "set", "lo", "up")
  96. ip("route", "add", "198.51.100.0/24", "dev", "lo", "metric", "10", "proto", "static")
  97. ip("route", "add", "198.51.100.0/24", "dev", "lo", "metric", "20", "proto", "static")
  98. m := &RoutingManager{}
  99. if err := m.syncRouteGroupToKernel("198.51.100.0/24", "main", 10); err != nil {
  100. t.Fatal(err)
  101. }
  102. out, err := exec.Command("ip", "route", "show", "198.51.100.0/24").CombinedOutput()
  103. if err != nil {
  104. t.Fatal(err)
  105. }
  106. if strings.Contains(string(out), "metric 10") || !strings.Contains(string(out), "metric 20") {
  107. t.Fatalf("unexpected routes: %s", out)
  108. }
  109. }
  110. func TestRouteEditSameGroupDoesNotDeleteReplacement(t *testing.T) {
  111. useTempRouteStores(t)
  112. bin := t.TempDir()
  113. t.Setenv("PATH", bin)
  114. logPath := filepath.Join(t.TempDir(), "commands")
  115. t.Setenv("DEBTIK_TEST_IP_LOG", logPath)
  116. script := "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$DEBTIK_TEST_IP_LOG\"\nexit 0\n"
  117. if err := os.WriteFile(filepath.Join(bin, "ip"), []byte(script), 0755); err != nil {
  118. t.Fatal(err)
  119. }
  120. old := RouteItem{ID: "r1", DstAddress: "198.51.100.0/24", Gateway: "192.0.2.1", Interface: "eth0", RoutingTable: "main", Distance: 10, Active: true}
  121. m := &RoutingManager{staticRoutes: []RouteItem{old}}
  122. if err := m.save(); err != nil {
  123. t.Fatal(err)
  124. }
  125. next := old
  126. next.Gateway = "192.0.2.2"
  127. if err := m.SaveRoute(&next); err != nil {
  128. t.Fatal(err)
  129. }
  130. data, err := os.ReadFile(logPath)
  131. if err != nil {
  132. t.Fatal(err)
  133. }
  134. commands := strings.Split(strings.TrimSpace(string(data)), "\n")
  135. if len(commands) != 1 || !strings.Contains(commands[0], "route replace 198.51.100.0/24 via 192.0.2.2") {
  136. t.Fatalf("replacement was deleted or repeated: %s", data)
  137. }
  138. }
  139. func TestRouteEditCleanupFailureRollsBack(t *testing.T) {
  140. useTempRouteStores(t)
  141. bin := t.TempDir()
  142. t.Setenv("PATH", bin)
  143. logPath := filepath.Join(t.TempDir(), "commands")
  144. t.Setenv("DEBTIK_TEST_IP_LOG", logPath)
  145. script := "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$DEBTIK_TEST_IP_LOG\"\nif [ \"$2\" = del ] && [ \"$3\" = 198.51.100.0/24 ]; then echo 'permission denied' >&2; exit 2; fi\nif [ \"$2\" = del ]; then echo 'RTNETLINK answers: No such process' >&2; exit 2; fi\nexit 0\n"
  146. if err := os.WriteFile(filepath.Join(bin, "ip"), []byte(script), 0755); err != nil {
  147. t.Fatal(err)
  148. }
  149. old := RouteItem{ID: "r1", DstAddress: "198.51.100.0/24", Gateway: "192.0.2.1", Interface: "eth0", RoutingTable: "main", Distance: 10, Active: true}
  150. m := &RoutingManager{staticRoutes: []RouteItem{old}}
  151. if err := m.save(); err != nil {
  152. t.Fatal(err)
  153. }
  154. next := old
  155. next.DstAddress = "203.0.113.0/24"
  156. err := m.SaveRoute(&next)
  157. if err == nil || !strings.Contains(err.Error(), "cleaning previous route group") {
  158. t.Fatalf("cleanup failure ignored: %v", err)
  159. }
  160. if len(m.staticRoutes) != 1 || m.staticRoutes[0].DstAddress != old.DstAddress {
  161. t.Fatalf("memory not restored: %+v", m.staticRoutes)
  162. }
  163. data, err := os.ReadFile(StaticRoutesStorePath)
  164. if err != nil {
  165. t.Fatal(err)
  166. }
  167. var persisted []RouteItem
  168. if err = json.Unmarshal(data, &persisted); err != nil {
  169. t.Fatal(err)
  170. }
  171. if len(persisted) != 1 || persisted[0].DstAddress != old.DstAddress {
  172. t.Fatalf("storage not restored: %+v", persisted)
  173. }
  174. cmds, err := os.ReadFile(logPath)
  175. if err != nil {
  176. t.Fatal(err)
  177. }
  178. if !strings.Contains(string(cmds), "route replace 198.51.100.0/24") || !strings.Contains(string(cmds), "route del 203.0.113.0/24") {
  179. t.Fatalf("rollback omitted: %s", cmds)
  180. }
  181. }
  182. func TestIsolatedRouteEditKeepsReplacementAndRollsBackFailure(t *testing.T) {
  183. if os.Getenv("DEBTIK_TEST_ISOLATED_NETNS") != "1" {
  184. t.Skip("requires a dedicated network namespace")
  185. }
  186. useTempRouteStores(t)
  187. ip := func(args ...string) string {
  188. t.Helper()
  189. out, err := exec.Command("ip", args...).CombinedOutput()
  190. if err != nil {
  191. t.Fatalf("ip %v: %s: %v", args, out, err)
  192. }
  193. return string(out)
  194. }
  195. ip("link", "set", "lo", "up")
  196. ip("link", "add", "route-test0", "type", "dummy")
  197. ip("link", "set", "route-test0", "up")
  198. old := RouteItem{ID: "r1", DstAddress: "198.51.101.0/24", Gateway: "192.0.2.1", Interface: "route-test0", RoutingTable: "main", Distance: 10, Active: true}
  199. ip("route", "add", old.DstAddress, "via", old.Gateway, "dev", old.Interface, "onlink", "metric", "10", "proto", "static")
  200. m := &RoutingManager{staticRoutes: []RouteItem{old}}
  201. if err := m.save(); err != nil {
  202. t.Fatal(err)
  203. }
  204. next := old
  205. next.Gateway = "192.0.2.2"
  206. if err := m.SaveRoute(&next); err != nil {
  207. t.Fatal(err)
  208. }
  209. output := ip("route", "show", old.DstAddress)
  210. if !strings.Contains(output, "via 192.0.2.2") || strings.Contains(output, "via 192.0.2.1") {
  211. t.Fatalf("replacement missing: %s", output)
  212. }
  213. invalid := next
  214. invalid.DstAddress = "203.0.114.0/24"
  215. invalid.Interface = "does-not-exist"
  216. if err := m.SaveRoute(&invalid); err == nil {
  217. t.Fatal("expected failure applying invalid device")
  218. }
  219. output = ip("route", "show", old.DstAddress)
  220. if !strings.Contains(output, "via 192.0.2.2") {
  221. t.Fatalf("old route lost after failed edit: %s", output)
  222. }
  223. if output = ip("route", "show", invalid.DstAddress); strings.TrimSpace(output) != "" {
  224. t.Fatalf("invalid route left in kernel: %s", output)
  225. }
  226. }
  227. func TestIsolatedRouteIDsAndMetricsRemainDistinct(t *testing.T) {
  228. if os.Getenv("DEBTIK_TEST_ISOLATED_NETNS") != "1" {
  229. t.Skip("requires a dedicated network namespace")
  230. }
  231. useTempRouteStores(t)
  232. ip := func(args ...string) {
  233. t.Helper()
  234. if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
  235. t.Fatalf("ip %v: %s %v", args, out, err)
  236. }
  237. }
  238. ip("link", "set", "lo", "up")
  239. ip("link", "add", "route-test1", "type", "dummy")
  240. ip("link", "set", "route-test1", "up")
  241. ip("route", "add", "198.51.102.0/24", "dev", "route-test1", "metric", "10", "proto", "static")
  242. ip("route", "add", "198.51.102.0/24", "dev", "route-test1", "metric", "20", "proto", "static")
  243. m := &RoutingManager{}
  244. var kernelIDs []string
  245. for _, r := range m.listLiveRoutesLocked() {
  246. if r.DstAddress == "198.51.102.0/24" {
  247. kernelIDs = append(kernelIDs, r.ID)
  248. }
  249. }
  250. if len(kernelIDs) != 2 || kernelIDs[0] == kernelIDs[1] {
  251. t.Fatalf("ambiguous kernel IDs: %v", kernelIDs)
  252. }
  253. m.staticRoutes = []RouteItem{{ID: "r20", DstAddress: "198.51.102.0/24", Interface: "route-test1", RoutingTable: "main", Distance: 20, Active: true}, {ID: "r10", DstAddress: "198.51.102.0/24", Interface: "route-test1", RoutingTable: "main", Distance: 10, Active: true}}
  254. seen := map[int]string{}
  255. for _, r := range m.ListRoutes() {
  256. if r.DstAddress == "198.51.102.0/24" && r.Active {
  257. seen[r.Distance] = r.ID
  258. }
  259. }
  260. if seen[10] != "r10" || seen[20] != "r20" {
  261. t.Fatalf("wrong route mapping: %v", seen)
  262. }
  263. m.staticRoutes[1].Disabled = true
  264. for _, r := range m.ListRoutes() {
  265. if r.ID == "r10" && r.Active {
  266. t.Fatal("disabled managed route displayed as active")
  267. }
  268. }
  269. }