diff --git a/README.md b/README.md index 0bd04fd..3aacef7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Greywall wraps commands in a deny-by-default sandbox. Filesystem access is restricted to the current directory by default. Use `--learning` to trace what else a command needs and auto-generate a config profile. All network traffic is transparently redirected through [greyproxy](https://github.com/GreyhavenHQ/greyproxy), a deny-by-default transparent proxy with a live allow/deny dashboard. Run `greywall setup` to install greyproxy automatically. -*Supports Linux and macOS. See [platform support](docs/platform-support.md) for details.* +*Supports Linux, macOS, and Windows (via WSL). See [platform support](docs/platform-support.md) for details.* https://github.com/user-attachments/assets/7d62d45d-a201-4f24-9138-b460e4c157a8 @@ -183,22 +183,51 @@ By default, traffic routes through the GreyProxy SOCKS5 proxy at `localhost:4305 ## Platform support -| Feature | Linux | macOS | -|---------|:-----:|:-----:| -| **Sandbox engine** | bubblewrap | sandbox-exec (Seatbelt) | -| **Filesystem deny-by-default (read/write)** | ✅ | ✅ | -| **Syscall filtering** | ✅ (seccomp) | ✅ (Seatbelt) | -| **Filesystem access control** | ✅ (Landlock + bubblewrap) | ✅ (Seatbelt) | -| **Violation monitoring** | ✅ (eBPF) | ✅ (Seatbelt denial logs) | -| **Transparent proxy (full traffic capture)** | ✅ (tun2socks + TUN) | ❌ | -| **DNS capture** | ✅ (DNS bridge) | ❌ | -| **Proxy via env vars (SOCKS5 / HTTP)** | ✅ | ✅ | -| **Network isolation** | ✅ (network namespace) | N/A | -| **Command allow/deny lists** | ✅ | ✅ | -| **Environment sanitization** | ✅ | ✅ | -| **Learning mode** | ✅ (strace) | ✅ (eslogger, requires sudo) | -| **PTY support** | ✅ | ✅ | -| **External deps** | bwrap, socat | none | +| Feature | Linux | macOS | Windows (WSL) | +|---------|:-----:|:-----:|:-------------:| +| **Sandbox engine** | bubblewrap | sandbox-exec (Seatbelt) | bubblewrap (WSL 2) | +| **Filesystem deny-by-default (read/write)** | ✅ | ✅ | ✅ (WSL 2) | +| **Syscall filtering** | ✅ (seccomp) | ✅ (Seatbelt) | ✅ (WSL 2) | +| **Filesystem access control** | ✅ (Landlock + bubblewrap) | ✅ (Seatbelt) | ✅ (WSL 2) | +| **Violation monitoring** | ✅ (eBPF) | ✅ (Seatbelt denial logs) | ✅ (WSL 2) | +| **Transparent proxy (full traffic capture)** | ✅ (tun2socks + TUN) | ❌ | ✅ (WSL 2) | +| **DNS capture** | ✅ (DNS bridge) | ❌ | ✅ (WSL 2) | +| **Proxy via env vars (SOCKS5 / HTTP)** | ✅ | ✅ | ✅ | +| **Network isolation** | ✅ (network namespace) | N/A | ✅ (WSL 2) | +| **Command allow/deny lists** | ✅ | ✅ | ✅ | +| **Environment sanitization** | ✅ | ✅ | ✅ | +| **Learning mode** | ✅ (strace) | ✅ (eslogger, requires sudo) | ✅ (WSL 2) | +| **PTY support** | ✅ | ✅ | ✅ | +| **External deps** | bwrap, socat | none | bwrap, socat (inside WSL) | + +### Windows (WSL) + +Greywall runs on Windows through **WSL 2** (Windows Subsystem for Linux). WSL 2 provides a full Linux kernel, so all Linux sandboxing features — including bubblewrap, seccomp, Landlock, and network namespaces — work as expected. + +**Requirements:** + +1. **WSL 2** — WSL 1 is not supported (no real Linux kernel). +2. **systemd enabled** — required for bubblewrap namespace support. Add to `/etc/wsl.conf`: + ```ini + [boot] + systemd = true + ``` +3. **`/etc/resolv.conf` must not be a symlink** — the default WSL-generated symlink breaks bwrap namespace setup. Fix it: + ```bash + sudo rm /etc/resolv.conf + echo "nameserver 10.255.255.254" | sudo tee /etc/resolv.conf + ``` + Then disable auto-generation in `/etc/wsl.conf`: + ```ini + [network] + generateResolvConf = false + ``` +4. **Restart WSL** after making changes: + ```powershell + wsl --shutdown + ``` + +Run `greywall check` to verify your WSL environment is correctly configured. See [platform support](docs/platform-support.md) for more details. diff --git a/cmd/greywall/main.go b/cmd/greywall/main.go index a513c13..a095085 100644 --- a/cmd/greywall/main.go +++ b/cmd/greywall/main.go @@ -9,6 +9,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "runtime" "strconv" "strings" "syscall" @@ -525,6 +526,29 @@ func runCheck(_ *cobra.Command, _ []string) error { steps := sandbox.PrintDependencyStatus() + // WSL2: check for common issues that break bwrap namespace setup + if isWSL() { + if info, err := os.Lstat("/etc/resolv.conf"); err == nil && info.Mode()&os.ModeSymlink != 0 { + fmt.Println(sandbox.CheckFail("WSL2: /etc/resolv.conf is a symlink (bwrap will fail)")) + steps = append(steps, + "Fix WSL2 resolv.conf symlink:\n"+ + " sudo rm /etc/resolv.conf && echo \"nameserver 10.255.255.254\" | sudo tee /etc/resolv.conf\n"+ + " Add to /etc/wsl.conf: [network] generateResolvConf = false | [boot] systemd = true\n"+ + " Then run: wsl --shutdown from PowerShell", + ) + } else { + fmt.Println(sandbox.CheckOK("WSL2: /etc/resolv.conf is a real file")) + } + if !isSystemdEnabled() { + fmt.Println(sandbox.CheckFail("WSL2: systemd not enabled (required for bwrap namespace support)")) + steps = append(steps, + "Enable systemd in /etc/wsl.conf: add [boot] systemd = true, then run wsl --shutdown", + ) + } else { + fmt.Println(sandbox.CheckOK("WSL2: systemd enabled")) + } + } + status := proxy.Detect() brewManaged := proxy.IsBrewManaged(status.Path) @@ -584,6 +608,33 @@ func runCheck(_ *cobra.Command, _ []string) error { return nil } +// isWSL reports whether the current environment is Windows Subsystem for Linux. +func isWSL() bool { + if runtime.GOOS != "linux" { + return false + } + // WSL sets the WSL_DISTRO_NAME environment variable + if os.Getenv("WSL_DISTRO_NAME") != "" { + return true + } + // Fallback: check /proc/version for Microsoft signature + data, err := os.ReadFile("/proc/version") + if err != nil { + return false + } + lower := strings.ToLower(string(data)) + return strings.Contains(lower, "microsoft") || strings.Contains(lower, "wsl") +} + +// isSystemdEnabled reports whether systemd is running as PID 1. +func isSystemdEnabled() bool { + target, err := os.Readlink("/proc/1/exe") + if err != nil { + return false + } + return strings.Contains(target, "systemd") +} + // newSetupCmd creates the setup subcommand for installing greyproxy. func newSetupCmd() *cobra.Command { return &cobra.Command{