| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- package routing
- import (
- "encoding/json"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "testing"
- )
- func useTempRouteStores(t *testing.T) {
- t.Helper()
- oldStatic, oldPolicy, oldTables := StaticRoutesStorePath, PolicyRulesStorePath, CustomTablesStorePath
- dir := t.TempDir()
- StaticRoutesStorePath = filepath.Join(dir, "static.json")
- PolicyRulesStorePath = filepath.Join(dir, "rules.json")
- CustomTablesStorePath = filepath.Join(dir, "tables.json")
- t.Cleanup(func() {
- StaticRoutesStorePath, PolicyRulesStorePath, CustomTablesStorePath = oldStatic, oldPolicy, oldTables
- })
- }
- func TestRouteEnableFailureRestoresStoredAndMemoryState(t *testing.T) {
- useTempRouteStores(t)
- bin := t.TempDir()
- t.Setenv("PATH", bin)
- script := "#!/bin/sh\nif [ \"$2\" = del ]; then echo 'RTNETLINK answers: No such process' >&2; exit 2; fi\necho 'permission denied' >&2; exit 2\n"
- if err := os.WriteFile(filepath.Join(bin, "ip"), []byte(script), 0755); err != nil {
- t.Fatal(err)
- }
- 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}
- m := &RoutingManager{staticRoutes: []RouteItem{old}}
- if err := m.save(); err != nil {
- t.Fatal(err)
- }
- err := m.SetRouteState("r1", false)
- if err == nil || !strings.Contains(err.Error(), "applying route change") {
- t.Fatalf("expected apply error: %v", err)
- }
- if !m.staticRoutes[0].Disabled || m.staticRoutes[0].Active {
- t.Fatalf("memory not restored: %+v", m.staticRoutes[0])
- }
- data, err := os.ReadFile(StaticRoutesStorePath)
- if err != nil {
- t.Fatal(err)
- }
- var persisted []RouteItem
- if err = json.Unmarshal(data, &persisted); err != nil {
- t.Fatal(err)
- }
- if len(persisted) != 1 || !persisted[0].Disabled {
- t.Fatalf("storage not restored: %+v", persisted)
- }
- }
- func TestRouteDeletionNeverUsesUnqualifiedFallback(t *testing.T) {
- useTempRouteStores(t)
- bin := t.TempDir()
- t.Setenv("PATH", bin)
- logPath := filepath.Join(t.TempDir(), "commands")
- t.Setenv("DEBTIK_TEST_IP_LOG", logPath)
- script := "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$DEBTIK_TEST_IP_LOG\"\necho 'RTNETLINK answers: No such process' >&2\nexit 2\n"
- if err := os.WriteFile(filepath.Join(bin, "ip"), []byte(script), 0755); err != nil {
- t.Fatal(err)
- }
- r := RouteItem{ID: "r1", DstAddress: "198.51.100.0/24", Gateway: "192.0.2.1", Interface: "eth0", RoutingTable: "100", Distance: 10, Active: true}
- m := &RoutingManager{staticRoutes: []RouteItem{r}}
- if err := m.save(); err != nil {
- t.Fatal(err)
- }
- if err := m.DeleteRoute("r1"); err != nil {
- t.Fatal(err)
- }
- data, err := os.ReadFile(logPath)
- if err != nil {
- t.Fatal(err)
- }
- lines := strings.Split(strings.TrimSpace(string(data)), "\n")
- if len(lines) != 2 {
- t.Fatalf("unexpected commands: %s", data)
- }
- for _, line := range lines {
- if !strings.Contains(line, "metric 10 table 100 proto static") {
- t.Fatalf("unsafe deletion: %s", line)
- }
- }
- }
- func TestIsolatedRouteGroupDeletionKeepsOtherMetric(t *testing.T) {
- if os.Getenv("DEBTIK_TEST_ISOLATED_NETNS") != "1" {
- t.Skip("requires a dedicated network namespace")
- }
- ip := func(args ...string) {
- t.Helper()
- if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
- t.Fatalf("ip %v: %s: %v", args, out, err)
- }
- }
- ip("link", "set", "lo", "up")
- ip("route", "add", "198.51.100.0/24", "dev", "lo", "metric", "10", "proto", "static")
- ip("route", "add", "198.51.100.0/24", "dev", "lo", "metric", "20", "proto", "static")
- m := &RoutingManager{}
- if err := m.syncRouteGroupToKernel("198.51.100.0/24", "main", 10); err != nil {
- t.Fatal(err)
- }
- out, err := exec.Command("ip", "route", "show", "198.51.100.0/24").CombinedOutput()
- if err != nil {
- t.Fatal(err)
- }
- if strings.Contains(string(out), "metric 10") || !strings.Contains(string(out), "metric 20") {
- t.Fatalf("unexpected routes: %s", out)
- }
- }
- func TestRouteEditSameGroupDoesNotDeleteReplacement(t *testing.T) {
- useTempRouteStores(t)
- bin := t.TempDir()
- t.Setenv("PATH", bin)
- logPath := filepath.Join(t.TempDir(), "commands")
- t.Setenv("DEBTIK_TEST_IP_LOG", logPath)
- script := "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$DEBTIK_TEST_IP_LOG\"\nexit 0\n"
- if err := os.WriteFile(filepath.Join(bin, "ip"), []byte(script), 0755); err != nil {
- t.Fatal(err)
- }
- old := RouteItem{ID: "r1", DstAddress: "198.51.100.0/24", Gateway: "192.0.2.1", Interface: "eth0", RoutingTable: "main", Distance: 10, Active: true}
- m := &RoutingManager{staticRoutes: []RouteItem{old}}
- if err := m.save(); err != nil {
- t.Fatal(err)
- }
- next := old
- next.Gateway = "192.0.2.2"
- if err := m.SaveRoute(&next); err != nil {
- t.Fatal(err)
- }
- data, err := os.ReadFile(logPath)
- if err != nil {
- t.Fatal(err)
- }
- commands := strings.Split(strings.TrimSpace(string(data)), "\n")
- if len(commands) != 1 || !strings.Contains(commands[0], "route replace 198.51.100.0/24 via 192.0.2.2") {
- t.Fatalf("replacement was deleted or repeated: %s", data)
- }
- }
- func TestRouteEditCleanupFailureRollsBack(t *testing.T) {
- useTempRouteStores(t)
- bin := t.TempDir()
- t.Setenv("PATH", bin)
- logPath := filepath.Join(t.TempDir(), "commands")
- t.Setenv("DEBTIK_TEST_IP_LOG", logPath)
- 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"
- if err := os.WriteFile(filepath.Join(bin, "ip"), []byte(script), 0755); err != nil {
- t.Fatal(err)
- }
- old := RouteItem{ID: "r1", DstAddress: "198.51.100.0/24", Gateway: "192.0.2.1", Interface: "eth0", RoutingTable: "main", Distance: 10, Active: true}
- m := &RoutingManager{staticRoutes: []RouteItem{old}}
- if err := m.save(); err != nil {
- t.Fatal(err)
- }
- next := old
- next.DstAddress = "203.0.113.0/24"
- err := m.SaveRoute(&next)
- if err == nil || !strings.Contains(err.Error(), "cleaning previous route group") {
- t.Fatalf("cleanup failure ignored: %v", err)
- }
- if len(m.staticRoutes) != 1 || m.staticRoutes[0].DstAddress != old.DstAddress {
- t.Fatalf("memory not restored: %+v", m.staticRoutes)
- }
- data, err := os.ReadFile(StaticRoutesStorePath)
- if err != nil {
- t.Fatal(err)
- }
- var persisted []RouteItem
- if err = json.Unmarshal(data, &persisted); err != nil {
- t.Fatal(err)
- }
- if len(persisted) != 1 || persisted[0].DstAddress != old.DstAddress {
- t.Fatalf("storage not restored: %+v", persisted)
- }
- cmds, err := os.ReadFile(logPath)
- if err != nil {
- t.Fatal(err)
- }
- if !strings.Contains(string(cmds), "route replace 198.51.100.0/24") || !strings.Contains(string(cmds), "route del 203.0.113.0/24") {
- t.Fatalf("rollback omitted: %s", cmds)
- }
- }
- func TestIsolatedRouteEditKeepsReplacementAndRollsBackFailure(t *testing.T) {
- if os.Getenv("DEBTIK_TEST_ISOLATED_NETNS") != "1" {
- t.Skip("requires a dedicated network namespace")
- }
- useTempRouteStores(t)
- ip := func(args ...string) string {
- t.Helper()
- out, err := exec.Command("ip", args...).CombinedOutput()
- if err != nil {
- t.Fatalf("ip %v: %s: %v", args, out, err)
- }
- return string(out)
- }
- ip("link", "set", "lo", "up")
- ip("link", "add", "route-test0", "type", "dummy")
- ip("link", "set", "route-test0", "up")
- old := RouteItem{ID: "r1", DstAddress: "198.51.101.0/24", Gateway: "192.0.2.1", Interface: "route-test0", RoutingTable: "main", Distance: 10, Active: true}
- ip("route", "add", old.DstAddress, "via", old.Gateway, "dev", old.Interface, "onlink", "metric", "10", "proto", "static")
- m := &RoutingManager{staticRoutes: []RouteItem{old}}
- if err := m.save(); err != nil {
- t.Fatal(err)
- }
- next := old
- next.Gateway = "192.0.2.2"
- if err := m.SaveRoute(&next); err != nil {
- t.Fatal(err)
- }
- output := ip("route", "show", old.DstAddress)
- if !strings.Contains(output, "via 192.0.2.2") || strings.Contains(output, "via 192.0.2.1") {
- t.Fatalf("replacement missing: %s", output)
- }
- invalid := next
- invalid.DstAddress = "203.0.114.0/24"
- invalid.Interface = "does-not-exist"
- if err := m.SaveRoute(&invalid); err == nil {
- t.Fatal("expected failure applying invalid device")
- }
- output = ip("route", "show", old.DstAddress)
- if !strings.Contains(output, "via 192.0.2.2") {
- t.Fatalf("old route lost after failed edit: %s", output)
- }
- if output = ip("route", "show", invalid.DstAddress); strings.TrimSpace(output) != "" {
- t.Fatalf("invalid route left in kernel: %s", output)
- }
- }
- func TestIsolatedRouteIDsAndMetricsRemainDistinct(t *testing.T) {
- if os.Getenv("DEBTIK_TEST_ISOLATED_NETNS") != "1" {
- t.Skip("requires a dedicated network namespace")
- }
- useTempRouteStores(t)
- ip := func(args ...string) {
- t.Helper()
- if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
- t.Fatalf("ip %v: %s %v", args, out, err)
- }
- }
- ip("link", "set", "lo", "up")
- ip("link", "add", "route-test1", "type", "dummy")
- ip("link", "set", "route-test1", "up")
- ip("route", "add", "198.51.102.0/24", "dev", "route-test1", "metric", "10", "proto", "static")
- ip("route", "add", "198.51.102.0/24", "dev", "route-test1", "metric", "20", "proto", "static")
- m := &RoutingManager{}
- var kernelIDs []string
- for _, r := range m.listLiveRoutesLocked() {
- if r.DstAddress == "198.51.102.0/24" {
- kernelIDs = append(kernelIDs, r.ID)
- }
- }
- if len(kernelIDs) != 2 || kernelIDs[0] == kernelIDs[1] {
- t.Fatalf("ambiguous kernel IDs: %v", kernelIDs)
- }
- 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}}
- seen := map[int]string{}
- for _, r := range m.ListRoutes() {
- if r.DstAddress == "198.51.102.0/24" && r.Active {
- seen[r.Distance] = r.ID
- }
- }
- if seen[10] != "r10" || seen[20] != "r20" {
- t.Fatalf("wrong route mapping: %v", seen)
- }
- m.staticRoutes[1].Disabled = true
- for _, r := range m.ListRoutes() {
- if r.ID == "r10" && r.Active {
- t.Fatal("disabled managed route displayed as active")
- }
- }
- }
|