diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-05-29 20:11:07 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-05-29 20:11:07 -0400 |
| commit | 4ddd0d2ffc7073f2d55ffb6777e3a168af0051f0 (patch) | |
| tree | aae9afa6681f4366807b0bd70fd8bec00ab0db8d /internal/namespace/namespace.go | |
| parent | a7c7fa9e76c9c7015c31378062aa5d0c17b0f38f (diff) | |
Refactor rootless namespace joining to use C launcher
Fix an architectural shortfall where concurrent sessions failed to share
the target network and mount namespaces. Because the Go runtime is
multi-threaded, calling unix.Setns with CLONE_NEWNS from Go always returned
EINVAL, silently forcing concurrent runs to fall back to bootstrapping separate
isolated namespaces and separate WireGuard connections.
This commit resolves the issue by extending the embedded single-threaded C
launcher to handle namespace joining, and introducing a host-to-isolated path
propagation pattern:
1. Launcher setns Support: The C launcher now checks for WG_WRAP_JOIN_PID in
the environment. If present, it joins the User, Mount, and Network
namespaces of the active PID in single-threaded mode before executing the Go
binary.
2. BootstrapJoin Integration: Implemented namespace.BootstrapJoin to
transition joining sessions via the launcher.
3. Path Preservation: Export WG_WRAP_HOST_RUNTIME_BASE_DIR from the host to ensure
the isolated instance maps the profile and PID directories to the exact
same location.
4. Redundant Tunnel Bypass: Detect joined sessions via WG_WRAP_JOINED=1 in the CLI
and bypass starting a duplicate WireGuard tunnel on the occupied tun0.
5. Testing: Added tests/e2e/sharing_test.go to assert namespace ID equality,
which now passes successfully.
6. Git Tracking: Fixed .gitignore overmatch to stop ignoring cmd/wg-wrap/.
Diffstat (limited to 'internal/namespace/namespace.go')
| -rw-r--r-- | internal/namespace/namespace.go | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/internal/namespace/namespace.go b/internal/namespace/namespace.go index ab3797d..0f2618b 100644 --- a/internal/namespace/namespace.go +++ b/internal/namespace/namespace.go @@ -194,3 +194,98 @@ func Bootstrap() (err error) { return nil } + +// BootstrapJoin joins the namespaces of the target PID and replaces the current process. +func BootstrapJoin(targetPid int) (err error) { + if IsIsolated() { + return nil + } + + var fdsToClose []int + defer func() { + for _, fd := range fdsToClose { + _ = syscall.Close(fd) + } + }() + + // Validate current arguments for null bytes before proceeding. + for i, arg := range os.Args { + for j := 0; j < len(arg); j++ { + if arg[j] == 0 { + return fmt.Errorf("argument %d contains null byte at position %d", i, j) + } + } + } + + self, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + // 1. Create a secure temporary file for the launcher binary. + tmpFile, err := os.CreateTemp("", "wg-wrap-launcher-") + if err != nil { + return fmt.Errorf("failed to create temp launcher file: %w", err) + } + launcherPath := tmpFile.Name() + + // 2. Write the embedded launcher binary to the temp file. + if _, err := tmpFile.Write(launcherBytes); err != nil { + _ = tmpFile.Close() + _ = os.Remove(launcherPath) + return fmt.Errorf("failed to write launcher binary: %w", err) + } + + // Ensure the binary is executable (0700) + if err := tmpFile.Chmod(0700); err != nil { + _ = tmpFile.Close() + _ = os.Remove(launcherPath) + return fmt.Errorf("failed to set launcher permissions: %w", err) + } + + // 2b. Open a read-only fd of the launcher to exec + execFd, err := syscall.Open(launcherPath, syscall.O_RDONLY, 0) + if err != nil { + _ = tmpFile.Close() + _ = os.Remove(launcherPath) + return fmt.Errorf("failed to open launcher for exec: %w", err) + } + fdsToClose = append(fdsToClose, execFd) + + // Close the write file descriptor (to avoid ETXTBSY) + _ = tmpFile.Close() + + // Unlink the file from disk (makes it invisible and ensures it is deleted on exit) + _ = os.Remove(launcherPath) + + // Clear close-on-exec so it remains open across syscall.Exec + if flags, err := unix.FcntlInt(uintptr(execFd), unix.F_GETFD, 0); err == nil { + _, _ = unix.FcntlInt(uintptr(execFd), unix.F_SETFD, flags&^unix.FD_CLOEXEC) + } + + // 3. Prepare arguments for the launcher. + args := []string{self} + args = append(args, os.Args[1:]...) + + for i, arg := range args { + for j := 0; j < len(arg); j++ { + if arg[j] == 0 { + return fmt.Errorf("launcher argument %d contains null byte at position %d", i, j) + } + } + } + + // Set environment variables to tell the C launcher to join, + // and to tell the second wg-wrap instance that we are in a joined session. + env := append(os.Environ(), + fmt.Sprintf("WG_WRAP_JOIN_PID=%d", targetPid), + "WG_WRAP_JOINED=1", + ) + + err = syscall.Exec(fmt.Sprintf("/proc/self/fd/%d", execFd), args, env) + if err != nil { + return fmt.Errorf("launcher exec failed: %w", err) + } + + return nil +} |
