summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-05-29 21:24:42 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-05-29 21:24:42 -0400
commitb098e2845b68ce90f34e4e1e927b4914d0b00ef7 (patch)
tree2288efa8e09ab93735e6ae2c25d42ee33634eb2a
parent0f3806f77164af99466bfc8c0d7d5f85f9ec078f (diff)
fix: resolve deadlocks, routing errors, and test timings in test suite
- fix(cli): resolve Flock self-deadlock in ExecuteCommand cleanup by reusing the existing lockFile handle if already held during premature exit. - fix(wireguard): configure explicit default route destination (0.0.0.0/0 for IPv4 and ::/0 for IPv6) to avoid netlink "either Dst.IP, Src.IP or Gw must be set" error. - fix(wireguard): initialize the Tunnel return parameter in StartTunnel to prevent a nil pointer dereference. - test(e2e): fix argument ordering in waitForLifecycle to pass "test-lifecycle" first, and increase sleep duration of dummy processes to 1.0s to ensure reliable process persistence under race detection.
-rw-r--r--internal/cli/cli.go24
-rw-r--r--internal/wireguard/wireguard.go17
-rw-r--r--tests/e2e/config_hotswap_test.go2
-rw-r--r--tests/e2e/lifecycle_test.go6
4 files changed, 38 insertions, 11 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 7d5a05c..0e3b8ad 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -167,8 +167,13 @@ func (a *App) ExecuteCommand(cfg *config.Config) error {
// Acquire execution lock during configuration and startup inside the namespace
lockFile, lockErr := namespace.AcquireProfileLock(pm, cfg.Profile)
+ var lockFileReleased bool
if lockErr == nil {
- defer namespace.ReleaseProfileLock(lockFile)
+ defer func() {
+ if !lockFileReleased {
+ namespace.ReleaseProfileLock(lockFile)
+ }
+ }()
}
if err := namespace.PruneStalePids(pm, cfg.Profile); err != nil {
@@ -179,8 +184,17 @@ func (a *App) ExecuteCommand(cfg *config.Config) error {
}
defer func() {
- // Re-acquire lock for the entire cleanup sequence to ensure atomic unregister and unpin
- cleanupLock, cleanupErr := namespace.AcquireProfileLock(pm, cfg.Profile)
+ var cleanupLock *os.File
+ var cleanupErr error
+
+ if lockErr == nil && !lockFileReleased {
+ // We already hold the lock, so we can just reuse lockFile for cleanup!
+ cleanupLock = lockFile
+ } else {
+ // Re-acquire lock for the entire cleanup sequence to ensure atomic unregister and unpin
+ cleanupLock, cleanupErr = namespace.AcquireProfileLock(pm, cfg.Profile)
+ }
+
if cleanupErr == nil {
// 1. Unregister the process first.
if err := namespace.UnregisterProcess(pm, cfg.Profile); err != nil {
@@ -200,6 +214,9 @@ func (a *App) ExecuteCommand(cfg *config.Config) error {
fmt.Printf("failed to unpin namespace: %v\n", err)
}
}
+ if lockErr == nil && !lockFileReleased {
+ lockFileReleased = true
+ }
namespace.ReleaseProfileLock(cleanupLock)
} else {
// Fallback if lock fails to ensure we still unregister
@@ -270,6 +287,7 @@ func (a *App) ExecuteCommand(cfg *config.Config) error {
}
// We can now release the startup lock and execute the command
+ lockFileReleased = true
namespace.ReleaseProfileLock(lockFile)
cmd := exec.Command(cfg.Command[0], cfg.Command[1:]...)
diff --git a/internal/wireguard/wireguard.go b/internal/wireguard/wireguard.go
index 3c293b4..e250dab 100644
--- a/internal/wireguard/wireguard.go
+++ b/internal/wireguard/wireguard.go
@@ -98,15 +98,17 @@ func StartTunnel(cfg *wgconf.Config, dnsServer string) (t *Tunnel, err error) {
return nil, fmt.Errorf("failed to configure network interface %s: %w", tunName, err)
}
+ var dnsFile string
if path, err := ConfigureResolvConf(dnsServer); err != nil {
fmt.Printf("warning: failed to configure DNS resolver: %v\n", err)
} else {
- t.dnsFile = path
+ dnsFile = path
}
return &Tunnel{
- Device: wgDev,
- Tun: tunDev,
+ Device: wgDev,
+ Tun: tunDev,
+ dnsFile: dnsFile,
}, nil
}
@@ -194,10 +196,17 @@ func configureInterface(name, address string, mtu int) error {
}
}
+ var dst *net.IPNet
+ if addr.IP.To4() != nil {
+ _, dst, _ = net.ParseCIDR("0.0.0.0/0")
+ } else {
+ _, dst, _ = net.ParseCIDR("::/0")
+ }
+
route := &netlink.Route{
Scope: netlink.SCOPE_UNIVERSE,
LinkIndex: link.Attrs().Index,
- Dst: nil,
+ Dst: dst,
}
if err := netlink.RouteAdd(route); err != nil {
diff --git a/tests/e2e/config_hotswap_test.go b/tests/e2e/config_hotswap_test.go
index 5c483ac..b4a7ca5 100644
--- a/tests/e2e/config_hotswap_test.go
+++ b/tests/e2e/config_hotswap_test.go
@@ -42,7 +42,7 @@ Endpoint = 1.1.1.1:51820
}
// Start a process to establish the session
- cmdA := exec.Command(binaryPath, "--profile", profile, "--", "sleep", "0.1")
+ cmdA := exec.Command(binaryPath, "--profile", profile, "--", "sleep", "1.0")
cmdA.Env = append(os.Environ(),
fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir),
fmt.Sprintf("XDG_CONFIG_HOME=%s", tmpConfigDir),
diff --git a/tests/e2e/lifecycle_test.go b/tests/e2e/lifecycle_test.go
index 4b452da..67b1370 100644
--- a/tests/e2e/lifecycle_test.go
+++ b/tests/e2e/lifecycle_test.go
@@ -47,7 +47,7 @@ func waitForLifecycle(t *testing.T, binaryPath, runtimeDir, profile string, expe
case <-timeout:
t.Fatalf("Timed out waiting for lifecycle state: expected active=%v", expectedActive)
case <-tick.C:
- cmd := exec.Command(binaryPath, "--profile", profile, "test-lifecycle")
+ cmd := exec.Command(binaryPath, "test-lifecycle", "--profile", profile)
cmd.Env = append(os.Environ(), fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir))
err := cmd.Run()
@@ -78,7 +78,7 @@ func TestNamespaceLifecycleAutomation(t *testing.T) {
t.Run("ReferenceCounting", func(t *testing.T) {
// Start a process that exits quickly
- cmd1 := exec.Command(binaryPath, "--profile", "default", "--", "sleep", "0.1")
+ cmd1 := exec.Command(binaryPath, "--profile", "default", "--", "sleep", "1.0")
cmd1.Env = append(os.Environ(), fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir))
if err := cmd1.Start(); err != nil {
t.Fatalf("Failed to start cmd1: %v", err)
@@ -88,7 +88,7 @@ func TestNamespaceLifecycleAutomation(t *testing.T) {
waitForLifecycle(t, binaryPath, tmpRuntimeDir, "default", true)
// Start a second process using the same profile
- cmd2 := exec.Command(binaryPath, "--profile", "default", "--", "sleep", "0.1")
+ cmd2 := exec.Command(binaryPath, "--profile", "default", "--", "sleep", "1.0")
cmd2.Env = append(os.Environ(), fmt.Sprintf("XDG_RUNTIME_DIR=%s", tmpRuntimeDir))
if err := cmd2.Start(); err != nil {
t.Fatalf("Failed to start cmd2: %v", err)