From d2173cdbc03884ecd9534e9369f8ebe1634f7e9c Mon Sep 17 00:00:00 2001 From: James O'Doherty Date: Fri, 29 May 2026 21:07:46 -0400 Subject: feat: harden bootstrap and optimize network data path - Security: Eliminate namespace escape risk by removing `HostBind` and enforcing `FDBind` using pre-opened host socket FDs. - Security: Replace unsafe `atoi` with `strtol` and strict validation in the C launcher to prevent malformed PID joins. - Stability: Fix PID wraparound by storing session timestamps in PID files to detect recycled PIDs. - Stability: Resolve DNS mount leaks by implementing proper unmounting of `/etc/resolv.conf` during tunnel shutdown. - Performance: Optimize `FDBind` throughput by implementing batch packet processing in the receive loop. - Deployment: Implement `memfd_create` for the C launcher to support `noexec` temporary directories and reduce disk I/O. - Maintenance: Replace external `ip` CLI dependency with native `netlink` library for robust network configuration. - Quality: Fix all `golangci-lint` errors and replace remaining panics with explicit error handling. --- tests/e2e/mount_leak_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/e2e/mount_leak_test.go (limited to 'tests/e2e/mount_leak_test.go') diff --git a/tests/e2e/mount_leak_test.go b/tests/e2e/mount_leak_test.go new file mode 100644 index 0000000..bdc9d75 --- /dev/null +++ b/tests/e2e/mount_leak_test.go @@ -0,0 +1,70 @@ +package e2e + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" + "testing" +) + +// TestDNSMountLeak verifies that /etc/resolv.conf bind mounts are cleaned up +// after a profile is stopped. +func TestDNSMountLeak(t *testing.T) { + bin, err := GetBinaryPath() + if err != nil { + t.Fatal(err) + } + + profile := "leak-test" + dnsServer := "8.8.8.8" + + // Pre-create a dummy config for the profile + configDir := "/tmp/wg-wrap-test-configs" + if err := os.MkdirAll(configDir, 0755); err != nil { + t.Fatalf("failed to create config dir: %v", err) + } + configPath := fmt.Sprintf("%s/%s.conf", configDir, profile) + if err := os.WriteFile(configPath, []byte("[Interface]\nAddress = 10.0.0.1/24\nPrivateKey = aAAA\n"), 0644); err != nil { + t.Fatalf("failed to write config file: %v", err) + } + + // Run the binary with the custom config dir override. + // We use a short-lived command ('true') to trigger the deferred cleanup. + fullCmd := fmt.Sprintf("WG_WRAP_CONFIG_DIR=%s %s -profile %s -dns-server %s -- true", configDir, bin, profile, dnsServer) + + cmd := exec.Command("bash", "-c", fullCmd) + if err := cmd.Run(); err != nil { + t.Logf("Command exited with error (might be normal in some test envs): %v", err) + } + + // 2. Inspect /proc/self/mounts for any remnants of "resolvconf" + // Note: In a real scenario, we might need to inspect mounts from a privileged + // perspective or check the target's namespace mounts if we had a way to keep it open. + // But since we are checking the host's mount table for leaked bind mounts + // that weren't unmounted, we check /proc/self/mounts. + mounts, err := os.Open("/proc/self/mounts") + if err != nil { + t.Fatalf("failed to open /proc/self/mounts: %v", err) + } + defer func() { + if err := mounts.Close(); err != nil { + t.Errorf("failed to close mounts file: %v", err) + } + }() + + scanner := bufio.NewScanner(mounts) + foundLeak := false + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, "resolvconf") && strings.Contains(line, "/etc/resolv.conf") { + foundLeak = true + t.Errorf("Found leaking bind mount in /proc/self/mounts: %s", line) + } + } + + if foundLeak { + t.Errorf("Detected a DNS resolv.conf mount leak after profile exit") + } +} -- cgit v1.2.3