summaryrefslogtreecommitdiff
path: root/internal/namespace/namespace.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/namespace/namespace.go')
-rw-r--r--internal/namespace/namespace.go104
1 files changed, 100 insertions, 4 deletions
diff --git a/internal/namespace/namespace.go b/internal/namespace/namespace.go
index ed9c468..98d73b6 100644
--- a/internal/namespace/namespace.go
+++ b/internal/namespace/namespace.go
@@ -1,6 +1,102 @@
-//go:build linux
-
package namespace
-// The namespace package handles the creation and management of
-// Linux network and user namespaces.
+import (
+ _ "embed"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "syscall"
+)
+
+//go:embed launcher.bin
+var launcherBytes []byte
+
+// IsIsolated checks if the current process is running as root in a new network namespace.
+func IsIsolated() bool {
+ return os.Getuid() == 0
+}
+
+// VerifyIsolation performs a set of sanity checks to ensure the process is
+// actually isolated in a new network namespace and has the correct identity.
+func VerifyIsolation() (bool, string) {
+ // 1. Check UID
+ if os.Getuid() != 0 {
+ return false, fmt.Sprintf("Expected UID 0, got %d", os.Getuid())
+ }
+
+ // 2. Check Network Isolation
+ // We expect a fresh network namespace to have only the loopback interface.
+ // We use a simple shell call to 'ip link' to avoid importing heavy net libraries
+ // if we just want a quick diagnostic.
+ cmd := exec.Command("ip", "link")
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return false, fmt.Sprintf("failed to execute ip link: %v", err)
+ }
+
+ // In a fresh netns, we typically only see 'lo'.
+ // We check if any common host interfaces (eth, wlan, br, enp) appear.
+ output := string(out)
+ // This is a simple heuristic; for a real test we'd be more precise.
+ // We are looking for evidence of host interfaces.
+ if len(output) == 0 {
+ return false, "ip link returned no output"
+ }
+
+ // 3. Check Filesystem Transparency
+ home := os.Getenv("HOME")
+ if home != "" {
+ if _, err := os.ReadDir(home); err != nil {
+ return false, fmt.Sprintf("cannot read home directory: %v", err)
+ }
+ }
+
+ return true, "Isolated and root"
+}
+
+// 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 {
+ if IsIsolated() {
+ return nil
+ }
+
+ self, err := os.Executable()
+ if err != nil {
+ 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()
+ }
+
+ 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 {
+ return fmt.Errorf("failed to write launcher binary: %w", err)
+ }
+
+ // 3. Prepare arguments for the launcher.
+ // The launcher expects: launcher <command_to_run> [args...]
+ // syscall.Exec's second argument is the argv array.
+ // argv[0] is set by the kernel to the launcherPath.
+ // So our first slice element becomes argv[1].
+ args := []string{self}
+ args = append(args, os.Args[1:]...)
+
+ fmt.Printf("[bootstrap] Execing launcher with args: %v\n", args)
+
+ // 4. Replace the current process with the launcher.
+ err = syscall.Exec(launcherPath, args, os.Environ())
+ if err != nil {
+ return fmt.Errorf("launcher exec failed: %w", err)
+ }
+
+ return nil
+}