Skip to content

feat(net): auto-remap EXPOSE host port on conflict#614

Open
G4614 wants to merge 3 commits into
boxlite-ai:mainfrom
G4614:feat/expose-auto-remap
Open

feat(net): auto-remap EXPOSE host port on conflict#614
G4614 wants to merge 3 commits into
boxlite-ai:mainfrom
G4614:feat/expose-auto-remap

Conversation

@G4614
Copy link
Copy Markdown
Contributor

@G4614 G4614 commented May 28, 2026

Auto-remap an image EXPOSE port to an OS-allocated ephemeral host port when its desired host port is busy (instead of failing fast),
explicit user -p mappings are never remapped.

Test plan

Unit (resolve_expose_host_port): _uses_desired_when_free (free → 1:1 AutoExpose) and _falls_back_when_busy (busy → ephemeral AutoRemap).

Integration expose_auto_remap — pre-bind 0.0.0.0:6379, then boxlite run -d redis:alpine (image EXPOSEs 6379). Verified two-sided (resolve_expose_host_port stubbed to never remap vs applied):

observed (the EXPOSE 6379 mapping) pre-fix (never remap) post-fix
host_port 6379 (1:1, collides with the pre-bound port) OS-allocated ephemeral (≠ 6379)
inspect Source auto_expose auto_remap
test result FAILassert host_port != 6379 at expose_auto_remap.rs:155 PASS

Guest keeps listening on 6379; only the host side moves.

Integration user_published_port_keeps_user_source (scope guard) — boxlite run -d -p <free>:<guest> alpine (no EXPOSE, so the explicit -p is the only mapping). Verified two-sided (user -p routed through resolve_expose_host_port vs not):

observed (the user -p mapping) broken (user -p routed through remap) correct
inspect Source auto_expose user
host_port chosen (free, so unchanged) chosen
test result FAILassert Source == "user" at expose_auto_remap.rs:273 PASS

When an image declares EXPOSE ports (e.g., redis:6379, docker:dind:2375),
boxlite previously bound them 1:1 to the host. If the desired host port
was already in use, gvproxy_create would fail and the box could not
start — even though the user never explicitly requested that host port.

resolve_expose_host_port() now probes the desired host port; on collision
it falls back to an OS-allocated ephemeral host port. Guest still listens
on the EXPOSE port; only the host binding moves. User-provided -p
mappings are never remapped — they continue to fail fast via the
gvproxy initErr path (covered by gvproxy_port_conflict regression).

ResolvedPortMapping carries the final binding (host_port, guest_port,
protocol, source) and is persisted on BoxState.port_mappings, exposed
via inspect/list/REST so users can discover the resolved host port for
detached boxes.

Ported from upstream PR boxlite-ai#568 (only the EXPOSE auto-resolve subset;
the --privileged/dind machinery remains in boxlite-ai#568 for separate
consideration).

Drive-by: tests/image_registries.rs needed `.clone()` after boxlite-ai#604 added
Drop to PerTestBoxHome (one-char fix).

Regression: expose_auto_remap_falls_back_when_desired_port_busy.
Two-side verified — fails without this change ("listen tcp 0.0.0.0:6379:
bind: address already in use"), passes with it (inspect shows
host:<ephemeral> → guest:6379 source=auto_remap).
gamnaansong and others added 2 commits May 28, 2026 09:37
Locks in the scope boundary that auto-remap only touches implicit image
EXPOSE ports, never explicit user `-p HOST:GUEST` (the user picked that
host port deliberately). alpine has no EXPOSE, so the only host-side
mapping is the explicit `-p`; assert inspect tags it Source=user with
the chosen host port preserved.

Two-sided: routing user `-p` through resolve_expose_host_port flips a
free port to Source=auto_expose → the assertion fails; the unchanged
path keeps Source=user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap the `as u16` cast and the long eprintln! line — fixes the "Check
Rust formatting" CI check on the prior commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@G4614 G4614 marked this pull request as ready for review May 28, 2026 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant