diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-06-13 13:50:25 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-06-13 13:50:25 -0400 |
| commit | 5646eca119f80f8f45ebec9fcbe666ca614ebf5d (patch) | |
| tree | a785cb7f30b5a6444e208ae6717a73a758644998 /internal/namespace/preflight.go | |
| parent | 29621ecbd1e77e6e1a70b6b3ea8fbe3a56e47df3 (diff) | |
feat: implement system preflight checks and health diagnostics
Introduced a tiered system verification mechanism to improve reliability
and provide actionable feedback to users, avoiding false positives in
the critical execution path.
Key changes:
- Implement `CheckSystemRequirements` for critical, non-ambiguous
requirements (e.g., TUN device availability) to ensure fatal
environment issues are caught immediately during bootstrap.
- Implement a user-facing `healthcheck` command that provides
comprehensive diagnostics and actionable configuration hints for
common misconfigurations (e.g., unprivileged user namespaces,
subuid/subgid mappings, and kernel sysctls).
- Refactor the `FileSystem` interface to support full mockability,
allowing for exhaustive unit testing of diagnostic logic.
- Add comprehensive unit tests in `internal/namespace/preflight_test.go`
covering various Linux distributions, privilege levels, and
hardware availability scenarios.
- Ensure code quality through formatting, static analysis (golangci-lint),
and validation of all existing unit, integration, and E2E tests.
Diffstat (limited to 'internal/namespace/preflight.go')
| -rw-r--r-- | internal/namespace/preflight.go | 156 |
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 +} |
