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) } }