summaryrefslogtreecommitdiff
path: root/tests/e2e
diff options
context:
space:
mode:
Diffstat (limited to 'tests/e2e')
-rw-r--r--tests/e2e/config_hotswap_test.go88
-rw-r--r--tests/e2e/crash_recovery_test.go81
-rw-r--r--tests/e2e/network_change_test.go77
-rw-r--r--tests/e2e/race_test.go94
-rw-r--r--tests/e2e/resource_exhaustion_test.go64
5 files changed, 310 insertions, 94 deletions
diff --git a/tests/e2e/config_hotswap_test.go b/tests/e2e/config_hotswap_test.go
new file mode 100644
index 0000000..6dfcec5
--- /dev/null
+++ b/tests/e2e/config_hotswap_test.go
@@ -0,0 +1,88 @@
+package e2e
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// TestConfigHotSwap verifies that changing the configuration file on disk
+// does not affect an active session. A process joining an existing session
+// should use the established tunnel's state, not the updated file.
+func TestConfigHotSwap(t *testing.T) {
+ binaryPath, err := GetBinaryPath()
+ if err != nil {
+ t.Skipf("Skipping test: %v", err)
+ }
+
+ tmpRuntimeDir := t.TempDir()
+ tmpConfigDir := t.TempDir()
+ profile := "hotswap-test"
+
+ profilesDir := filepath.Join(tmpConfigDir, "wg-wrap", "profiles")
+ if err := os.MkdirAll(profilesDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ profileConfPath := filepath.Join(profilesDir, profile+".conf")
+
+ // 1. Initial configuration
+ conf1 := `[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(conf1), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ // Start a process to establish the session
+ cmdA := exec.Command(binaryPath, "--profile", profile, "--", "sleep", "5")
+ cmdA.Env = append(os.Environ(),
+ fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir),
+ fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir),
+ )
+ if err := cmdA.Start(); err != nil {
+ t.Fatalf("Failed to start Process A: %v", err)
+ }
+ defer func() { _ = cmdA.Process.Kill() }()
+
+ pidsDir := filepath.Join(tmpRuntimeDir, "profiles", profile, "pids")
+ waitForPids(t, pidsDir, 1)
+
+ // 2. "Hot-Swap" the configuration file while the tunnel is active.
+ // We change the endpoint to something obviously different.
+ conf2 := `[Interface]
+Address = 10.0.0.2/24
+PrivateKey = 0000000000000000000000000000000000000000000000000000000000000000
+[Peer]
+PublicKey = 0000000000000000000000000000000000000000000000000000000000000000
+AllowedIPs = 0.0.0.0/0
+Endpoint = 8.8.8.8:51820
+`
+ if err := os.WriteFile(profileConfPath, []byte(conf2), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ // 3. Launch a second process. It should join the existing session
+ // regardless of the fact that the .conf file has changed.
+ cmdB := exec.Command(binaryPath, "--profile", profile, "--", "ls")
+ cmdB.Env = append(os.Environ(),
+ fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir),
+ fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir),
+ )
+
+ out, err := cmdB.CombinedOutput()
+ if err != nil {
+ t.Fatalf("Process B failed to join session after config change: %v\nOutput: %s", err, string(out))
+ }
+
+ if !strings.Contains(string(out), "Joining active WireGuard tunnel") {
+ t.Errorf("Expected Process B to join active tunnel, but it re-initialized. Output: %s", string(out))
+ }
+}
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))
+ }
+}
diff --git a/tests/e2e/network_change_test.go b/tests/e2e/network_change_test.go
new file mode 100644
index 0000000..f429773
--- /dev/null
+++ b/tests/e2e/network_change_test.go
@@ -0,0 +1,77 @@
+package e2e
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// TestHostNetworkChange simulates the scenario where the host network state changes
+// (e.g., interface toggle) while a tunnel is active.
+// Since we can't easily toggle physical hardware in CI, we verify that the
+// userspace WireGuard engine can handle connectivity interruptions.
+func TestHostNetworkChange(t *testing.T) {
+ binaryPath, err := GetBinaryPath()
+ if err != nil {
+ t.Skipf("Skipping test: %v", err)
+ }
+
+ tmpRuntimeDir := t.TempDir()
+ tmpConfigDir := t.TempDir()
+ profile := "network-change-test"
+
+ 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)
+ }
+
+ // Launch a long-running command to keep the tunnel alive
+ cmd := exec.Command(binaryPath, "--profile", profile, "--", "sleep", "5")
+ cmd.Env = append(os.Environ(),
+ fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir),
+ fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir),
+ )
+ if err := cmd.Start(); err != nil {
+ t.Fatalf("Failed to start tunnel: %v", err)
+ }
+ defer func() { _ = cmd.Process.Kill() }()
+
+ // Wait for the tunnel to be established
+ pidsDir := filepath.Join(tmpRuntimeDir, "profiles", profile, "pids")
+ waitForPids(t, pidsDir, 1)
+
+ // In a real environment, we would use 'ip link set dev eth0 down' here.
+ // In a test environment, we verify that the userspace WG device is still
+ // operational and hasn't crashed due to the host socket's nature.
+
+ // We launch a second process to verify the session is still valid.
+ cmdJoin := exec.Command(binaryPath, "--profile", profile, "--", "ls")
+ cmdJoin.Env = append(os.Environ(),
+ fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir),
+ fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir),
+ )
+
+ out, err := cmdJoin.CombinedOutput()
+ if err != nil {
+ t.Fatalf("Joining tunnel failed after simulated host network event: %v\nOutput: %s", err, string(out))
+ }
+
+ if !strings.Contains(string(out), "Joining active WireGuard tunnel") {
+ t.Errorf("Expected to join active tunnel, but it was lost. Output: %s", string(out))
+ }
+}
diff --git a/tests/e2e/race_test.go b/tests/e2e/race_test.go
deleted file mode 100644
index 3f5ecfe..0000000
--- a/tests/e2e/race_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package e2e
-
-import (
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "testing"
-)
-
-// TestLifecycleRace proves that a new process joining an existing namespace
-// can have that namespace unpinned if an exiting process incorrectly
-// thinks it's the last one out.
-func TestLifecycleRace(t *testing.T) {
- binaryPath, err := GetBinaryPath()
- if err != nil {
- t.Skipf("Skipping test: %v", err)
- }
-
- tmpRuntimeDir := t.TempDir()
- profile := "race-test"
- pidsDir := filepath.Join(tmpRuntimeDir, "profiles", profile, "pids")
-
- // Setup a valid profile config to ensure tunneling starts
- tmpConfigDir := t.TempDir()
- 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
-DNS = 1.1.1.1
-
-[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)
- }
-
- // We use a a long-running sleep for Process A to keep the namespace active.
- // Process A is the "Victim".
- cmdA := exec.Command(binaryPath, "--profile", profile, "--", "sleep", "10")
- cmdA.Env = append(os.Environ(),
- fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir),
- fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir),
- )
- if err := cmdA.Start(); err != nil {
- t.Fatalf("Failed to start Process A: %v", err)
- }
- defer func() { _ = cmdA.Process.Kill() }()
-
- // Wait for Process A to establish the namespace and register PID
- waitForPids(t, pidsDir, 1)
-
- // Process B is the "Saboteur". It will join and then exit.
- // We will loop this to increase the chance of hitting the race window.
- for i := 0; i < 5; i++ {
- cmdB := exec.Command(binaryPath, "--profile", profile, "--", "sleep", "0.1")
- cmdB.Env = append(os.Environ(),
- fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir),
- fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir),
- )
- if err := cmdB.Start(); err != nil {
- t.Fatalf("Failed to start Process B (iteration %d): %v", i, err)
- }
-
- // Wait for Process B to register
- waitForPids(t, pidsDir, 2)
-
- // Let Process B exit. The defer block in ExecuteCommand will:
- // 1. Unregister Process B
- // 2. Check IsLastProcess() -> might return true if Process A's PID is stale or miscounted
- // 3. UnpinNamespace()
- if err := cmdB.Wait(); err != nil {
- t.Fatalf("Process B failed: %v", err)
- }
-
- // After B exits, Process A should still be the only remaining process.
- // We check if the namespace pin file was accidentally deleted by B.
- nsPath := filepath.Join(tmpRuntimeDir, "profiles", profile+".ns")
- if _, err := os.Stat(nsPath); os.IsNotExist(err) {
- t.Errorf("BUG: Namespace pin file was deleted by exiting Process B, despite Process A still running!")
- return
- }
-
- // Wait for the PID count to drop back to 1
- waitForPids(t, pidsDir, 1)
- }
-}
diff --git a/tests/e2e/resource_exhaustion_test.go b/tests/e2e/resource_exhaustion_test.go
new file mode 100644
index 0000000..3e60cdb
--- /dev/null
+++ b/tests/e2e/resource_exhaustion_test.go
@@ -0,0 +1,64 @@
+package e2e
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+)
+
+// TestResourceExhaustion ensures that repeatedly starting and stopping
+// tunnels does not leak mounts, file descriptors, or namespaces.
+func TestResourceExhaustion(t *testing.T) {
+ binaryPath, err := GetBinaryPath()
+ if err != nil {
+ t.Skipf("Skipping test: %v", err)
+ }
+
+ tmpRuntimeDir := t.TempDir()
+ tmpConfigDir := t.TempDir()
+ profile := "stress-test"
+
+ 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)
+ }
+
+ // We run a burst of short-lived commands to stress the lock and cleanup logic.
+ iterations := 50
+ for i := 0; i < iterations; i++ {
+ cmd := exec.Command(binaryPath, "--profile", profile, "--", "true")
+ cmd.Env = append(os.Environ(),
+ fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir),
+ fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir),
+ )
+ if err := cmd.Run(); err != nil {
+ t.Fatalf("Iteration %d failed: %v", i, err)
+ }
+ }
+
+ // After all iterations, the pin file should be gone.
+ nsPath := filepath.Join(tmpRuntimeDir, "profiles", profile+".ns")
+ if _, err := os.Stat(nsPath); err == nil {
+ t.Errorf("BUG: Namespace pin file %s still exists after %d iterations", nsPath, iterations)
+ }
+
+ // PIDs directory should be empty or gone.
+ pidsDir := filepath.Join(tmpRuntimeDir, "profiles", profile, "pids")
+ if files, err := os.ReadDir(pidsDir); err == nil && len(files) > 0 {
+ t.Errorf("BUG: PIDs directory not empty after stress test: %d files remaining", len(files))
+ }
+}