auto-publish: forward 127.0.0.1 guest binds too, not only 0.0.0.0

Lima itself defaults to loopback-only (skipping 0.0.0.0) because on
its host-network model 0.0.0.0 in the guest is reachable from the
LAN and silently mirroring that on host loopback would change the
security posture. Our setup is the other way around — smoltcp is
in-process inside agent-vm, so 0.0.0.0 in the guest is reachable
only from the agent-vm process anyway. Forwarding both is the same
trust boundary and saves the user from having to remember which
address their dev server happened to pick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Evgeny Boger 2026-05-27 22:46:05 +00:00
parent 913d9e0066
commit 800b51c411
2 changed files with 25 additions and 19 deletions

View file

@ -5,10 +5,15 @@
//! listener forwards each accepted connection through a
//! per-connection in-guest python3 tunneller (see [`crate::exec_tunnel`]).
//!
//! Mirrors Lima's default policy: only `0.0.0.0` / `[::]` binds are
//! auto-forwarded — loopback-only guest services stay private. To
//! reach a guest 127.0.0.1 service from the host, use `--publish`
//! (native, declarative) instead.
//! Forwards both `127.0.0.1` *and* `0.0.0.0` listeners. Lima's
//! default is loopback-only (because on Lima's host-network model
//! `0.0.0.0` inside the guest is genuinely reachable from the LAN,
//! and silently re-exposing that on host loopback would change the
//! security posture). For us, smoltcp is in-process — both 127.0.0.1
//! and 0.0.0.0 inside the guest are reachable only from the
//! agent-vm process anyway, so forwarding either is the same trust
//! boundary and skipping 0.0.0.0 would just mean "the user has to
//! remember which one their dev server picked."
//!
//! Cancellation: the discovery task aborts itself when reading
//! `/proc/net/tcp` errors repeatedly (sandbox shutting down). On
@ -124,13 +129,14 @@ struct ForwardedPort {
task: JoinHandle<()>,
}
/// Decide whether to auto-forward this listener. Mirrors Lima's
/// default: only forward wildcard binds; loopback-only services
/// stay private to the guest.
/// Decide whether to auto-forward this listener. Wildcard *and*
/// loopback both qualify — see the module-level comment for why
/// 0.0.0.0 is safe to forward in our in-process-smoltcp setup
/// (unlike Lima).
fn should_forward(entry: ListenEntry) -> bool {
match entry.addr {
IpAddr::V4(a) => a.is_unspecified(),
IpAddr::V6(a) => a.is_unspecified(),
IpAddr::V4(a) => a.is_unspecified() || a.is_loopback(),
IpAddr::V6(a) => a.is_unspecified() || a.is_loopback(),
}
}

View file

@ -141,15 +141,13 @@ pub struct Args {
#[arg(long = "publish", short = 'p')]
publish: Vec<String>,
/// Auto-detect new `0.0.0.0` / `[::]` TCP listeners inside the
/// guest and mirror each on `127.0.0.1:<same port>` on the host
/// (Lima-style). Polls `/proc/net/tcp{,6}` over the agentd
/// channel every ~2s; spawns a tokio TCP listener per detected
/// port and bridges each accepted connection through a
/// per-connection in-guest python3 tunneller. Loopback-only
/// guest binds (`127.0.0.1`) are intentionally NOT forwarded
/// (privacy / Lima parity). Heavy on overhead — fine for
/// dev tunnels, use `--publish` for high-throughput.
/// Auto-detect new TCP listeners inside the guest (both
/// `127.0.0.1` and `0.0.0.0`/`[::]`) and mirror each on
/// `127.0.0.1:<same port>` on the host. Polls `/proc/net/tcp{,6}`
/// over the agentd channel every ~2s; spawns a tokio TCP listener
/// per detected port and bridges each accepted connection through
/// a per-connection in-guest python3 tunneller. Heavy on overhead
/// — fine for dev tunnels, use `--publish` for high-throughput.
#[arg(long = "auto-publish", default_value_t = false)]
auto_publish: bool,
@ -682,7 +680,9 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
.context("verifying image-API contract version")?;
if args.auto_publish {
eprintln!("==> auto-publish: polling /proc/net/tcp every ~2s for new 0.0.0.0 listeners");
eprintln!(
"==> auto-publish: polling /proc/net/tcp every ~2s for new 127.0.0.1 / 0.0.0.0 listeners"
);
crate::auto_publish::spawn(sandbox.clone());
}