summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-05-22 10:22:40 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-05-22 10:22:40 -0400
commit401683a6b11e5a7810c949147a12f2c4bbfba48a (patch)
tree30e411bf7eefcb91e5f17921d42b63ba2f3a3fb5
parent5dbc46f3c1c75bf922bcc1c3df342323c23c04ce (diff)
feat: add argument verification diagnostic and secure temp files for launcher
-rw-r--r--AGENTS.md7
-rw-r--r--README.md15
-rw-r--r--internal/cli/cli.go24
-rw-r--r--internal/namespace/namespace.go40
4 files changed, 61 insertions, 25 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 8886c0b..f9e085d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -49,9 +49,14 @@ To maintain a high-velocity development cycle without sacrificing correctness, w
- **Parallelism**: Use `t.Parallel()` in integration and E2E tests. Use `t.TempDir()` to ensure resource isolation.
- **Granular Timeouts**: All system calls, network operations, and external command executions must be wrapped in a `context.WithTimeout` (typically 2-5 seconds) to prevent hanging tests.
- **Interface Mocking**: Use interfaces for "heavy" system operations (e.g., routing, namespace creation) to allow fast unit testing of logic via mocks, reserving real syscalls for the integration tier.
- - **Shared Fixtures**: Use `sync.Once` or `TestMain` for expensive setup (e.g., Virtual Peer) to avoid redundant boot-ups across tests.
+ ### 2. Testing & Stubbing Conventions
+...
+- **Interface Mocking**: Use interfaces for "heavy" system operations (e.g., routing, namespace creation) to allow fast unit testing of logic via mocks, reserving real syscalls for the integration tier.
+- **Shared Fixtures**: Use `sync.Once` or `TestMain` for expensive setup (e.g., Virtual Peer) to avoid redundant boot-ups across tests.
+- **Argument Integrity**: To prevent shell injection and argument splitting, never concatenate arguments into a single string for execution. Always use "vector-based" execution (e.g., `os/exec.Command` in Go or `execv`/`execvp` in C) to ensure that arguments containing spaces or special characters are preserved as discrete literals.
### 3. Platform Compatibility & Build Constraints
+...
`wg-wrap` is fundamentally a Linux system tool. To ensure the module remains compilable on other platforms while restricting Linux-specific syscalls, we use the following patterns:
- **Build Tags**: All files interacting with `golang.org/x/sys/unix`, network namespaces, or TUN devices must start with `//go:build linux`.
diff --git a/README.md b/README.md
index 1b09bb7..0c493cb 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,20 @@
# wg-wrap: Transparent Userspace VPN Wrapper
## Overview
-wg-wrap is a design for 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 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.
+
+### 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+)
+
+**Build Instructions:**
+```bash
+make
+```
+This will compile the C launcher and embed it into the final `wg-wrap` binary.
## Profile Management
To simplify usage, `wg-wrap` implements a profile system for managing WireGuard configurations.
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 6118ee5..b315fba 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -18,19 +18,19 @@ func NewApp(args []string) *App {
}
func (a *App) Run() error {
- // 1. Ensure we are in an isolated network namespace
- if err := namespace.Bootstrap(); err != nil {
- return fmt.Errorf("namespace bootstrap failed: %w", err)
- }
-
- // Handle the internal diagnostic command first
- if len(a.Args) > 1 && a.Args[1] == "test-ns" {
- ok, msg := namespace.VerifyIsolation()
- if !ok {
- return fmt.Errorf("isolation check failed: %s", msg)
+ // Handle the internal diagnostic commands first
+ if len(a.Args) > 1 {
+ switch a.Args[1] {
+ case "test-ns":
+ ok, msg := namespace.VerifyIsolation()
+ if !ok {
+ return fmt.Errorf("isolation check failed: %s", msg)
+ }
+ fmt.Println("Isolation Verified: OK")
+ return nil
+ case "test-args":
+ return namespace.VerifyArguments(a.Args)
}
- fmt.Println("Isolation Verified: OK")
- return nil
}
// Handle subcommands first (profile list, import, configure, delete, stop)
diff --git a/internal/namespace/namespace.go b/internal/namespace/namespace.go
index 5e31b9d..1a09b78 100644
--- a/internal/namespace/namespace.go
+++ b/internal/namespace/namespace.go
@@ -2,10 +2,10 @@ package namespace
import (
_ "embed"
+ "encoding/json"
"fmt"
"os"
"os/exec"
- "path/filepath"
"syscall"
)
@@ -55,6 +55,18 @@ func VerifyIsolation() (bool, string) {
return true, "Isolated and root"
}
+// VerifyArguments prints the current process arguments as a JSON array.
+// This is used for E2E testing to verify that argument splitting and
+// shell injection are not occurring during the bootstrap loop.
+func VerifyArguments(args []string) error {
+ out, err := json.Marshal(args)
+ if err != nil {
+ return fmt.Errorf("failed to marshal arguments: %w", err)
+ }
+ fmt.Println(string(out))
+ return nil
+}
+
// Bootstrap ensures the process is running in an isolated user and network namespace.
// It writes the embedded C launcher to a temporary file and replaces the current process.
func Bootstrap() error {
@@ -67,20 +79,26 @@ func Bootstrap() error {
return fmt.Errorf("failed to get executable path: %w", err)
}
- // 1. Determine a secure location for the launcher binary.
- // We use /run/user/$UID if available, otherwise /tmp.
- tmpDir := os.Getenv("XDG_RUNTIME_DIR")
- if tmpDir == "" {
- tmpDir = os.TempDir()
+ // 1. Create a secure temporary file for the launcher binary.
+ // os.CreateTemp ensures a unique, unpredictable filename and restrictive permissions.
+ tmpFile, err := os.CreateTemp("", "wg-wrap-launcher-")
+ if err != nil {
+ return fmt.Errorf("failed to create temp launcher file: %w", err)
}
+ launcherPath := tmpFile.Name()
- launcherPath := filepath.Join(tmpDir, "wg-wrap-launcher")
-
- // 2. Write the embedded launcher binary to disk.
- // We use 0700 permissions to ensure only the current user can execute it.
- if err := os.WriteFile(launcherPath, launcherBytes, 0700); err != nil {
+ // 2. Write the embedded launcher binary to the temp file.
+ if _, err := tmpFile.Write(launcherBytes); err != nil {
+ tmpFile.Close()
return fmt.Errorf("failed to write launcher binary: %w", err)
}
+
+ // Ensure the binary is executable (0700)
+ if err := tmpFile.Chmod(0700); err != nil {
+ tmpFile.Close()
+ return fmt.Errorf("failed to set launcher permissions: %w", err)
+ }
+ tmpFile.Close()
// 3. Prepare arguments for the launcher.
// The launcher expects: launcher <command_to_run> [args...]