summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-06-04 22:38:44 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-06-04 22:38:44 -0400
commit66b782e261f1cd928ad6a8482788a65fb484db45 (patch)
tree38b6c46200d9c4464affc1c0c43494a5555acf33 /README.md
parentc53503b52b6fc6de37b6053719521054003fa50b (diff)
refactor: simplify architecture and improve documentation
- Extract orchestration logic from `internal/cli` into a new `internal/manager` package for better composability. - Migrate technical implementation details from README.md to package-level godoc strings. - Rewrite README.md to be more user-centric, focusing on quick start and usage. - Add comprehensive documentation for exported structs and fields across the project. - Verify all changes with `go fmt`, `go vet`, `golangci-lint`, and full E2E test suite.
Diffstat (limited to 'README.md')
-rw-r--r--README.md236
1 files changed, 79 insertions, 157 deletions
diff --git a/README.md b/README.md
index c648168..ee4ea6a 100644
--- a/README.md
+++ b/README.md
@@ -1,169 +1,91 @@
# wg-wrap: Transparent Userspace VPN Wrapper
-## Overview
-wg-wrap is a tool that allows a native Linux process to communicate over a WireGuard VPN without requiring the host kernel to manage the VPN tunnel or requiring global root privileges. It achieves this by bridging a Linux network namespace's TUN device directly to a userspace WireGuard implementation.
+`wg-wrap` allows you to run specific Linux applications over a WireGuard VPN without requiring root privileges or affecting your entire system's network configuration.
-### Building from Source
-Because `wg-wrap` uses an embedded C launcher to handle rootless namespace transitions, it cannot be built using `go build` alone. You must use the provided Makefile.
-
-**Requirements:**
-- `gcc`
-- `go` (1.23+)
+## 🚀 Quick Start
-**Build Instructions:**
+### 1. Install
+Build the binary using the provided Makefile:
```bash
make
```
-This will compile the C launcher and embed it into the final `wg-wrap` binary.
-
-**Testing the Project:**
-The project uses a `Makefile` to orchestrate building and testing.
-- **Standard Tests**: Run the unit and integration tests:
- ```bash
- make test
- ```
-- **Security Fuzzing**: Run the 8-bit clean argument integrity fuzzer:
- ```bash
- make fuzz
- ```
- You can adjust the fuzzer's parallelism and duration:
- ```bash
- FUZZ_PARALLEL=4 FUZZ_TIME=1h make fuzz
- ```
-
-## Profile Management
-To simplify usage, `wg-wrap` implements a profile system for managing WireGuard configurations.
-- **Storage**: Profiles are stored as standard `.conf` files in `~/.config/wg-wrap/profiles/`.
-- **Selection**: Users can reference a profile by its filename (without the extension).
- - Example: `wg-wrap --profile home-vpn curl google.com` (looks for `~/.config/wg-wrap/profiles/home-vpn.conf`).
-- **Command Separation**: Supports the `--` delimiter to explicitly separate `wg-wrap` flags from the target command.
- - Example: `wg-wrap --profile home-vpn -- curl --option-that-looks-like-a-flag`
-- **Default**: An optional `default.conf` in the same directory can be used if no profile is specified.
-
-### Profile Management Commands
-Beyond wrapping commands, `wg-wrap` provides management sub-commands to handle profiles:
-- `wg-wrap profile list`: Lists all available profiles in the config directory.
-- `wg-wrap profile import <path> [name]`: Imports a `.conf` file into the profiles directory. If `[name]` is not provided, the profile name is derived from the `.conf` filename. Otherwise, the specified custom name is used.
-- `wg-wrap profile configure <name>`: Opens the selected profile in the system's default editor.
-- `wg-wrap profile delete <name>`: Removes the specified profile.
-- `wg-wrap profile stop <name>`: Stops the tunnel/namespace associated with the specified profile and unpins it.
+
+### 2. Setup a Profile
+Import your WireGuard `.conf` file as a profile:
+```bash
+./wg-wrap profile import ~/my-vpn.conf home-vpn
+```
+
+### 3. Run an Application
+Run any command wrapped in the VPN:
+```bash
+./wg-wrap --profile home-vpn -- curl https://ifconfig.me
+```
+*Only the `curl` command is routed through the VPN; your browser, SSH sessions, and other apps remain on your local network.*
+
+---
+
+## 🛠️ Feature Overview
+
+### Process-Level Isolation
+Unlike standard VPNs, `wg-wrap` provides complete network isolation per process. This means:
+- **No Route Pollution**: Your host routing table remains untouched.
+- **Rootless Operation**: Works without `sudo` using unprivileged user namespaces.
+- **VPN Concurrency**: Run multiple different VPN profiles at the same time in different terminals.
+- **Zero-Leak DNS**: Each process gets its own isolated DNS resolver, preventing leaks to your ISP.
+
+### Profile Management
+Manage your VPN configurations easily from the CLI:
+
+| Command | Description |
+| :--- | :--- |
+| `profile list` | List all available VPN profiles. |
+| `profile import <path> [name]` | Import a `.conf` file as a new profile. |
+| `profile configure <name>` | Edit a profile's configuration in your default editor. |
+| `profile delete <name>` | Remove a profile. |
+| `profile stop <name>` | Force-stop an active tunnel session. |
### Diagnostics
-For debugging and environment verification, `wg-wrap` provides diagnostic tools:
-- `wg-wrap show-config [--profile <name>]`: Displays the current runtime configuration, including resolved paths for the runtime base directory and PID tracking, and verifies whether the process is currently isolated.
-- `wg-wrap test-ns`: Verifies that the process is correctly isolated in a rootless network namespace.
-- `wg-wrap test-args`: Outputs the current process arguments as hex-encoded strings to verify argument integrity through the bootstrap loop.
-
-
-## The Core Architecture
-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 & 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. **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 session-based 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
-
-#### Network Namespace Manager
-- **Isolation**: Uses `unshare -r -n` to create a new network namespace. This maps the current user to root within the namespace, allowing the creation of network interfaces without host-level root access.
-- **Interface Setup**: Creates a virtual TUN device (e.g., `tun0`).
-- **Kernel Routing**: Configures the Linux kernel's routing table inside the namespace to ensure the target traffic is directed into the TUN device.
- - `ip addr add <internal-ip> dev tun0`
- - `ip link set tun0 up`
- - `ip route add <vpn-subnet> dev tun0` (or a default route `0.0.0.0/0`).
-
-#### Userspace WireGuard Controller
-- **Engine**: Utilizes a userspace WireGuard implementation.
- - Recommended: `golang.zx2c4.com/wireguard` (the official Go implementation).
- - For advanced network stack isolation or userspace TCP/IP, consider `gvisor.dev/gvisor` (specifically `pkg/tcpip` for its `NetworkDispatcher` and `PacketBuffer` logic).
-- **Binding**: The WireGuard device is bound directly to the TUN device.
- - Use `golang.zx2c4.com/wireguard/tun` to interact with the Linux TUN device.
- - Consider implementing a bridge similar to a userspace network stack to connect GVisor's network stack to the TUN device.
-- **Configuration**: Parses standard WireGuard `.conf` files.
- - **Implementation Note**: Use the `IpcSet` method of the `device.Device` to apply keys and endpoints. This avoids complex internal state management and is the standard way to interact with the userspace engine.
- - Profiles must resolve to: Local Private Key, Remote Peer Public Keys, Remote Endpoints (UDP IP:Port), and Allowed IPs.
-
-
-
-#### Execution Wrapper
-- **Bootstrap**: Orchestrates the namespace creation and WireGuard initialization.
-- **Command Execution**: Uses `exec` to replace the bootstrap process with the user's requested command (e.g., `curl`, `ssh`), ensuring the command runs within the configured network environment.
-
-## Data Flow Detail
-1. **Egress**: A process (e.g., `curl`) sends a packet to a remote IP. The Linux kernel sees the route via `tun0` and writes the raw IP packet to the TUN device. The userspace WireGuard device reads this packet, encrypts it according to the configured peer, and sends it as a UDP packet to the remote endpoint.
-2. **Ingress**: A UDP packet arrives at the local port. The userspace WireGuard device decrypts it and writes the resulting raw IP packet back into the TUN device. The Linux kernel receives this packet and delivers it to the waiting application.
-
-## Comparison: Userspace vs. Kernel VPN
-| Feature | Kernel WireGuard | WGWRAP (Userspace) |
-| :--- | :--- | :--- |
-| **Privileges** | Requires `CAP_NET_ADMIN` (Root) | No root required (`unshare`) |
-| **Deployment** | Requires Kernel Module | Standalone Binary |
-| **Isolation** | Global | Per-process/Namespace |
-| **Routing** | Host Routing Table | Isolated Namespace Table |
+Check your environment and verify isolation:
+- `show-config`: View resolved paths and current isolation status.
+- `test-ns`: Verify that you are correctly isolated in a network namespace.
+- `test-args`: (For developers) Verify 8-bit clean argument passing.
+
+---
+
+## 📖 Usage Examples
+
+**Run Firefox on a specific VPN:**
+```bash
+./wg-wrap --profile privacy-vpn -- firefox
+```
+
+**Run a series of tests against a private VPC:**
+```bash
+./wg-wrap --profile dev-vpc -- pytest tests/integration
+```
+
+**Connect to a home server and a work server simultaneously:**
+```bash
+# Terminal 1
+./wg-wrap --profile home-vpn -- ssh home-nas
+# Terminal 2
+./wg-wrap --profile work-vpn -- ssh work-server
+```
+
+---
+
+## 🏗️ Development
+
+### Building from Source
+`wg-wrap` requires `gcc` and `go` (1.23+). It uses an embedded C launcher to handle the rootless namespace transition, so you must use the Makefile:
+```bash
+make
+```
+
+### Testing
+- **Unit & Integration Tests**: `make test`
+- **Security Fuzzing**: `make fuzz` (tests argument integrity through the bootstrap loop).
## License
This project is free and unencumbered software released into the public domain. See the [LICENSE](LICENSE) file for details.
-
-## Technical Gotchas & Implementation Details
-
-### 1. MTU Management
-WireGuard adds overhead. If the TUN device is set to 1500, encrypted packets may be dropped.
-- **Action**: Set the TUN device MTU to `1420` bytes to prevent fragmentation/drops.
-
-### 2. DNS Handling
-Routing traffic to the VPN doesn't guarantee DNS is routed.
-- **Action**: The wrapper should update `/etc/resolv.conf` within the namespace or use the GVisor stack to intercept and redirect DNS queries to the VPN's designated DNS server.
-- **Fallback Strategy**: **Never use the host's DNS servers**, as this creates DNS leaks and undermines the isolation of the network namespace. If no DNS server is specified in the WireGuard config:
- - **Action**: Fall back to trusted, encrypted-capable public providers (e.g., Cloudflare `1.1.1.1` or Google `8.8.8.8`) via the VPN tunnel.
- - **User Control**: Provide a flag (e.g., `--dns-server <IP>`) to allow the user to override the fallback and specify their own trusted resolver.
-
-### 3. Namespace Lifecycle
-Network namespaces can leak if not managed. To prevent this, `wg-wrap` implements a "last-man-out" reference counting system:
-- **Tracking**: Every process using a profile creates a PID file in `/run/user/$UID/wg-wrap/profiles/<name>/pids/`.
-- **Automatic Cleanup**: When a process exits, it removes its PID file. If no PID files remain for a profile, `wg-wrap` automatically unpins the namespace and terminates the associated userspace WireGuard process.
-- **Resilience**: Stale PID files (from crashed processes) are pruned during the initial join sequence of any new process.
-- **Manual Override**: The controller also provides `wg-wrap profile stop <name>` to force the immediate teardown of a profile's namespace.
-
-### 4. User Namespace Sequence
-To create a network namespace without root, you must create a user namespace first.
-- **Sequence**: `CLONE_NEWUSER` $\rightarrow$ `CLONE_NEWNET` $\rightarrow$ `Setuid/Setgid` $\rightarrow$ Configure Interfaces.
-
-## Testing Strategy
-
-To ensure the correctness of the network isolation and the stability of the userspace tunnel, the following testing approach will be used:
-
-### 1. Component Testing (Unit/Integration)
-- **Config Parser**: Test against a variety of WireGuard `.conf` files (edge cases: missing fields, invalid keys, extreme MTUs).
-- **Profile Manager**: Verify that import/delete/edit operations correctly manipulate `~/.config/wg-wrap/profiles/`.
-- **Namespace Logic**: Verify that `unshare` and `setns` calls correctly transition the process into the target namespace.
-
-### 2. Functional Testing (The "Data Plane" Test)
-To avoid dependence on external infrastructure, we will implement a "Virtual Peer" test harness. This involves creating a standalone Go process that acts as the VPN server using a userspace network stack (e.g., GVisor).
-- **Self-Contained Peer**: The test peer will instantiate its own userspace WireGuard device and an embedded TCP/IP stack to host virtual services (e.g., an HTTP server) on a private VPN IP.
-- **Dynamic Config Generation**: The harness will generate a matching WireGuard `.conf` profile on the fly, allowing `wg-wrap` to connect to the virtual peer.
-- **Connectivity Test**: `wg-wrap --profile test curl <virtual-peer-ip>` should successfully retrieve data from the embedded service.
-- **Isolation Test**: `curl` executed *without* `wg-wrap` should not be able to reach the virtual peer's internal IP.
-- **DNS Leak Test**: Use `dig` or `nslookup` to verify that DNS queries are routed through the VPN and not the host's resolver.
-- **MTU Test**: Send large packets (e.g., using `ping -s 1400`) to ensure no fragmentation or packet loss occurs.
-
-### 3. Persistence & Concurrency Testing
-- **Shared Namespace Test**: Start one command with a profile, then start another. Verify that both share the same internal IP address.
-- **Lifecycle Test**: Terminate the first process and verify that the second process maintains connectivity (proving the namespace is pinned).
-- **Cleanup Test**: Implement a `wg-wrap profile stop <name>` command to unpin the namespace and kill the associated userspace WG process.
-
-### 4. Environment Testing
-- **Rootless Verification**: Run the entire suite as a non-privileged user to ensure no `CAP_NET_ADMIN` requirements are accidentally introduced.
-- **Kernel Compatibility**: Test on multiple Linux kernels to ensure `CLONE_NEWUSER` and `CLONE_NEWNET` behave consistently.