Skip to content

Commit 493bb07

Browse files
author
Roy Lin
committed
fix(cli): resolve -p 0 (auto-assign host port) to a real free port
`docker run -p 0:80` (or an auto-assign publish) picks a free host port; a3s-box stored host port 0, which the shim/passt filters skip (host_port != 0) so it was never forwarded and `port` printed literal 0. normalize_port_maps now resolves a 0 host port to a concrete free ephemeral port (claimed briefly via a TcpListener), so it is forwarded and `port` shows the real number. Verified on Linux: `-p 0:80` -> `80/tcp -> 0.0.0.0:36517`.
1 parent 76f5bda commit 493bb07

1 file changed

Lines changed: 41 additions & 1 deletion

File tree

src/cli/src/commands/common.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,32 @@ pub(crate) fn effective_stop_signal(
330330

331331
/// Validate and normalize published port mappings to the runtime format.
332332
pub(crate) fn normalize_port_maps(entries: &[String]) -> Result<Vec<String>, String> {
333-
a3s_box_core::normalize_port_maps(entries)
333+
a3s_box_core::normalize_port_maps(entries)?
334+
.into_iter()
335+
.map(resolve_auto_host_port)
336+
.collect()
337+
}
338+
339+
/// Resolve an auto-assign host port (host part `0`, e.g. `-p 0:80`) to a
340+
/// concrete free ephemeral port by briefly claiming one with a TcpListener, so
341+
/// the shim/passt actually forward it and `port` prints the real number
342+
/// (matching Docker). A non-zero host port is returned unchanged.
343+
fn resolve_auto_host_port(entry: String) -> Result<String, String> {
344+
let Some((host, guest)) = entry.split_once(':') else {
345+
return Ok(entry);
346+
};
347+
if host != "0" {
348+
return Ok(entry);
349+
}
350+
let listener = std::net::TcpListener::bind("0.0.0.0:0")
351+
.map_err(|e| format!("failed to allocate a host port for '{entry}': {e}"))?;
352+
let port = listener
353+
.local_addr()
354+
.map_err(|e| format!("failed to read allocated host port: {e}"))?
355+
.port();
356+
// Release it so the box can bind the port (small TOCTOU window, as Docker has).
357+
drop(listener);
358+
Ok(format!("{port}:{guest}"))
334359
}
335360

336361
/// Reject runtime options that a3s-box cannot enforce yet.
@@ -510,6 +535,21 @@ pub(crate) fn build_resource_limits(
510535
mod tests {
511536
use super::*;
512537

538+
#[test]
539+
fn test_resolve_auto_host_port() {
540+
// A non-zero host port is unchanged.
541+
assert_eq!(
542+
resolve_auto_host_port("8080:80".to_string()).unwrap(),
543+
"8080:80"
544+
);
545+
// `0:80` resolves to a concrete non-zero host port, keeping the guest port.
546+
let resolved = resolve_auto_host_port("0:80".to_string()).unwrap();
547+
let (host, guest) = resolved.split_once(':').unwrap();
548+
assert_eq!(guest, "80");
549+
assert_ne!(host, "0");
550+
assert!(host.parse::<u16>().unwrap() > 0);
551+
}
552+
513553
// --- parse_env_vars tests ---
514554

515555
#[test]

0 commit comments

Comments
 (0)