diff --git a/server/src/chat/transport/matrix/new_project.rs b/server/src/chat/transport/matrix/new_project.rs index 23778380..66578d8e 100644 --- a/server/src/chat/transport/matrix/new_project.rs +++ b/server/src/chat/transport/matrix/new_project.rs @@ -1440,12 +1440,18 @@ fn interpret_docker_run_error(stderr: &str, image: &str) -> String { } } +/// Host ports reserved for the sled SSH range and never handed out as a +/// newly allocated `ssh_port`, even when momentarily bindable during a scan. +const RESERVED_SSH_PORTS: std::ops::RangeInclusive = 2200..=2202; + /// Scan `start..start+range` for a bindable TCP port on 127.0.0.1. /// /// Returns `Some(port)` for the first port that can be bound, or `None` if all -/// ports in the range are occupied. +/// ports in the range are occupied. Ports in [`RESERVED_SSH_PORTS`] are never +/// returned, even if bindable at scan time. fn find_free_port_in_range(start: u16, range: u16) -> Option { (start..start.saturating_add(range)) + .filter(|port| !RESERVED_SSH_PORTS.contains(port)) .find(|&port| std::net::TcpListener::bind(("127.0.0.1", port)).is_ok()) } @@ -1720,6 +1726,21 @@ mod tests { assert_eq!(find_free_port_in_range(port, 1), None); } + #[test] + fn find_free_port_skips_reserved_ssh_range() { + // The reserved 2200-2202 sled SSH range must never be handed out as + // a newly allocated ssh_port, even when those ports are bindable at + // scan time. Loop many times to rule out a flaky, timing-dependent + // pass rather than trusting a single lucky draw. + for _ in 0..100 { + let port = find_free_port(2200).expect("expected Some(port) in scan range"); + assert!( + !RESERVED_SSH_PORTS.contains(&port), + "returned port {port} falls within the reserved 2200-2202 SSH range" + ); + } + } + #[test] fn detect_stack_go_mod() { let dir = tempfile::tempdir().unwrap();