Files
zero/internal/sandbox/wsl_linux.go
KRATOS ba8fbb8b71 feat(sandbox): complete sandbox breadth — WSL fallback + opt-in MITM TLS inspection (#206)
* feat(sandbox): complete sandbox breadth — WSL fallback + opt-in MITM TLS inspection

Adds the two remaining sandbox features on top of the strict fail-closed core
(the other four — SOCKS5 egress, re-entrancy guard, fine-grained path lists,
sandbox-aware ripgrep — already shipped and are untouched). Both new features are
opt-in and the DEFAULT policy is unchanged (enforce + network deny, InspectTLS
off; AllowPolicyOnlyRunner keeps its existing default).

1) WSL detection + safe fail-closed fallback
   - parseWSL/detectWSL (wsl.go + wsl_linux.go reading /proc/version; wsl_other.go
     stub) classify WSL/WSL2.
   - SelectBackend: on Linux without bubblewrap, if running under WSL it selects a
     new BackendWSL instead of silently degrading to a plain policy-only runner.
   - BuildCommandPlan(BackendWSL): FAILS CLOSED unless Policy.AllowPolicyOnlyRunner
     is set (errWSLPolicyOnlyDisabled explains how to opt in). When allowed it runs
     the policy-only fallback that still routes egress through the filtering proxy
     (Backend.ProxyEgress => EnforcesScopedEgress, best-effort HTTP+SOCKS), sets
     ZERO_SANDBOXED=1 / ZERO_SANDBOX_BACKEND=wsl, and records an auditable
     least-privilege downgrade note (CommandPlan.Notes). Never runs unsandboxed
     silently.

2) Optional gated MITM TLS inspection (Policy.InspectTLS, default OFF)
   - Off => the egress proxy is a pure CONNECT passthrough, byte-for-byte
     unchanged (no CA generated).
   - On => the proxy terminates TLS toward the sandboxed client with an EPHEMERAL,
     locally generated ECDSA CA (mitm.go), minting per-host leaves on the fly, so
     it can re-check the DECRYPTED request Host through the SAME
     authorize()/domainAllowed() gate (deny-wins, empty allowlist => deny) — MITM
     widens visibility, never authority. The UPSTREAM dial is ALWAYS validated
     against the system roots (never InsecureSkipVerify; tests inject a known root
     pool to exercise validation). The CA's PUBLIC cert is written owner-only and
     surfaced via ZERO_SANDBOX_CA_CERT; MITM only runs on backends that already
     permit reading it (sandbox-exec allows file-read; WSL shares the host fs), so
     no new bind path is introduced. Trust implications documented loudly on the
     field. (No "raw passthrough only" flag exists in zero, so the mutual-exclusion
     case is N/A.)

Tests: parseWSL table; WSL fail-closed-without-opt-in and policy-only-with-proxy
(markers + ProxyEnvWithSocks) + deny-mode markers-no-proxy; MITM passthrough-by-
default (no CA), CA mints a chaining leaf, full TLS-terminate-and-forward of an
allowlisted host (upstream validated via an explicit root pool), and decrypted-
Host deny. Gates: gofmt, go vet (host + GOOS=linux), go build (host/linux/
windows), go test (incl -race), staticcheck (no new, host + linux), govulncheck
(0), deadcode -test=false under GOOS=linux (no new unreachable funcs).

* sandbox: address review on #206 (WSL + MITM)

- mitm: bound the upstream transport's TCP connect (DialContext 30s) so a
  blackholed upstream can't park a goroutine; disable upstream HTTP/2 and
  normalize the response to HTTP/1.1 before writing to the HTTP/1.1 client.
- mitm: mint leaves for the AUTHORIZED CONNECT host (not arbitrary client SNI)
  and cap the leaf cache (512) so SNI churn can't drive unbounded keygen/memory.
- mitm: re-check the decrypted Host with the FULL authorize gate
  (authorizeTarget) so a DomainPrompt-allowed host is honored consistently with
  the CONNECT check, not just domainAllowed.
- runner (WSL): preserve the caller's env in the direct-run WSL plan (it is not
  OS-wrapped), appending the sandbox markers last; the downgrade note says
  "with proxy egress" only when a proxy was actually started.
- tests: WSL caller-env preservation; deny-mode asserts no HTTP/HTTPS/ALL_PROXY
  leak + the downgrade note; MITM forward asserts HTTP 200; mitmConnect sets an
  I/O deadline.

Gates: gofmt, vet (host+linux), build (host/linux/windows), test (incl -race),
staticcheck (no new), govulncheck (0), deadcode -test=false (GOOS=linux) no new.
2026-06-15 11:38:14 +05:30

16 lines
326 B
Go

//go:build linux
package sandbox
import "os"
// detectWSL reads /proc/version once and classifies the WSL environment. A read
// error (no /proc) yields the zero WSLInfo (not WSL).
func detectWSL() WSLInfo {
data, err := os.ReadFile("/proc/version")
if err != nil {
return WSLInfo{}
}
return parseWSL(string(data))
}