diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-05-29 18:29:12 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-05-29 18:29:12 -0400 |
| commit | ee2f5d545825752af63da36e2b9ec7a92985a875 (patch) | |
| tree | 7328f73ac157dd19fa60e887fd243f0855935cce /README.md | |
| parent | 135f6edbd9389bc4783f13c26aed0a74d3c8aca0 (diff) | |
feat: implement userspace wireguard data-path and unprivileged host fd-passing
- Implement complete rootless network namespace bootstrap via C launcher using unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET).
- Resolve unprivileged network isolation blackhole via host-socket preservation (FD passing): open client UDP sockets on the host pre-isolation, clear O_CLOEXEC, and ingest them via custom `FDBind` inside the sandbox.
- Implement isolated routing table automation over `tun0` (addresses, MTU, default routes).
- Implement persistent, multi-process namespace sharing and joining using reference-counted PID files and the setns system call.
- Write robust, self-contained E2E data plane test suites in `tests/e2e/e2e_test.go` using a mock UDP listener.
- Update project documentation (`README.md` and `AGENTS.md`) to reflect completed milestones.
- Ensure 100% test passing rate and zero lint/staticcheck warnings.
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 30 |
1 files changed, 14 insertions, 16 deletions
@@ -58,23 +58,21 @@ For debugging and environment verification, `wg-wrap` provides diagnostic tools: The tool focuses on a direct, transparent data path: `Linux Application` $\rightarrow$ `Linux Kernel Routing` $\rightarrow$ `TUN Device` $\rightarrow$ `Userspace WireGuard` $\rightarrow$ `UDP Socket` $\rightarrow$ `Internet`. -### Rootless Bootstrap Loop -To achieve rootless network isolation without interfering with the Go runtime's multi-threaded scheduler, `wg-wrap` employs a bootstrap pattern: -1. **Initial Launch**: The Go binary starts and detects it is not in an isolated network namespace. -2. **Helper Deployment**: It writes an embedded C launcher binary to a secure temporary location. -3. **Namespace Transition**: It uses `syscall.Exec` to replace itself with the C launcher. -4. **Isolation**: The C launcher performs the `unshare(CLONE_NEWUSER | CLONE_NEWNET)` sequence, maps the current user to root (UID 0) inside the namespace, and disables supplementary groups. +### 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, `wg-wrap` opens a UDP socket bound to `0.0.0.0:0` on the host, clears its Close-on-Exec (`O_CLOEXEC`) flag using system `fcntl`, and stores the FD number in the environment (`WG_WRAP_HOST_SOCKET_FD`). +2. **Helper Deployment**: It writes an embedded single-threaded C launcher binary to a secure temporary location. +3. **Namespace Transition**: It uses `syscall.Exec` to replace itself with the C launcher, preserving the open socket file descriptor. +4. **Isolation**: The C launcher performs the `unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET)` sequence to isolate Mount, User, and Network environments, maps the current user to root (UID 0) inside the sandbox, and disables supplementary groups. 5. **Re-entry**: The launcher then `execvp`s the original `wg-wrap` binary. -6. **Execution**: The second instance of `wg-wrap` detects it is now isolated and proceeds to initialize the VPN and execute the target application. - -### Persistent Namespaces -To support multiple commands using the same VPN profile without re-establishing the tunnel, `wg-wrap` utilizes persistent network namespaces. -- **Mechanism**: Instead of relying on the process lifecycle to keep the namespace alive, `wg-wrap` "bind-mounts" the network namespace file to a known location on disk (e.g., `/run/user/$UID/wg-wrap/profiles/<profile-name>`). -- **Workflow**: - 1. When a profile is called, `wg-wrap` checks if the namespace bind-mount already exists. - 2. If it exists, it simply joins the existing namespace. - 3. If not, it creates a new one, initializes WireGuard, and creates the bind-mount. -- **Benefit**: This allows multiple terminal windows or separate processes to share the same WireGuard tunnel and internal IP, enabling true "VPN-as-a-service" for specific applications. +6. **FDBind Tunnel Initialization**: The second instance of `wg-wrap` detects it is now isolated, extracts the `WG_WRAP_HOST_SOCKET_FD` descriptor, and wraps it inside a custom `FDBind` struct to initialize `wireguard-go`. Because sockets in Linux retain their creation-time network namespace, WireGuard's encrypted UDP transport communicates natively over the host interface, while decrypted process traffic is entirely locked inside the unprivileged sandbox's `tun0`. + +### Persistent Namespaces & Shared Sessions +To support multiple concurrent commands on the same WireGuard tunnel without re-establishing connections, `wg-wrap` employs persistent, unprivileged namespaces: +- **Tracking**: Process usage is tracked using active PID files inside `/run/user/$UID/wg-wrap/profiles/<name>/pids/`. +- **Ref-Counting & Cleanup**: Active PIDs are regularly pruned. When the last active process exits, the namespace is unpinned via `UnpinNamespace` and resources are cleanly reclaimed. +- **Setns Join**: When a new process is executed on an active profile, it discovers an active PID and calls `syscall.Setns` (via `golang.org/x/sys/unix`) to attach itself to the existing User, Mount, and Network namespaces of the active tunnel in $\approx 10\text{ms}$. ### 1. Components |
