summaryrefslogtreecommitdiff
path: root/internal/namespace/namespace.go
blob: 1a09b78d901ce6e884fd10d598838dd1b331016f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package namespace

import (
	_ "embed"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"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"
}

// 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 {
	if IsIsolated() {
		return nil
	}

	self, err := os.Executable()
	if err != nil {
		return fmt.Errorf("failed to get executable path: %w", err)
	}

	// 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()

	// 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...]
	args := []string{self}
	args = append(args, os.Args[1:]...)

	// 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
}