summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-05-29 18:29:12 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-05-29 18:29:12 -0400
commitee2f5d545825752af63da36e2b9ec7a92985a875 (patch)
tree7328f73ac157dd19fa60e887fd243f0855935cce /internal/cli
parent135f6edbd9389bc4783f13c26aed0a74d3c8aca0 (diff)
feat: implement userspace wireguard data-path and unprivileged host fd-passing
- Implement complete rootless network namespace bootstrap via C launcher using unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET). - Resolve unprivileged network isolation blackhole via host-socket preservation (FD passing): open client UDP sockets on the host pre-isolation, clear O_CLOEXEC, and ingest them via custom `FDBind` inside the sandbox. - Implement isolated routing table automation over `tun0` (addresses, MTU, default routes). - Implement persistent, multi-process namespace sharing and joining using reference-counted PID files and the setns system call. - Write robust, self-contained E2E data plane test suites in `tests/e2e/e2e_test.go` using a mock UDP listener. - Update project documentation (`README.md` and `AGENTS.md`) to reflect completed milestones. - Ensure 100% test passing rate and zero lint/staticcheck warnings.
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/cli.go42
-rw-r--r--internal/cli/cli_test.go19
2 files changed, 59 insertions, 2 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 66b5f79..0876d08 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -11,6 +11,7 @@ import (
"git.theodohertyfamily.com/tools/wg-wrap/internal/config"
"git.theodohertyfamily.com/tools/wg-wrap/internal/namespace"
"git.theodohertyfamily.com/tools/wg-wrap/internal/paths"
+ "git.theodohertyfamily.com/tools/wg-wrap/internal/wireguard"
"git.theodohertyfamily.com/tools/wg-wrap/pkg/wgconf"
)
@@ -120,6 +121,16 @@ func (a *App) Run() error {
return a.ExecuteCommand(cfg)
}
+ // Before bootstrapping, see if an active namespace/process for the profile exists.
+ // If yes, we can join it!
+ pm := a.getPathManager()
+ joined, err := namespace.JoinExistingNamespace(pm, cfg.Profile)
+ if err == nil && joined {
+ // We have joined the active namespace (user, mnt, net).
+ // We can now execute the command immediately in this context!
+ return a.ExecuteCommand(cfg)
+ }
+
if err := namespace.Bootstrap(); err != nil {
return fmt.Errorf("bootstrap failed: %w", err)
}
@@ -154,7 +165,36 @@ func (a *App) ExecuteCommand(cfg *config.Config) error {
}()
fmt.Printf("Initializing WireGuard tunnel for profile %s...\n", cfg.Profile)
- // TODO: Integrate with internal/wireguard to set up TUN and WG-Go
+
+ // Parse the profile configuration
+ profilesDir := pm.ConfigDir()
+ profilePath := filepath.Join(profilesDir, cfg.Profile+".conf")
+
+ // Create tunnel if the file exists
+ if _, err := os.Stat(profilePath); err == nil {
+ wgCfg, err := wgconf.Parse(profilePath)
+ if err != nil {
+ return fmt.Errorf("failed to parse profile %s: %w", cfg.Profile, err)
+ }
+
+ // Start the WireGuard userspace device & routing table setup
+ tunnel, err := wireguard.StartTunnel(wgCfg)
+ if err != nil {
+ return fmt.Errorf("failed to start WireGuard tunnel: %w", err)
+ }
+ defer tunnel.Close()
+
+ // Pin the namespace so others can join it
+ if err := namespace.PinNamespace(pm, cfg.Profile); err != nil {
+ fmt.Printf("warning: failed to pin namespace: %v\n", err)
+ }
+ } else {
+ // If profile is not default or it was explicitly requested but doesn't exist, we error
+ if cfg.Profile != "default" {
+ return fmt.Errorf("profile %s not found: %w", cfg.Profile, err)
+ }
+ fmt.Printf("warning: default profile configuration not found. Executing command in bare isolation.\n")
+ }
cmd := exec.Command(cfg.Command[0], cfg.Command[1:]...)
cmd.Stdin = os.Stdin
diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go
index a0d6263..fcf489a 100644
--- a/internal/cli/cli_test.go
+++ b/internal/cli/cli_test.go
@@ -1,6 +1,8 @@
package cli
import (
+ "os"
+ "path/filepath"
"strings"
"testing"
)
@@ -10,6 +12,21 @@ func TestAppRun_ProfileDirInjection(t *testing.T) {
// Set up a temporary directory to simulate XDG_CONFIG_HOME/wg-wrap/profiles
tmpDir := t.TempDir()
+ // Write a valid test-vpn.conf profile file to the temporary directory
+ confContent := `[Interface]
+PrivateKey = YXNkZmFzZGZhc2RmYXNkZmFzZGZhc2RmYXNkZmFzZGY=
+Address = 10.0.0.2/24
+
+[Peer]
+PublicKey = YXNkZmFzZGZhc2RmYXNkZmFzZGZhc2RmYXNkZmFzZGY=
+Endpoint = 127.0.0.1:51820
+AllowedIPs = 10.0.0.0/24
+`
+ importPath := filepath.Join(tmpDir, "test-vpn.conf")
+ if err := os.WriteFile(importPath, []byte(confContent), 0644); err != nil {
+ t.Fatalf("failed to write test profile: %v", err)
+ }
+
tests := []struct {
name string
args []string
@@ -17,7 +34,7 @@ func TestAppRun_ProfileDirInjection(t *testing.T) {
}{
{
name: "valid profile with injected dir",
- args: []string{"wg-wrap", "--profile", "test-vpn", "curl", "google.com"},
+ args: []string{"wg-wrap", "--profile", "test-vpn", "true"},
wantErr: false,
},
}