smolmachines: scope TSI allowlist to a per-bottle loopback alias (v2) #75

Closed
opened 2026-05-27 15:58:22 -04:00 by didericis-claude · 1 comment
Collaborator

Context

The Docker-Desktop fix (PR #74) routes the smolmachines agent through 127.0.0.1:<host port> loopback forwards instead of the bundle's docker-bridge IP. Necessary because Docker Desktop's daemon runs inside its own Linux VM and bridge IPs aren't reachable from macOS networking.

Side effect: the TSI allowlist is now 127.0.0.1/32, and TSI filters by IP only — no port granularity. So the agent VM can reach any service bound to macOS's loopback, not just the bundle's published ports: postgres on 5432, dev servers on 3000, other bottles' published ports, mDNSResponder, cupsd, etc. The docker backend keeps the bottle on a --internal docker network and doesn't have this issue.

See PRD 0023 open question #8 + the README "Smolmachines backend (experimental, macOS-only)" subsection for the full discussion.

Proposed fix

Bind each bottle's bundle ports on a per-bottle loopback alias (e.g. 127.0.0.2 for bottle A, 127.0.0.3 for B), set TSI's allowlist to that single /32. Macros loopback aliases require ifconfig lo0 alias <ip> — sudo on macOS. Options:

  • A small setuid helper shipped alongside the launcher that adds aliases on-demand.
  • A prompt-once setup step (./cli.py smolvm-setup) that adds a reserved /28 of aliases (127.0.0.16 .. 127.0.0.31) and assigns one per bottle from a pool.
  • A privilege-helper LaunchDaemon (heavier, more macOS-native).

v2 candidate; v1 ships with the loopback widening documented.

Acceptance

  • TSI allowlist = <per-bottle-loopback-alias>/32, not 127.0.0.1/32.
  • Two simultaneous bottles can't see each other's published ports.
  • Manual probe: agent VM cannot connect to a sentinel service on 127.0.0.1:<other-port> while the bottle is running.
  • README + PRD 0023 updated to note the resolution.
## Context The Docker-Desktop fix (PR #74) routes the smolmachines agent through `127.0.0.1:<host port>` loopback forwards instead of the bundle's docker-bridge IP. Necessary because Docker Desktop's daemon runs inside its own Linux VM and bridge IPs aren't reachable from macOS networking. **Side effect**: the TSI allowlist is now `127.0.0.1/32`, and TSI filters by IP only — no port granularity. So the agent VM can reach **any service bound to macOS's loopback**, not just the bundle's published ports: postgres on 5432, dev servers on 3000, other bottles' published ports, mDNSResponder, cupsd, etc. The docker backend keeps the bottle on a `--internal` docker network and doesn't have this issue. See PRD 0023 open question #8 + the README "Smolmachines backend (experimental, macOS-only)" subsection for the full discussion. ## Proposed fix Bind each bottle's bundle ports on a per-bottle loopback alias (e.g. `127.0.0.2` for bottle A, `127.0.0.3` for B), set TSI's allowlist to that single /32. Macros loopback aliases require `ifconfig lo0 alias <ip>` — sudo on macOS. Options: - A small setuid helper shipped alongside the launcher that adds aliases on-demand. - A prompt-once setup step (`./cli.py smolvm-setup`) that adds a reserved /28 of aliases (`127.0.0.16` .. `127.0.0.31`) and assigns one per bottle from a pool. - A privilege-helper LaunchDaemon (heavier, more macOS-native). v2 candidate; v1 ships with the loopback widening documented. ## Acceptance - TSI allowlist = `<per-bottle-loopback-alias>/32`, not `127.0.0.1/32`. - Two simultaneous bottles can't see each other's published ports. - Manual probe: agent VM cannot connect to a sentinel service on `127.0.0.1:<other-port>` while the bottle is running. - README + PRD 0023 updated to note the resolution.
Author
Collaborator

Resolved by #76. Each bottle now reserves a unique 127.0.0.16 .. 127.0.0.31 loopback alias, the bundle's port-forwards bind there, and the agent's TSI allowlist is the alias's /32. Smolvm 0.8.0 silently drops --allow-cidr when combined with --from, so the launcher patches smolvm's persistent state DB (~/Library/Application Support/smolvm/server/smolvm.db, vms.data BLOB) between machine create and machine start to set allowed_cidrs directly — smolvm reads it on start and TSI enforces.

End-to-end verified on Docker Desktop / macOS:

VM → 127.0.0.1:3000      → BLOCKED (Permission denied)   [host probe outside the bottle's /32]
VM → 8.8.8.8:53          → BLOCKED (Permission denied)
VM → 127.0.0.16:<bundle> → CONNECTED                     [bottle's own bundle, in allowlist]

The DB-patch hack is tagged temporary — when smolvm honors --allow-cidr with --from upstream, loopback_alias.force_allowlist becomes a no-op call to remove.

Resolved by #76. Each bottle now reserves a unique `127.0.0.16` .. `127.0.0.31` loopback alias, the bundle's port-forwards bind there, and the agent's TSI allowlist is the alias's `/32`. Smolvm 0.8.0 silently drops `--allow-cidr` when combined with `--from`, so the launcher patches smolvm's persistent state DB (`~/Library/Application Support/smolvm/server/smolvm.db`, `vms.data` BLOB) between `machine create` and `machine start` to set `allowed_cidrs` directly — smolvm reads it on start and TSI enforces. End-to-end verified on Docker Desktop / macOS: ``` VM → 127.0.0.1:3000 → BLOCKED (Permission denied) [host probe outside the bottle's /32] VM → 8.8.8.8:53 → BLOCKED (Permission denied) VM → 127.0.0.16:<bundle> → CONNECTED [bottle's own bundle, in allowlist] ``` The DB-patch hack is tagged temporary — when smolvm honors `--allow-cidr` with `--from` upstream, `loopback_alias.force_allowlist` becomes a no-op call to remove.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: didericis/bot-bottle#75