From 756ba94292b408cc4f23d137b2c4c52009b2b38d Mon Sep 17 00:00:00 2001 From: James O'Doherty Date: Fri, 22 May 2026 09:13:16 -0400 Subject: Scaffold wg-wrap project structure and toolchain --- .gitignore | 27 ++++++++++++++ AGENTS.md | 70 ++++++++++++++++++++++++++++++++++++ cmd/wg-test-peer/main.go | 39 ++++++++++++++++++++ internal/config/config.go | 7 ++++ internal/namespace/namespace_test.go | 24 +++++++++++++ internal/wireguard/wireguard_test.go | 17 +++++++++ pkg/wgconf/wgconf_test.go | 15 ++++++++ tests/e2e/e2e_test.go | 31 ++++++++++++++++ 8 files changed, 230 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 cmd/wg-test-peer/main.go create mode 100644 internal/config/config.go create mode 100644 internal/namespace/namespace_test.go create mode 100644 internal/wireguard/wireguard_test.go create mode 100644 pkg/wgconf/wgconf_test.go create mode 100644 tests/e2e/e2e_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3c9fcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Binaries +bin/ +wg-wrap + +# Go build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test + +# Test coverage +coverage.out + +# OS generated files +.DS_Store +Thumbs.db + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo + +# Local config/profiles (Avoid committing test profiles) +.config/wg-wrap/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8bb175a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# wg-wrap Agent Guidelines + +This document defines the architectural conventions, project layout, and system assumptions for the development of `wg-wrap`. + +## Project Goals +`wg-wrap` is a transparent userspace VPN wrapper that allows native Linux processes to communicate over WireGuard without requiring host-level root privileges. It utilizes User and Network namespaces to isolate traffic and a userspace WireGuard implementation to handle encryption. + +## Project Layout +The project follows a modern Go project structure to ensure scalability and a clear separation between public APIs and internal implementation details. + +```text +. +├── cmd/ +│ └── wg-wrap/ # CLI Entry point. Handles flag parsing and subcommand routing. +├── internal/ +│ ├── config/ # Application-wide configuration types. +│ ├── namespace/ # Linux namespace management (unshare, setns, pinning). +│ └── wireguard/ # Userspace WireGuard controller and TUN device binding. +├── pkg/ +│ └── wgconf/ # WireGuard .conf parsing logic. Reusable library. +├── tests/ +│ └── e2e/ # End-to-End "Data Plane" tests using Virtual Peers. +├── go.mod # Module definition. +└── README.md # Project overview and design specification. +``` + +## Development Conventions + +### 1. Quality Assurance Pipeline +No piece of code is considered "done" until it has passed the full verification pipeline. Every implementation cycle must conclude with the following checks: + +1. **Formatting**: `go fmt ./...` +2. **Static Analysis**: `go vet ./...` +3. **Linting**: `golangci-lint run` +4. **Verification**: `go test` (relevant packages) + +If any of these tools report an error or warning, the code must be corrected before the task is marked as complete. + +### 2. Testing Strategy +We employ a three-tier testing approach to balance speed and reliability: + +| Tier | Location | Type | Scope | Requirement | +| :--- | :--- | :--- | :--- | :--- | +| **Unit** | Package dirs | `go test` | Pure logic (e.g., parsing, validation) | None | +| **Integration** | `internal/**/*_test.go` | `go test -tags=integration` | Syscalls, Namespace creation, Routing | Linux | +| **E2E** | `tests/e2e/` | `go test ./tests/e2e/...` | Full data path (Connect $\rightarrow$ Curl $\rightarrow$ Peer) | Linux + TUN access | + +### 2. Platform Constraints +- **Target OS**: Linux only. +- **Build Tags**: Use `//go:build linux` or `//go:build linux,integration` for any code interacting with `golang.org/x/sys/unix` or network namespaces. +- **MTU**: Always default the TUN device MTU to `1420` to account for WireGuard overhead. + +### 3. Namespace Lifecycle +- **Creation**: `CLONE_NEWUSER` $\rightarrow$ `CLONE_NEWNET`. +- **Persistence**: Namespaces are pinned by bind-mounting the namespace file to `/run/user/$UID/wg-wrap/profiles/`. +- **Cleanup**: The tool must monitor the wrapped process and ensure the namespace is unpinned/torn down via `wg-wrap profile stop` or upon process termination. + +## System Assumptions +The project assumes the target environment is a modern Linux system configured for rootless container operations (e.g., Podman is installed and functional): +- **User Namespaces**: `kernel.unprivileged_userns_clone=1` is assumed. +- **UID Mapping**: SubUIDs/SubGIDs are configured. +- **TUN Access**: The user has permission to access `/dev/net/tun`. +- **Tooling**: The `ip` command (iproute2) is available in the environment. + +## Roadmap Priority +1. **Configuration**: Implement robust `.conf` parsing in `pkg/wgconf`. +2. **Bootstrapping**: Implement the `unshare` and user-mapping flow in `internal/namespace`. +3. **Data Path**: Integrate `wireguard-go` with the TUN device in `internal/wireguard`. +4. **Routing**: Automate the isolated routing table setup. +5. **Lifecycle**: Implement namespace pinning and cleanup. diff --git a/cmd/wg-test-peer/main.go b/cmd/wg-test-peer/main.go new file mode 100644 index 0000000..938400d --- /dev/null +++ b/cmd/wg-test-peer/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + "net/http" +) + +type PeerConfig struct { + InternalIP string + ListenPort int + PrivateKey string +} + +func main() { + internalIP := flag.String("internal-ip", "10.0.0.1", "Internal VPN IP for the peer") + listenPort := flag.Int("port", 51820, "UDP port to listen on") + flag.Parse() + + fmt.Printf("Starting wg-test-peer on UDP port %d...\n", *listenPort) + fmt.Printf("Peer Internal IP: %s\n", *internalIP) + + // The peer will eventually implement a userspace WireGuard device + // and a simple TCP/IP stack (via GVisor or similar) to handle the traffic. + + // For now, we start a dummy HTTP server to demonstrate the intended flow. + // In a real E2E test, this server would be reached via the TUN device. + http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("token") + _, _ = fmt.Fprintf(w, "Verification token received: %s\n", token) + }) + + fmt.Printf("HTTP verification server running on :%s (simulated)\n", *internalIP) + // Note: In final implementation, the HTTP server binds to the internal IP + // provided by the userspace network stack. + + // This is a stub. In a real run, this would block. + fmt.Println("Peer is ready. Generate a matching .conf for the client.") +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..5aa8462 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,7 @@ +package config + +type Config struct { + Profile string + DNSServer string + Command []string +} diff --git a/internal/namespace/namespace_test.go b/internal/namespace/namespace_test.go new file mode 100644 index 0000000..cfa0e9b --- /dev/null +++ b/internal/namespace/namespace_test.go @@ -0,0 +1,24 @@ +//go:build linux && integration + +package namespace + +import ( + "testing" +) + +func TestNamespaceCreation(t *testing.T) { + t.Log("Integration Test: Verifying CLONE_NEWUSER and CLONE_NEWNET syscalls") + // TODO: Verify that unshare creates a new network namespace + // TODO: Verify that the process has root privileges inside the namespace +} + +func TestNamespacePinning(t *testing.T) { + t.Log("Integration Test: Verifying bind-mount of namespace to /run/user/$UID/wg-wrap/") + // TODO: Verify that the namespace survives after the process exits + // TODO: Verify that we can re-join the namespace via setns +} + +func TestRoutingSetup(t *testing.T) { + t.Log("Integration Test: Verifying TUN device creation and IP routing table setup") + // TODO: Mock 'ip' command or use netlink to verify route exists +} diff --git a/internal/wireguard/wireguard_test.go b/internal/wireguard/wireguard_test.go new file mode 100644 index 0000000..05e0fb7 --- /dev/null +++ b/internal/wireguard/wireguard_test.go @@ -0,0 +1,17 @@ +//go:build linux && integration + +package wireguard + +import ( + "testing" +) + +func TestWireGuardDeviceBinding(t *testing.T) { + t.Log("Integration Test: Verifying binding of userspace WG device to TUN device") + // TODO: Initialize a wg-go device and link it to a mock TUN +} + +func TestIpcSetConfiguration(t *testing.T) { + t.Log("Integration Test: Verifying IpcSet applies keys and endpoints correctly") + // TODO: Verify that configuration updates are reflected in the device state +} diff --git a/pkg/wgconf/wgconf_test.go b/pkg/wgconf/wgconf_test.go new file mode 100644 index 0000000..ccd3960 --- /dev/null +++ b/pkg/wgconf/wgconf_test.go @@ -0,0 +1,15 @@ +package wgconf + +import ( + "testing" +) + +func TestParseConfig(t *testing.T) { + t.Log("Unit Test: Verifying WireGuard .conf parsing logic") + // TODO: Implement test cases for valid/invalid configs, MTU, and DNS +} + +func TestValidateProfile(t *testing.T) { + t.Log("Unit Test: Verifying profile validation and path resolution") + // TODO: Implement test cases for ~/.config/wg-wrap/profiles/ resolution +} diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go new file mode 100644 index 0000000..5966659 --- /dev/null +++ b/tests/e2e/e2e_test.go @@ -0,0 +1,31 @@ +package e2e + +import ( + "testing" +) + +func TestDataPlaneConnectivity(t *testing.T) { + t.Log("E2E Test: Virtual Peer connectivity check") + // 1. Spin up a Virtual Peer (GVisor-based userspace stack) + // 2. Generate a matching .conf profile + // 3. Run `wg-wrap --profile test curl ` + // 4. Verify HTTP response is received +} + +func TestNetworkIsolation(t *testing.T) { + t.Log("E2E Test: Verifying host isolation") + // 1. Ensure host cannot ping the Virtual Peer's internal IP + // 2. Ensure wrapped process CAN ping the Virtual Peer's internal IP +} + +func TestDNSLeakage(t *testing.T) { + t.Log("E2E Test: Verifying DNS is routed via VPN") + // 1. Run `wg-wrap --profile test dig ` + // 2. Verify that the DNS query goes to the VPN DNS server, not host resolver +} + +func TestMTUFragmentation(t *testing.T) { + t.Log("E2E Test: Verifying MTU 1420 prevents packet drop") + // 1. Send large pings (-s 1400) through the tunnel + // 2. Verify packets are received without fragmentation errors +} -- cgit v1.2.3