summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-05-22 09:13:16 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-05-22 09:13:16 -0400
commit756ba94292b408cc4f23d137b2c4c52009b2b38d (patch)
tree85ed0158eef7009826c5707e976538784f10f1d8
parenta4cd7de209fe90006b3e6e67c69dea5ed0c9f832 (diff)
Scaffold wg-wrap project structure and toolchain
-rw-r--r--.gitignore27
-rw-r--r--AGENTS.md70
-rw-r--r--cmd/wg-test-peer/main.go39
-rw-r--r--internal/config/config.go7
-rw-r--r--internal/namespace/namespace_test.go24
-rw-r--r--internal/wireguard/wireguard_test.go17
-rw-r--r--pkg/wgconf/wgconf_test.go15
-rw-r--r--tests/e2e/e2e_test.go31
8 files changed, 230 insertions, 0 deletions
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/<name>`.
+- **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 <virtual-peer-ip>`
+ // 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 <domain>`
+ // 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
+}