Add --firewall: domain-level network isolation via tinyproxy + softnet#48
Add --firewall: domain-level network isolation via tinyproxy + softnet#48zachary-s-wiens wants to merge 8 commits into
Conversation
Two-layer host-side enforcement that guest sudo cannot bypass: 1. tinyproxy (explicit forward proxy) — domain allowlist via Filter + FilterDefaultDeny. VM's HTTPS_PROXY points here. 2. Tart softnet — hypervisor-level isolation blocks all VM outbound except the gateway IP (where the proxy runs). Usage: clod --firewall shell myvm # built-in defaults (.github.com, .anthropic.com) clod --firewall=domains.txt shell myvm # custom domain allowlist The domain file uses one domain per line, leading dot for subdomains: .github.com # matches *.github.com and github.com exact.example.com # matches only exact.example.com Gateway IP is detected dynamically from bridge100 after VM boot (softnet uses 192.168.2.1, differs from bridged mode's 192.168.64.1). Includes ShellSpec tests for filter generation, gateway detection, softnet args, and proxy URL construction. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
FYI there's a few improvements I'm adding. Will rebase this PR when ready. |
NSURLSession-backed tools (xcodebuild's SwiftPM, system frameworks,
anything reading CFNetworkCopySystemProxySettings) ignore HTTP_PROXY
env vars and read system proxy settings from macOS preferences. Under
--firewall, those tools were bypassing tinyproxy entirely, so SwiftPM
binary XCFramework downloads (Firebase gRPC, Facebook SDK, etc.) hit
the softnet block and timed out, leaving workspace-state.json with
empty "artifacts": [] and silently corrupting the SPM cache.
ssh_into_vm now calls vm_apply_system_proxy on every connect:
- firewall active -> setwebproxy + setsecurewebproxy to the same URL
already injected via HTTP_PROXY env
- firewall off -> setwebproxystate off / setsecurewebproxystate off
so a no-firewall session after a firewall session
doesn't inherit stale settings
Uses sudo -n; requires NOPASSWD sudo for networksetup on the guest,
which is already provided by ALLOW_SUDO=true at build-base time.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
firewall_start only checked _FIREWALL_PID, an in-process variable that tracks tinyproxys started by THIS shell. When a sibling tool (e.g. a devcontainer script sharing the same port to reuse one upstream proxy) already holds the port, clod's tinyproxy fails to bind, exits with "Could not create listening sockets", and the startup loop aborts. Add an nc -z probe ahead of the spawn. If the port is already open, log that we're reusing and return — _FIREWALL_PID stays empty so the matching firewall_stop path won't try to kill a process it doesn't own. Both sides are expected to use the same allowlist file, so the filter set is identical and sharing is safe. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Previous version silently exited 0 when no network service was detected and swallowed all stderr from networksetup, making it impossible to diagnose why NSURLSession-backed tools (xcodebuild/SwiftPM) were still bypassing the firewall. Now: prints the detected service name, dumps the resulting proxy state on apply, and surfaces stderr indented under a host-side warning on failure. Empty service list is treated as a failure rather than a silent no-op.
The previous 'first non-disabled service' heuristic picked up virtual network services like com.redhat.spice.0 (SPICE virtio) that don't carry VM traffic. networksetup happily set the proxy on that service, NSURLSession ignored it (since real traffic flows through a different interface), and SwiftPM kept hitting the softnet block with timeouts. Now we read the default-route interface from 'route get default' and pick the networksetup service whose '(Hardware Port: ..., Device: enX)' line matches. Fails loudly if no service maps to the active device.
Feature/system proxy
Lets the guest tunnel native SSH to allowlisted hosts (e.g. github.com:22) through tinyproxy. The domain Filter still applies — port 22 isn't a wildcard hole, only allowlisted hosts accept CONNECT on it. This replaces the older insteadOf+credential-helper chain consumers had to use to route git through the HTTPS proxy. With ConnectPort 22, guests can use a plain ProxyCommand and speak SSH directly.
Gradle's parallel artifact resolve (AGP InstrumentationAnalysisTransform running across the Android/Kotlin classpath) opens >50 concurrent CONNECT tunnels through tinyproxy and exhausts the previous limit. Symptom: gradle hangs indefinitely at the resolve step; tinyproxy log shows "Maximum number of connections reached. Refusing new connections." 200 has comfortable headroom for parallel resolves without affecting memory footprint. Co-Authored-By: Claude Opus 4.7 <[email protected]>
|
I've created a copy of your PR (#49) because I can't push my changes to your branch. I've added quite a lot of changes because I can't quite get things to work on my computer. Would you like to look over that PR and incorporate any changes you'd like into your PR (it's fine if you want to squash out my commits or whatever, I'm not concerned about authorship). Some of the issues I corrected:
Unfortunately, I tried, but it's just not working yet :( Thanks for your help! |
|
Hey, I'll have time next week to look in detail. I have a few things going on in the install-extra.sh on my end too, might be something there. What is breaking for you? |
|
I think I fixed all the issues, and I've pushed updates to the branch. I've done local testing, would you do me the favor of testing on your computer and see if you can spot any issues? Thanks! |
|
Don't know if you saw this @zachary-s-wiens:
The changes are in #49 |
|
Hey I did - Hoping to get to today still. I'm really trying to finish up
something for work and have been running into worktrees in vm + sim + host
cross compile issues with xcode, on the computer I was using this setup on.
Need to finish up a few tasks then I'll test.
…On Fri, Jun 5, 2026 at 8:05 AM Patrick Wyatt ***@***.***> wrote:
*webcoyote* left a comment (webcoyote/clodpod#48)
<#48 (comment)>
Don't know if you saw this @zachary-s-wiens
<https://github.com/zachary-s-wiens>:
I think I fixed all the issues, and I've pushed updates to the branch.
I've done local testing, would you do me the favor of testing on your
computer and see if you can spot any issues? Thanks!
—
Reply to this email directly, view it on GitHub
<#48?email_source=notifications&email_token=BQBGZODXBIOJSRQPBJZ46UT46LOUFA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINRTGI4TEMJRG422M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-4632921175>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/BQBGZOFYU6MBFPYJF56O7XD46LOUFAVCNFSM6AAAAACZIMOCGWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DMMZSHEZDCMJXGU>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Summary
--firewall[=FILE]global option for host-side domain-level network isolationFilterDefaultDeny— only allowlisted domains passbridge100after VM boot (softnet uses192.168.2.1)HTTPS_PROXY,HTTP_PROXY,NO_PROXY) injected into VM SSH sessionUsage
Domain file format (one per line, leading dot = subdomains):
Files changed
lib/firewall.shclod--firewallflag parsing, proxy start/stop in shell dispatchlib/instance.shssh_into_vm(), softnet flags invm_shell()lib/commands.shspec/firewall_spec.shspec/help_spec.sh--firewallin help outputTest plan
shellspec— 47/47 pass (13 new + 34 existing)clod --firewall shell <instance>→ verify allowed domains work, blocked domains get HTTP 403 from proxycurl --noproxy '*' <anything>inside VM → verify softnet blocks direct bypass🤖 Generated with Claude Code