summaryrefslogtreecommitdiff
path: root/internal/namespace/preflight.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/namespace/preflight.go')
-rw-r--r--internal/namespace/preflight.go156
1 files changed, 156 insertions, 0 deletions
diff --git a/internal/namespace/preflight.go b/internal/namespace/preflight.go
new file mode 100644
index 0000000..2db44ab
--- /dev/null
+++ b/internal/namespace/preflight.go
@@ -0,0 +1,156 @@
+package namespace
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "os/user"
+ "strings"
+)
+
+// Requirement defines a system check and the hint to provide if it fails.
+type Requirement struct {
+ Name string
+ Check func() (bool, string)
+ Hint string
+}
+
+// Preflight handles system requirement checks and health diagnostics.
+type Preflight struct {
+ FS FileSystem
+}
+
+// NewPreflight creates a new preflight checker with the provided filesystem.
+func NewPreflight(fs FileSystem) *Preflight {
+ return &Preflight{FS: fs}
+}
+
+// CheckSystemRequirements verifies the absolute minimum requirements
+// that must be met for wg-wrap to function.
+func (p *Preflight) CheckSystemRequirements() error {
+ // The only absolute hard requirement that isn't better caught by a syscall
+ // is the existence and accessibility of the TUN device.
+ if _, err := p.FS.Stat("/dev/net/tun"); os.IsNotExist(err) {
+ return fmt.Errorf("critical requirement missing: /dev/net/tun not found. Please ensure the tun module is loaded (sudo modprobe tun)")
+ }
+
+ f, err := p.FS.Open("/dev/net/tun")
+ if err != nil {
+ return fmt.Errorf("critical requirement missing: /dev/net/tun is not accessible: %v", err)
+ }
+ _ = f.Close()
+
+ return nil
+}
+
+// CheckSystemRequirements is the global wrapper for the preflight check.
+func CheckSystemRequirements() error {
+ return (&Preflight{FS: DefaultFS}).CheckSystemRequirements()
+}
+
+// RunHealthCheck performs a comprehensive set of diagnostics to help users
+// configure their system for rootless network namespaces.
+func (p *Preflight) RunHealthCheck(uid int, username string) []HealthResult {
+ isRoot := uid == 0
+
+ checks := []Requirement{
+ {
+ Name: "TUN Device Accessibility",
+ Check: func() (bool, string) {
+ if _, err := p.FS.Stat("/dev/net/tun"); os.IsNotExist(err) {
+ return false, "/dev/net/tun does not exist"
+ }
+ f, err := p.FS.Open("/dev/net/tun")
+ if err != nil {
+ return false, fmt.Sprintf("cannot open /dev/net/tun: %v", err)
+ }
+ _ = f.Close()
+ return true, ""
+ },
+ Hint: "Ensure the TUN module is loaded (sudo modprobe tun) and that your user has permissions to access /dev/net/tun",
+ },
+ {
+ Name: "Unprivileged User Namespaces (Debian/Ubuntu)",
+ Check: func() (bool, string) {
+ data, err := p.FS.ReadFile("/proc/sys/kernel/unprivileged_userns_clone")
+ if err != nil {
+ return true, "Not applicable (file not found)"
+ }
+ if strings.TrimSpace(string(data)) == "0" {
+ return false, "unprivileged_userns_clone is disabled"
+ }
+ return true, ""
+ },
+ Hint: "Enable unprivileged user namespaces by running: sudo sysctl -w kernel.unprivileged_userns_clone=1",
+ },
+ {
+ Name: "User Namespace Limit",
+ Check: func() (bool, string) {
+ data, err := p.FS.ReadFile("/proc/sys/user/max_user_namespaces")
+ if err != nil {
+ return true, "Not applicable (file not found)"
+ }
+ if strings.TrimSpace(string(data)) == "0" {
+ return false, "max_user_namespaces is set to 0"
+ }
+ return true, ""
+ },
+ Hint: "Increase the user namespace limit: sudo sysctl -w user.max_user_namespaces=1024",
+ },
+ {
+ Name: "SubUID/SubGID Mappings",
+ Check: func() (bool, string) {
+ if isRoot {
+ return true, "Bypassed (Running as root)"
+ }
+ if username == "" {
+ return false, "Could not determine current username"
+ }
+ data, err := p.FS.ReadFile("/etc/subuid")
+ if err != nil {
+ return false, fmt.Sprintf("failed to read /etc/subuid: %v", err)
+ }
+
+ scanner := bufio.NewScanner(strings.NewReader(string(data)))
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.HasPrefix(line, username+":") {
+ return true, ""
+ }
+ }
+ return false, fmt.Sprintf("no subuid mapping found for user %s", username)
+ },
+ Hint: "Configure subuid and subgid for your user. Example: sudo usermod --add-subuids 100000-60000 --add-subgids 100000-60000 $USER",
+ },
+ }
+
+ var results []HealthResult
+ for _, req := range checks {
+ ok, msg := req.Check()
+ results = append(results, HealthResult{
+ Name: req.Name,
+ Passed: ok,
+ Message: msg,
+ Hint: req.Hint,
+ })
+ }
+ return results
+}
+
+// RunHealthCheck is the global wrapper for the health check.
+func RunHealthCheck() []HealthResult {
+ u, _ := user.Current()
+ username := ""
+ if u != nil {
+ username = u.Username
+ }
+ return (&Preflight{FS: DefaultFS}).RunHealthCheck(os.Getuid(), username)
+}
+
+// HealthResult describes the outcome of a single health check.
+type HealthResult struct {
+ Name string
+ Passed bool
+ Message string
+ Hint string
+}