diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 05e22e0cfb372..101f1eac29f91 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -230,8 +230,8 @@ pub struct CommandShellArgs { #[clap(long)] pub on_socket: bool, /// Listen on a host/port instead of stdin/stdout. - #[clap(long, num_args = 0..=1, default_missing_value = "0")] - pub on_port: Option, + #[clap(long, num_args = 0..=2, default_missing_value = "0")] + pub on_port: Vec, /// Listen on a host/port instead of stdin/stdout. #[clap[long]] pub on_host: Option, diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 688f603f5934c..754ce27f0e4bc 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -155,43 +155,52 @@ pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Resul code_server_args: (&ctx.args).into(), }; - let mut listener: Box = match (args.on_port, &args.on_host, args.on_socket) - { - (_, _, true) => { - let socket = get_socket_name(); - let listener = listen_socket_rw_stream(&socket) - .await - .map_err(|e| wrap(e, "error listening on socket"))?; - - params - .log - .result(format!("Listening on {}", socket.display())); - - Box::new(listener) - } - (Some(_), _, _) | (_, Some(_), _) => { - let addr = SocketAddr::new( - args.on_host + let mut listener: Box = + match (args.on_port.get(0), &args.on_host, args.on_socket) { + (_, _, true) => { + let socket = get_socket_name(); + let listener = listen_socket_rw_stream(&socket) + .await + .map_err(|e| wrap(e, "error listening on socket"))?; + + params + .log + .result(format!("Listening on {}", socket.display())); + + Box::new(listener) + } + (Some(_), _, _) | (_, Some(_), _) => { + let host = args + .on_host .as_ref() .map(|h| h.parse().map_err(CodeError::InvalidHostAddress)) - .unwrap_or(Ok(IpAddr::V4(Ipv4Addr::LOCALHOST)))?, - args.on_port.unwrap_or_default(), - ); - let listener = tokio::net::TcpListener::bind(addr) - .await - .map_err(|e| wrap(e, "error listening on port"))?; - - params - .log - .result(format!("Listening on {}", listener.local_addr().unwrap())); - - Box::new(listener) - } - _ => { - serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; - return Ok(0); - } - }; + .unwrap_or(Ok(IpAddr::V4(Ipv4Addr::LOCALHOST)))?; + + let lower_port = args.on_port.get(0).map(|p| *p).unwrap_or_default(); + let port_no = if let Some(upper) = args.on_port.get(1) { + find_unused_port(&host, lower_port, *upper) + .await + .unwrap_or_default() + } else { + lower_port + }; + + let addr = SocketAddr::new(host, port_no); + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(|e| wrap(e, "error listening on port"))?; + + params + .log + .result(format!("Listening on {}", listener.local_addr().unwrap())); + + Box::new(listener) + } + _ => { + serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; + return Ok(0); + } + }; let mut servers = FuturesUnordered::new(); @@ -216,6 +225,21 @@ pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Resul } } +async fn find_unused_port(host: &IpAddr, start_port: u16, end_port: u16) -> Option { + for port in start_port..=end_port { + if is_port_available(host.clone(), port).await { + return Some(port); + } + } + None +} + +async fn is_port_available(host: IpAddr, port: u16) -> bool { + tokio::net::TcpListener::bind(SocketAddr::new(host, port)) + .await + .is_ok() +} + pub async fn service( ctx: CommandContext, service_args: TunnelServiceSubCommands,