Add reusable proxy env and net wrapper helpers
ober
d661dcbcd6c30c412b855deade6191d5e7218d61
--- a/docs/limits-followup.md +++ b/docs/limits-followup.md @@ -1,7 +1,7 @@ # Limits Follow-Up: Missing Enforcement and Test Work Reviewed: 2026-05-21 -Status updated: 2026-05-22 (sandbox denied path phase) +Status updated: 2026-05-22 (proxy handoff helper phase) This document is a follow-up to `docs/limits.md` after reviewing the new limits/sandbox/audit module set added around commit `97eea41`. @@ -22,7 +22,7 @@ backend-specific filesystem and network denial tests are still needed. | 3 | Resource limits | SAFE PARTIAL — requested-limit plans and child install reports are per-kind; `require: '(limits)` fails closed before exec; parent-side time/output markers exist; **GAP**: cgroup v2 | | 4 | Executable identity | DONE — path search, realpath/stat identity, comparison helper, and TOCTOU caveat are present | | 5 | Filesystem tracing | SAFE PARTIAL — `tracefs-capabilities`, fail-closed wrapper, normalized `fs-event` output, best-effort fd/cwd tracking, and generic read/write/exec suggestions exist; **GAP**: native backends and deeper Linux traced-run tests | -| 6 | Network allowlist | SAFE PARTIAL — reusable decision layer, parent-side DNS resolution, resolved-address localnet recheck, structured proxy events, and binary CONNECT tunneling exist; **GAP**: child network sandbox/proxy handoff and direct-network denial tests | +| 6 | Network allowlist | SAFE PARTIAL — reusable decision layer, parent-side DNS resolution, resolved-address localnet recheck, structured proxy events, binary CONNECT tunneling, proxy env construction, and macOS local-only command wrapping exist; **GAP**: full child network sandbox/proxy handoff in consumers and direct-network denial tests | | 7 | Environment/secrets | DONE — default deny policy, argv validation, env construction, and redaction helpers exist | | 8 | Temp HOME/cache | DONE for helper layer — fake HOME, scratch/cache grants, env overrides, cleanup, sandbox grant helper, and reusable named path capability resolution exist | | 9 | Structured audit model | PARTIAL — event constructors and redaction exist; **GAP**: full integration from all primitives | @@ -116,6 +116,9 @@ Implemented: - `sandbox-command-wrapper-needed?`, `sandbox-command-wrapper-available?`, and `sandbox-wrap-command` expose the reusable command rewrite needed by custom launchers that cannot call `sandbox-launch` directly. +- `net: 'local-only` and `net: 'allowlist` can use the macOS command wrapper + path so custom launchers can start a target with remote network denied before + the target binary runs. - Linux Landlock hook via `jerboa_landlock_sandbox`; policies with negative path exceptions report degraded on backends that cannot enforce those exceptions. @@ -150,7 +153,7 @@ Required next work: - fail-closed aborts when a required axis is not fully enforced 2. Split macOS capability reporting so "can wrap target with sandbox-exec" is distinguishable from "this already-running child process is confined". -3. Add a first-class network allowlist/proxy handoff requirement. +3. Add deeper direct-network denial tests for proxy handoff consumers. ## 2. Process Supervision --- a/docs/limits.md +++ b/docs/limits.md @@ -85,6 +85,9 @@ report status to the parent through `(std os supervise)` before exec. Policies also accept `deny-read-paths:`, `deny-write-paths:`, and `deny-exec-paths:`; macOS Seatbelt renders those as explicit SBPL denies, while backends without negative path exceptions report degraded so fail-closed callers can refuse. +`(std net allow-proxy)` provides `allow-proxy-url` and `allow-proxy-env` helpers +for proxy handoff, and macOS `sandbox-wrap-command` can now wrap `net: +local-only` policies so custom launchers can deny remote egress before exec. ## 2. Process Supervision --- a/lib/std/net/allow-proxy.ss +++ b/lib/std/net/allow-proxy.ss @@ -46,6 +46,8 @@ allow-proxy-stop! allow-proxy-port allow-proxy-host + allow-proxy-url + allow-proxy-env allow-proxy-allowlist allow-proxy-logger allow-proxy-stats @@ -104,6 +106,27 @@ [srv (tcp-server-port srv)] [else (allow-proxy-rec-port p)]))) + (def (allow-proxy-url p) + (string-append "http://" + (allow-proxy-host p) + ":" + (number->string (allow-proxy-port p)))) + + (def (allow-proxy-env p) + ;; Environment overrides understood by common HTTP clients. The + ;; lowercase spellings are intentional: curl, wget, Python requests, + ;; Node, and many package managers check one or both forms. + (let ([url (allow-proxy-url p)] + [no-proxy "localhost,127.0.0.1,::1"]) + (list (cons "HTTP_PROXY" url) + (cons "HTTPS_PROXY" url) + (cons "ALL_PROXY" url) + (cons "http_proxy" url) + (cons "https_proxy" url) + (cons "all_proxy" url) + (cons "NO_PROXY" no-proxy) + (cons "no_proxy" no-proxy)))) + (def (allow-proxy . opts) (let ([host "127.0.0.1"] [port 0] [allow '()] [logger #f] [allow-ipl? #f] [allow-localnet? #f]) --- a/lib/std/os/limits/sandbox.ss +++ b/lib/std/os/limits/sandbox.ss @@ -351,15 +351,24 @@ fs-status)) (syscalls . unavailable)))) + (def (sandbox-policy-net-wrapper-needed? pol) + ;; `deny` is the neutral default today, so do not force every default + ;; policy through sandbox-exec. Callers that need direct-network denial + ;; for proxy handoff should request local-only or allowlist explicitly. + (case (sandbox-policy-get pol 'net) + [(local-only allowlist) #t] + [else #f])) + (def (macos-wrap-command pol cmd) - ;; If the policy has paths, rewrite CMD to invoke /usr/bin/sandbox-exec - ;; with the generated SBPL profile, so the policy is applied BEFORE - ;; the target binary starts. + ;; If the policy has paths or an explicit wrapper-enforceable network + ;; mode, rewrite CMD to invoke /usr/bin/sandbox-exec so the policy is + ;; applied BEFORE the target binary starts. (let* ([read (sandbox-policy-get pol 'read-paths)] [write (sandbox-policy-get pol 'write-paths)] [exec (sandbox-policy-get pol 'exec-paths)]) (cond - [(sandbox-policy-has-paths? pol) + [(or (sandbox-policy-has-paths? pol) + (sandbox-policy-net-wrapper-needed? pol)) (let ([sbpl (build-sbpl read write exec (sandbox-policy-get pol 'deny-read-paths) (sandbox-policy-get pol 'deny-write-paths) @@ -383,9 +392,10 @@ (def (sandbox-command-wrapper-needed? pol) ;; Some backends install policy directly in the child between fork and ;; exec. macOS path policies are different: they must exec through - ;; sandbox-exec so the target starts inside the deny-default profile. + ;; sandbox-exec so the target starts inside the profile. (and (platform-macos?) - (sandbox-policy-has-paths? pol))) + (or (sandbox-policy-has-paths? pol) + (sandbox-policy-net-wrapper-needed? pol)))) (def (sandbox-command-wrapper-available? pol) (or (not (sandbox-command-wrapper-needed? pol)) --- a/tests/test-limits-primitives.ss +++ b/tests/test-limits-primitives.ss @@ -346,6 +346,24 @@ (alist-ref/default d 'connect-host #f) "127.0.0.1")) +(let ([p (allow-proxy 'host: "127.0.0.1" 'port: 12345 + 'allow: '("example.com:443"))]) + (test "allow-proxy-url renders host and port" + (allow-proxy-url p) + "http://127.0.0.1:12345") + (test-pred "allow-proxy-env exports HTTPS proxy" + (allow-proxy-env p) + (lambda (xs) + (and (assoc "HTTPS_PROXY" xs) + (string=? (cdr (assoc "HTTPS_PROXY" xs)) + "http://127.0.0.1:12345")))) + (test-pred "allow-proxy-env exports lowercase proxy" + (allow-proxy-env p) + (lambda (xs) + (and (assoc "https_proxy" xs) + (string=? (cdr (assoc "https_proxy" xs)) + "http://127.0.0.1:12345"))))) + (test-pred "net-allowlist decision layer matches wildcard host" (net-allowlist-host-allowed? '("*.example.com:443") "api.example.com" 443 #f #f) @@ -503,6 +521,29 @@ "/usr/bin/sandbox-exec" cmd))) +(let* ([pol (sandbox-policy 'net: 'local-only)] + [cmd '("/bin/echo" "net-wrapped?")] + [wrapped (sandbox-wrap-command pol cmd)] + [sbpl (sandbox-policy-sbpl pol)]) + (test "sandbox net-local wrapper need is platform-aware" + (sandbox-command-wrapper-needed? pol) + (platform-macos?)) + (test "sandbox net-local wrapper changes command only when needed and available" + (if (and (sandbox-command-wrapper-needed? pol) + (sandbox-command-wrapper-available? pol)) + (car wrapped) + wrapped) + (if (and (sandbox-command-wrapper-needed? pol) + (sandbox-command-wrapper-available? pol)) + "/usr/bin/sandbox-exec" + cmd)) + (test-pred "sandbox net-local SBPL denies network first" + sbpl + (lambda (s) (string-contains? s "(deny network*)"))) + (test-pred "sandbox net-local SBPL allows local network" + sbpl + (lambda (s) (string-contains? s "(allow network* (local ip))")))) + (let* ([pol (sandbox-policy 'write-paths: '("/tmp/project") 'deny-read-paths: '("/tmp/project/secret")