// Package namespace provides primitives for managing Linux user and network // namespaces, including bootstrapping and pinning. // // Rootless Bootstrap Loop & Host-Socket Preservation: // To achieve rootless network isolation without interfering with the Go runtime's multi-threaded // scheduler, and to maintain encrypted UDP socket connectivity over the host's network, // wg-wrap employs an advanced bootstrap loop: // // 1. Host-Bound Socket Creation: During the initial host-level start, a UDP socket is opened // on 0.0.0.0:0 on the host, and its FD is stored in the environment (WG_WRAP_HOST_SOCKET_FD). // 2. Helper Deployment: An embedded single-threaded C launcher is used to bridge the transition. // 3. Namespace Transition: The process replaces itself with the C launcher via syscall.Exec. // 4. Isolation: The launcher performs the unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET) // sequence to isolate Mount, User, and Network environments. // 5. Re-entry: The launcher then execvp's the original wg-wrap binary. // 6. FDBind Tunnel Initialization: The second instance of wg-wrap wraps the host socket FD // inside a custom FDBind struct to initialize wireguard-go. // // User Namespace Sequence: // To create a network namespace without root, wg-wrap follows the sequence: // CLONE_NEWUSER -> CLONE_NEWNET -> Setuid/Setgid -> Configure Interfaces. package namespace import ( _ "embed" "fmt" "net" "os" "syscall" "git.theodohertyfamily.com/wg-wrap/internal/network" "golang.org/x/sys/unix" ) // MountOps abstracts the filesystem mount operations. type MountOps interface { Mount(source, target, fstype string, flags uintptr, data string) error Unmount(target string, flags int) error } // realMountOps is the production implementation using unix.Mount. type realMountOps struct{} func (r *realMountOps) Mount(source, target, fstype string, flags uintptr, data string) error { return unix.Mount(source, target, fstype, flags, data) } func (r *realMountOps) Unmount(target string, flags int) error { return unix.Unmount(target, flags) } // DefaultMountOps is the global instance used by the package functions. var DefaultMountOps MountOps = &realMountOps{} // FileSystem abstracts the basic filesystem operations used for isolation. type FileSystem interface { Stat(name string) (os.FileInfo, error) MkdirAll(path string, perm os.FileMode) error CreateTemp(dir, pattern string) (*os.File, error) MkdirTemp(dir, pattern string) (string, error) Remove(name string) error } // realFS is the production implementation using the os package. type realFS struct{} func (r *realFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } func (r *realFS) MkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) } func (r *realFS) CreateTemp(dir, pattern string) (*os.File, error) { return os.CreateTemp(dir, pattern) } func (r *realFS) MkdirTemp(dir, pattern string) (string, error) { return os.MkdirTemp(dir, pattern) } func (r *realFS) Remove(name string) error { return os.Remove(name) } // DefaultFS is the global instance used by the package functions. var DefaultFS FileSystem = &realFS{} // ResetDefaults restores the default implementations of MountOps and FileSystem. func ResetDefaults() { DefaultMountOps = &realMountOps{} DefaultFS = &realFS{} } // BootstrapConfig contains the environment and arguments needed to execute the bootstrap launcher. type BootstrapConfig struct { Args []string Env []string Fds []int ExecPath string } //go:embed launcher.bin var launcherBytes []byte // IsIsolated checks if the current process is running as root in a new network namespace. func IsIsolated() bool { return os.Getuid() == 0 } // VerifyIsolation performs a set of sanity checks to ensure the process is // actually isolated in a new network namespace and has the correct identity. func VerifyIsolation() (bool, string) { // 1. Check UID if os.Getuid() != 0 { return false, fmt.Sprintf("Expected UID 0, got %d", os.Getuid()) } // 2. Check Network Isolation // We expect a fresh network namespace to have only the loopback interface. interfaces, err := network.ListInterfaces() if err != nil { return false, fmt.Sprintf("failed to list interfaces: %v", err) } // In a fresh netns, we typically only see 'lo'. // If we see more than just loopback, or loopback is missing, it might not be isolated. if len(interfaces) == 0 { return false, "no network interfaces found" } hasLo := false for _, iface := range interfaces { if iface.Name == "lo" { hasLo = true } else { // If we find any other interface (eth0, wlan0, etc.), we aren't isolated. return false, fmt.Sprintf("detected non-isolated interface: %s", iface.Name) } } if !hasLo { return false, "loopback interface missing" } // 3. Check Filesystem Transparency home := os.Getenv("HOME") if home != "" { if _, err := os.ReadDir(home); err != nil { return false, fmt.Sprintf("cannot read home directory: %v", err) } } return true, "Isolated and root" } // VerifyArguments prints the current process arguments as hex-encoded strings. // This is used for E2E testing to verify that the data path is 8-bit clean // and that no bytes are mutated during the bootstrap loop. func VerifyArguments(args []string) error { for i, arg := range args { fmt.Printf("%d:%x\n", i, arg) } return nil } // Bootstrap ensures the process is running in an isolated user and network namespace. // It uses memfd_create to run the embedded C launcher from memory, bypassing // disk-based noexec restrictions. func Bootstrap() (err error) { if IsIsolated() { return nil } config, err := PrepareBootstrap() if err != nil { return err } defer func() { for _, fd := range config.Fds { _ = syscall.Close(fd) } }() err = syscall.Exec(config.ExecPath, config.Args, config.Env) if err != nil { return fmt.Errorf("launcher exec failed: %w", err) } return nil } // PrepareBootstrap calculates the environment and arguments needed for the bootstrap launcher. func PrepareBootstrap() (*BootstrapConfig, error) { // 0. 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 nil, fmt.Errorf("argument %d contains null byte at position %d", i, j) } } } self, err := os.Executable() if err != nil { return nil, fmt.Errorf("failed to get executable path: %w", err) } execFd, err := prepareLauncher() if err != nil { return nil, err } // Clear close-on-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) } // 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 nil, fmt.Errorf("launcher argument %d contains null byte at position %d", i, j) } } } // Open the host network namespace file descriptor before unsharing. hostNetFd, err := syscall.Open("/proc/self/ns/net", syscall.O_RDONLY, 0) if err != nil { return nil, fmt.Errorf("failed to open host netns: %w", err) } // Clear close-on-exec if flags, err := unix.FcntlInt(uintptr(hostNetFd), unix.F_GETFD, 0); err == nil { _, _ = unix.FcntlInt(uintptr(hostNetFd), unix.F_SETFD, flags&^unix.FD_CLOEXEC) } env := append(os.Environ(), fmt.Sprintf("WG_WRAP_HOST_NETNS_FD=%d", hostNetFd)) // Open a host UDP socket on 0.0.0.0:0 before unsharing network namespace. laddr, errAddr := net.ResolveUDPAddr("udp", "0.0.0.0:0") if errAddr == nil { if conn, errConn := net.ListenUDP("udp", laddr); errConn == nil { if file, errFile := conn.File(); errFile == nil { hostSocketFd := file.Fd() if flags, fcntlErr := unix.FcntlInt(hostSocketFd, unix.F_GETFD, 0); fcntlErr == nil { _, _ = unix.FcntlInt(hostSocketFd, unix.F_SETFD, flags&^unix.FD_CLOEXEC) } env = append(env, fmt.Sprintf("WG_WRAP_HOST_SOCKET_FD=%d", hostSocketFd)) _ = conn.Close() } } } return &BootstrapConfig{ Args: args, Env: env, Fds: []int{execFd, hostNetFd}, ExecPath: fmt.Sprintf("/proc/self/fd/%d", execFd), }, nil } // BootstrapJoin joins the namespaces of the target PID and replaces the current process. func BootstrapJoin(targetPid int) (err error) { if IsIsolated() { return nil } config, err := PrepareBootstrapJoin(targetPid) if err != nil { return err } defer func() { for _, fd := range config.Fds { _ = syscall.Close(fd) } }() err = syscall.Exec(config.ExecPath, config.Args, config.Env) if err != nil { return fmt.Errorf("launcher exec failed: %w", err) } return nil } // PrepareBootstrapJoin calculates the environment and arguments needed to join a namespace. func PrepareBootstrapJoin(targetPid int) (*BootstrapConfig, error) { for i, arg := range os.Args { for j := 0; j < len(arg); j++ { if arg[j] == 0 { return nil, fmt.Errorf("argument %d contains null byte at position %d", i, j) } } } self, err := os.Executable() if err != nil { return nil, fmt.Errorf("failed to get executable path: %w", err) } execFd, err := prepareLauncher() if err != nil { return nil, err } if flags, err := unix.FcntlInt(uintptr(execFd), unix.F_GETFD, 0); err == nil { _, _ = unix.FcntlInt(uintptr(execFd), unix.F_SETFD, flags&^unix.FD_CLOEXEC) } 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 nil, fmt.Errorf("launcher argument %d contains null byte at position %d", i, j) } } } env := append(os.Environ(), fmt.Sprintf("WG_WRAP_JOIN_PID=%d", targetPid), "WG_WRAP_JOINED=1", ) return &BootstrapConfig{ Args: args, Env: env, Fds: []int{execFd}, ExecPath: fmt.Sprintf("/proc/self/fd/%d", execFd), }, nil } func prepareLauncher() (int, error) { // Use memfd_create to create an anonymous file in memory. // This bypasses the need for a temporary disk file and avoids noexec restrictions. fd, err := unix.MemfdCreate("wg-wrap-launcher", 0) if err != nil { return 0, fmt.Errorf("failed to create memfd: %w", err) } if _, err = unix.Write(fd, launcherBytes); err != nil { _ = unix.Close(fd) return 0, fmt.Errorf("failed to write launcher binary to memfd: %w", err) } return fd, nil }