feat(smolmachines): per-bottle loopback alias scopes TSI to single /32 #76

Merged
didericis-claude merged 4 commits from smolmachines-loopback-alias-scoping into main 2026-05-27 18:08:02 -04:00
3 changed files with 64 additions and 25 deletions
Showing only changes of commit a919268d5e - Show all commits
+20 -8
View File
@@ -200,14 +200,26 @@ sidecar bundle still in Docker. Selected via
The integration tests run against whichever backend the env var The integration tests run against whichever backend the env var
selects and skip cleanly when its prerequisites are missing. selects and skip cleanly when its prerequisites are missing.
**One-time sudo on first launch (macOS):** smolmachines needs **One-time sudo on first launch (macOS):** smolmachines bottles
per-bottle loopback aliases (`127.0.0.16` .. `127.0.0.31`) on `lo0` each reserve a loopback alias from a pool (`127.0.0.16` ..
so each bottle's TSI allowlist scopes to its own /32. The first `127.0.0.31`) and bind their bundle's port-forwards to it; the
`./cli.py start` after each reboot prompts for sudo to add the pool first `./cli.py start` after each reboot prompts for sudo to add
via `ifconfig lo0 alias`. Aliases persist until reboot; subsequent missing aliases via `ifconfig lo0 alias`. Aliases persist until
launches don't prompt. Without this, every bottle would share reboot; subsequent launches don't prompt.
`127.0.0.1` and be able to reach unrelated host services on the
loopback. **Known v1 limitation — agent can reach the whole host
loopback:** the alias-allocation infrastructure exists, but TSI
allowlist enforcement is blocked on a smolvm 0.8.0 upstream bug:
`smolvm machine create --from <smolmachine> --net --allow-cidr
X/32` silently drops the allowlist (the persisted
`agent.config.json` shows `allowed_cidrs: null`, and the running
VM reaches `127.0.0.0/8` regardless). So while a smolmachines
bottle is running, host-local dev services (postgres on 5432,
dev servers, etc.) are reachable from inside the agent even
though the launcher's `--allow-cidr` says otherwise. The docker
backend keeps the bottle on a `--internal` docker network and
doesn't have this issue. Tracked in gitea issue #75; will
auto-resolve once smolvm honors the flag.
## Manifest ## Manifest
@@ -7,11 +7,24 @@ reach **any** service bound to macOS's loopback, not just the
bundle's published ports. That's a real downgrade from the bundle's published ports. That's a real downgrade from the
docker backend's `--internal` network isolation. docker backend's `--internal` network isolation.
This module narrows the allowlist by allocating each bottle a This module is the host-side half of the eventual fix: allocate
unique loopback alias (`127.0.0.16` .. `127.0.0.31` by default). each bottle a unique loopback alias (`127.0.0.16` .. `127.0.0.31`
The bundle's port-forwards bind to that alias, TSI's allowlist is by default), bind the bundle's port-forwards to that alias, and
the alias /32, and other host loopback services stay invisible to pass the alias's /32 as smolvm's `--allow-cidr`. If TSI enforced
the bottle. the allowlist, the agent could only reach its own bundle.
**Upstream block, smolvm 0.8.0:** verified empirically that
`smolvm machine create --from <smolmachine> --net --allow-cidr
X/32` silently drops the allowlist. The persisted
`agent.config.json` shows `allowed_cidrs: null`, and the running
VM can reach any host loopback service regardless of the
flag. `machine update --allow-cidr` doesn't exist; stop-edit-
start of `agent.config.json` doesn't work (the file is removed
on stop); `--smolfile` is mutually exclusive with `--from`. So
the alias scoping infrastructure lives here, ready, but the
TSI enforcement is blocked on a smolvm upstream fix. Until that
lands, the agent can still reach the whole `127.0.0.0/8`. The
README + gitea issue #75 spell this out.
macOS only configures `127.0.0.1` on `lo0` by default; the macOS only configures `127.0.0.1` on `lo0` by default; the
additional aliases require `sudo ifconfig lo0 alias`. We lazily additional aliases require `sudo ifconfig lo0 alias`. We lazily
+26 -12
View File
@@ -600,18 +600,32 @@ PRD 0024's bundle image is a prerequisite — this PRD assumes
the plan is to filter on a deterministic name prefix the plan is to filter on a deterministic name prefix
`claude-bottle-<slug>` + cross-reference with on-disk metadata `claude-bottle-<slug>` + cross-reference with on-disk metadata
under `state/<slug>/`. under `state/<slug>/`.
8. **~~Loopback scoping (Docker Desktop pivot).~~ Resolved.** 8. **Loopback scoping (Docker Desktop pivot).** The original
Each bottle now allocates a per-bottle loopback alias from a design pinned the bundle at a docker bridge IP and set TSI's
pool of `127.0.0.16` .. `127.0.0.31`, binds the bundle's allowlist to `<bundle-ip>/32`. On Docker Desktop / macOS the
port-forwards to that alias, and sets TSI's allowlist to the daemon runs inside its own Linux VM, so bridge IPs aren't
alias's /32. So a smolmachines bottle can only reach its own reachable from macOS networking — TSI's syscall impersonation
bundle's published ports — not other bottles' ports, and not can't reach them. Resolution: publish each agent-facing bundle
unrelated host services on `127.0.0.1`. macOS loopback port on host loopback (`-p 127.0.0.1::<port>`) and set TSI to
aliases need `sudo ifconfig lo0 alias`; the launcher lazily `127.0.0.1/32`. **This widens the TSI allowlist to anything
adds missing pool entries on first launch per reboot (sudo bound to macOS's loopback** — postgres, dev servers, other
prompts once, aliases persist until reboot). Linux native bottles' published ports, mDNSResponder, etc.
daemons share the host's network namespace and skip the
alias dance. **Attempted fix + upstream block (`smolmachines-loopback-
alias-scoping` branch).** Allocate each bottle a unique
loopback alias (`127.0.0.16` .. `127.0.0.31`), bind bundle
port-forwards to it, set TSI's `--allow-cidr` to that /32.
Verified empirically that `smolvm 0.8.0 machine create --from
<smolmachine> --net --allow-cidr X/32` **silently drops the
allowlist** — `agent.config.json` shows `allowed_cidrs:null`
and the VM reaches all of `127.0.0.0/8` regardless of the
flag. Workarounds tried: `machine update --allow-cidr`
doesn't exist; stop-edit-`agent.config.json`-restart fails
(file is removed on stop); `--smolfile` is mutually exclusive
with `--from`. Alias-allocation infrastructure is in place
so the day smolvm honors `--allow-cidr` with `--from`, the
scoping starts working. Until then the agent can reach the
whole host loopback. Tracked in gitea issue #75.
## References ## References