From d4cec92f5690a60b3509ab718bdea72dc520110e Mon Sep 17 00:00:00 2001 From: James O'Doherty Date: Fri, 29 May 2026 20:35:31 -0400 Subject: feat: implement robust namespace lifecycle and resilience suite - Replace marker-file pinning with kernel bind-mount anchors for reliable namespace persistence. - Implement atomic "last-man-out" cleanup sequence using ProfileLock, preventing namespace leaks and race conditions. - Add comprehensive resilience test suite covering: - Crash recovery from stale runtime state. - Host network change stability. - Configuration hot-swap session persistence. - Resource exhaustion and high-churn lifecycle stress. - Align documentation and test expectations with rootless session-based persistence. - Fix argument integrity and isolation leaks. - Ensure 100% pass rate for all E2E and integration tests. --- tests/e2e/crash_recovery_test.go | 81 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/e2e/crash_recovery_test.go (limited to 'tests/e2e/crash_recovery_test.go') diff --git a/tests/e2e/crash_recovery_test.go b/tests/e2e/crash_recovery_test.go new file mode 100644 index 0000000..618417d --- /dev/null +++ b/tests/e2e/crash_recovery_test.go @@ -0,0 +1,81 @@ +package e2e + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestCrashRecovery verifies that wg-wrap can recover from a "dirty" state +// where a previous run crashed, leaving behind stale PID and pin files. +func TestCrashRecovery(t *testing.T) { + binaryPath, err := GetBinaryPath() + if err != nil { + t.Skipf("Skipping test: %v", err) + } + + tmpRuntimeDir := t.TempDir() + tmpConfigDir := t.TempDir() + profile := "crash-test" + + // Setup a valid profile config + profilesDir := filepath.Join(tmpConfigDir, "wg-wrap", "profiles") + if err := os.MkdirAll(profilesDir, 0755); err != nil { + t.Fatal(err) + } + profileConfPath := filepath.Join(profilesDir, profile+".conf") + conf := `[Interface] +Address = 10.0.0.2/24 +PrivateKey = 0000000000000000000000000000000000000000000000000000000000000000 +[Peer] +PublicKey = 0000000000000000000000000000000000000000000000000000000000000000 +AllowedIPs = 0.0.0.0/0 +Endpoint = 1.1.1.1:51820 +` + if err := os.WriteFile(profileConfPath, []byte(conf), 0644); err != nil { + t.Fatal(err) + } + + // 1. Simulate a "Crash" by creating stale state manually. + // We create a pin file and some PID files for processes that aren't actually running. + nsPath := filepath.Join(tmpRuntimeDir, "profiles", profile+".ns") + profilesRuntimeDir := filepath.Join(tmpRuntimeDir, "profiles") + if err := os.MkdirAll(profilesRuntimeDir, 0755); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(nsPath, []byte("stale-pin"), 0644); err != nil { + t.Fatal(err) + } + + pidsDir := filepath.Join(tmpRuntimeDir, "profiles", profile, "pids") + if err := os.MkdirAll(pidsDir, 0755); err != nil { + t.Fatal(err) + } + // Create a fake PID file for a process that likely doesn't exist (PID 1234567) + if err := os.WriteFile(filepath.Join(pidsDir, "1234567"), []byte(""), 0644); err != nil { + t.Fatal(err) + } + + // 2. Try to run wg-wrap. + // It should see the stale PID, prune it, realize the namespace is actually dead, + // and start a fresh tunnel. + cmd := exec.Command(binaryPath, "--profile", profile, "--", "ls") + cmd.Env = append(os.Environ(), + fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir), + fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir), + ) + + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("wg-wrap failed to recover from stale state: %v\nOutput: %s", err, string(out)) + } + + // 3. Verify it actually started a tunnel (it should have printed "Initializing...") + if !strings.Contains(string(out), "Initializing WireGuard tunnel") { + t.Errorf("Expected wg-wrap to initialize a new tunnel, but it didn't. Output: %s", string(out)) + } +} -- cgit v1.2.3