summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-05-29 18:29:12 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-05-29 18:29:12 -0400
commitee2f5d545825752af63da36e2b9ec7a92985a875 (patch)
tree7328f73ac157dd19fa60e887fd243f0855935cce /README.md
parent135f6edbd9389bc4783f13c26aed0a74d3c8aca0 (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.md30
1 files changed, 14 insertions, 16 deletions
diff --git a/README.md b/README.md
index d0a7622..c6d572f 100644
--- a/README.md
+++ b/README.md
@@ -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