From b55b3364af2be4c8a1f24ba27d772e90a6db00eb Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Wed, 11 Feb 2026 19:31:24 -0600 Subject: [PATCH] feat: add dependency status to --version and document AppArmor userns fix Show installed dependencies, security features, and transparent proxy availability when running --version. Detect AppArmor unprivileged_userns restriction on Ubuntu 24.04+ and suggest the fix. Document the RTM_NEWADDR issue in experience.md. --- cmd/greywall/main.go | 1 + docs/experience.md | 26 ++++++ internal/sandbox/linux_features.go | 101 ++++++++++++++++++++++++ internal/sandbox/linux_features_stub.go | 24 ++++++ 4 files changed, 152 insertions(+) diff --git a/cmd/greywall/main.go b/cmd/greywall/main.go index 6afe7e4..6113f21 100644 --- a/cmd/greywall/main.go +++ b/cmd/greywall/main.go @@ -125,6 +125,7 @@ func runCommand(cmd *cobra.Command, args []string) error { fmt.Printf(" Version: %s\n", version) fmt.Printf(" Built: %s\n", buildTime) fmt.Printf(" Commit: %s\n", gitCommit) + sandbox.PrintDependencyStatus() return nil } diff --git a/docs/experience.md b/docs/experience.md index 38eb19e..5ca66c4 100644 --- a/docs/experience.md +++ b/docs/experience.md @@ -67,3 +67,29 @@ Lessons learned and issues encountered during development. **Problem:** gost's SOCKS5 server always selects authentication method 0x02 (username/password), even when no real credentials are needed. Clients that only offer method 0x00 (no auth) get rejected. **Fix:** Always include credentials in the proxy URL (e.g., `proxy:proxy@`). In tun2socks proxy URL construction, include `userinfo` so tun2socks offers both auth methods during SOCKS5 negotiation. + +--- + +## Network namespaces fail on Ubuntu 24.04 (`RTM_NEWADDR: Operation not permitted`) + +**Problem:** On Ubuntu 24.04 (tested in a KVM guest with bridged virtio/virbr0), `--version` reports `bwrap(no-netns)` and transparent proxy is unavailable. `kernel.unprivileged_userns_clone=1` is set, bwrap and socat are installed, but `bwrap --unshare-net` fails with: +``` +bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted +``` + +**Cause:** Ubuntu 24.04 introduced `kernel.apparmor_restrict_unprivileged_userns` (default: 1). This strips capabilities like `CAP_NET_ADMIN` from processes inside unprivileged user namespaces, even without a bwrap-specific AppArmor profile. Bubblewrap creates the network namespace successfully but cannot configure the loopback interface (adding 127.0.0.1 via netlink RTM_NEWADDR requires `CAP_NET_ADMIN`). Not a hypervisor issue — happens on bare metal Ubuntu 24.04 too. + +**Diagnosis:** +```bash +sysctl kernel.apparmor_restrict_unprivileged_userns # likely returns 1 +bwrap --unshare-net --ro-bind / / -- /bin/true # reproduces the error +``` + +**Fix:** Disable the restriction (requires root on the guest): +```bash +sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 +# Persist across reboots: +echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/99-greywall-userns.conf +``` + +**Alternative:** Accept the limitation — greywall still works for filesystem sandboxing, seccomp, and Landlock. Network access is blocked outright rather than redirected through a proxy. diff --git a/internal/sandbox/linux_features.go b/internal/sandbox/linux_features.go index df4380a..8cef9dd 100644 --- a/internal/sandbox/linux_features.go +++ b/internal/sandbox/linux_features.go @@ -276,6 +276,107 @@ func (f *LinuxFeatures) MinimumViable() bool { return f.HasBwrap && f.HasSocat } +// PrintDependencyStatus prints dependency status with install suggestions for Linux. +func PrintDependencyStatus() { + features := DetectLinuxFeatures() + + fmt.Printf("\n Platform: linux (kernel %d.%d)\n", features.KernelMajor, features.KernelMinor) + + fmt.Printf("\n Dependencies (required):\n") + + allGood := true + if features.HasBwrap { + fmt.Printf(" ✓ bubblewrap (bwrap)\n") + } else { + fmt.Printf(" ✗ bubblewrap (bwrap) — REQUIRED\n") + allGood = false + } + if features.HasSocat { + fmt.Printf(" ✓ socat\n") + } else { + fmt.Printf(" ✗ socat — REQUIRED\n") + allGood = false + } + + if !allGood { + fmt.Printf("\n Install missing dependencies:\n") + fmt.Printf(" %s\n", suggestInstallCmd(features)) + } + + fmt.Printf("\n Security features: %s\n", features.Summary()) + + if features.CanUseTransparentProxy() { + fmt.Printf(" Transparent proxy: available\n") + } else { + parts := []string{} + if !features.HasIpCommand { + parts = append(parts, "iproute2") + } + if !features.HasDevNetTun { + parts = append(parts, "/dev/net/tun") + } + if !features.CanUnshareNet { + parts = append(parts, "network namespace") + } + if len(parts) > 0 { + fmt.Printf(" Transparent proxy: unavailable (missing: %s)\n", strings.Join(parts, ", ")) + } else { + fmt.Printf(" Transparent proxy: unavailable\n") + } + + if !features.CanUnshareNet && features.HasBwrap { + if val := readSysctl("kernel/apparmor_restrict_unprivileged_userns"); val == "1" { + fmt.Printf("\n Note: AppArmor is restricting unprivileged user namespaces.\n") + fmt.Printf(" This prevents bwrap --unshare-net (needed for transparent proxy).\n") + fmt.Printf(" To fix: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\n") + fmt.Printf(" Persist: echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/99-greywall-userns.conf\n") + } + } + } + + if allGood { + fmt.Printf("\n Status: ready\n") + } else { + fmt.Printf("\n Status: missing required dependencies\n") + } +} + +func suggestInstallCmd(features *LinuxFeatures) string { + var missing []string + if !features.HasBwrap { + missing = append(missing, "bubblewrap") + } + if !features.HasSocat { + missing = append(missing, "socat") + } + pkgs := strings.Join(missing, " ") + + switch { + case commandExists("apt-get"): + return fmt.Sprintf("sudo apt install %s", pkgs) + case commandExists("dnf"): + return fmt.Sprintf("sudo dnf install %s", pkgs) + case commandExists("yum"): + return fmt.Sprintf("sudo yum install %s", pkgs) + case commandExists("pacman"): + return fmt.Sprintf("sudo pacman -S %s", pkgs) + case commandExists("apk"): + return fmt.Sprintf("sudo apk add %s", pkgs) + case commandExists("zypper"): + return fmt.Sprintf("sudo zypper install %s", pkgs) + default: + return fmt.Sprintf("install %s using your package manager", pkgs) + } +} + +func readSysctl(name string) string { + data, err := os.ReadFile("/proc/sys/" + name) + if err != nil { + return "" + } + return strings.TrimSpace(string(data)) +} + func commandExists(name string) bool { _, err := exec.LookPath(name) return err == nil diff --git a/internal/sandbox/linux_features_stub.go b/internal/sandbox/linux_features_stub.go index 3caddcb..205de01 100644 --- a/internal/sandbox/linux_features_stub.go +++ b/internal/sandbox/linux_features_stub.go @@ -2,6 +2,12 @@ package sandbox +import ( + "fmt" + "os/exec" + "runtime" +) + // LinuxFeatures describes available Linux sandboxing features. // This is a stub for non-Linux platforms. type LinuxFeatures struct { @@ -51,3 +57,21 @@ func (f *LinuxFeatures) CanUseTransparentProxy() bool { func (f *LinuxFeatures) MinimumViable() bool { return false } + +// PrintDependencyStatus prints dependency status for non-Linux platforms. +func PrintDependencyStatus() { + if runtime.GOOS == "darwin" { + fmt.Printf("\n Platform: macOS\n") + fmt.Printf("\n Dependencies (required):\n") + if _, err := exec.LookPath("sandbox-exec"); err == nil { + fmt.Printf(" ✓ sandbox-exec (Seatbelt)\n") + fmt.Printf("\n Status: ready\n") + } else { + fmt.Printf(" ✗ sandbox-exec — REQUIRED (should be built-in on macOS)\n") + fmt.Printf("\n Status: missing required dependencies\n") + } + } else { + fmt.Printf("\n Platform: %s (unsupported)\n", runtime.GOOS) + fmt.Printf("\n Status: this platform is not supported\n") + } +}