diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-05-22 10:22:40 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-05-22 10:22:40 -0400 |
| commit | 401683a6b11e5a7810c949147a12f2c4bbfba48a (patch) | |
| tree | 30e411bf7eefcb91e5f17921d42b63ba2f3a3fb5 | |
| parent | 5dbc46f3c1c75bf922bcc1c3df342323c23c04ce (diff) | |
feat: add argument verification diagnostic and secure temp files for launcher
| -rw-r--r-- | AGENTS.md | 7 | ||||
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | internal/cli/cli.go | 24 | ||||
| -rw-r--r-- | internal/namespace/namespace.go | 40 |
4 files changed, 61 insertions, 25 deletions
@@ -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`. @@ -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...] |
