2 Commits

Author SHA1 Message Date
b55b3364af feat: add dependency status to --version and document AppArmor userns fix
Some checks failed
Build and test / Build (push) Successful in 11s
Build and test / Lint (push) Failing after 1m24s
Build and test / Test (Linux) (push) Failing after 40s
Build and test / Test (macOS) (push) Has been cancelled
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.
2026-02-11 19:31:24 -06:00
70d0685c97 fix: use UDP instead of TCP for DNS bridge to host DNS server
The DnsBridge socat relay was forwarding queries via TCP, but the
GreyHaven DNS service (gost) only listens on UDP, causing DNS
resolution failures ("Could not resolve host") inside the sandbox.
2026-02-11 19:30:56 -06:00
5 changed files with 155 additions and 3 deletions

View File

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

View File

@@ -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.

View File

@@ -32,7 +32,7 @@ type ProxyBridge struct {
// DnsBridge bridges DNS queries from the sandbox to a host-side DNS server via Unix socket.
// Inside the sandbox, a socat relay converts UDP DNS queries (port 53) to the Unix socket.
// On the host, socat forwards from the Unix socket to the actual DNS server (TCP).
// On the host, socat forwards from the Unix socket to the actual DNS server (UDP).
type DnsBridge struct {
SocketPath string // Unix socket path
DnsAddr string // Host-side DNS address (host:port)
@@ -61,10 +61,10 @@ func NewDnsBridge(dnsAddr string, debug bool) (*DnsBridge, error) {
debug: debug,
}
// Start bridge: Unix socket -> DNS server TCP
// Start bridge: Unix socket -> DNS server UDP
socatArgs := []string{
fmt.Sprintf("UNIX-LISTEN:%s,fork,reuseaddr", socketPath),
fmt.Sprintf("TCP:%s", dnsAddr),
fmt.Sprintf("UDP:%s", dnsAddr),
}
bridge.process = exec.Command("socat", socatArgs...) //nolint:gosec // args constructed from trusted input
if debug {

View File

@@ -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

View File

@@ -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")
}
}