diff --git a/src/cmd/create/public.rs b/src/cmd/create/public.rs index 1fc44c5..345df97 100644 --- a/src/cmd/create/public.rs +++ b/src/cmd/create/public.rs @@ -2,7 +2,7 @@ use crate::{ cmd::create::CreateCommon, config::{Client, ClientType, Config}, http::{create_client, HttpOptions}, - server::Server, + server::{Bind, Server}, utils::OrNone, }; use anyhow::bail; @@ -30,6 +30,18 @@ pub struct CreatePublic { #[arg(short, long)] pub open: bool, + /// Choose how to bind the local server + #[arg(short, long, env = "BIND_MODE", value_enum, default_value_t = Bind::Prefer6)] + pub bind: Bind, + + /// Use IPv4 only binding (equivalent to --bind only4) + #[arg(short = '4', conflicts_with_all = ["bind", "only6"])] + pub only4: bool, + + /// Use IPv6 only binding (equivalent to --bind only6) + #[arg(short = '6', conflicts_with = "bind")] + pub only6: bool, + #[command(flatten)] pub http: HttpOptions, } @@ -47,7 +59,7 @@ impl CreatePublic { ); } - let server = Server::new(self.port).await?; + let server = Server::new(self.bind_mode(), self.port).await?; let redirect = format!("http://localhost:{}", server.port); let client = create_client(&self.http).await?; @@ -105,4 +117,14 @@ Open the following URL in your browser and perform the interactive login process Ok(()) } + + fn bind_mode(&self) -> Bind { + if self.only4 { + Bind::Only4 + } else if self.only6 { + Bind::Only6 + } else { + self.bind + } + } } diff --git a/src/server.rs b/src/server.rs index edabd0e..01d54f7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,6 +9,54 @@ use tokio::{ sync::{oneshot, Mutex}, }; +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, clap::ValueEnum)] +pub enum Bind { + /// Try IPv6 first, then fall back to IPv4 + #[default] + Prefer6, + /// Try IPv4 first, then fall back to IPv6 + Prefer4, + /// Only try IPv6 + Only6, + /// Only try IPv4 + Only4, +} + +impl Bind { + pub async fn into_acceptor(self, port: u16) -> anyhow::Result { + Ok(match self { + Self::Prefer6 => match TcpListener::bind((Ipv6Addr::LOCALHOST, port)).await { + Ok(acceptor) => acceptor, + Err(err) => { + log::info!("Failed to bind to IPv6 localhost, trying IPv4 instead: {err}"); + match TcpListener::bind((Ipv4Addr::LOCALHOST, port)).await { + Ok(acceptor) => acceptor, + Err(err) => { + log::error!("Failed to bind to either IPv6 or IPv4: {err}"); + bail!("Unable to bind to IPv4 or IPv6: {err}"); + } + } + } + }, + Self::Prefer4 => match TcpListener::bind((Ipv4Addr::LOCALHOST, port)).await { + Ok(acceptor) => acceptor, + Err(err) => { + log::info!("Failed to bind to IPv4 localhost, trying IPv6 instead: {err}"); + match TcpListener::bind((Ipv6Addr::LOCALHOST, port)).await { + Ok(acceptor) => acceptor, + Err(err) => { + log::error!("Failed to bind to either IPv6 or IPv4: {err}"); + bail!("Unable to bind to IPv4 or IPv6: {err}"); + } + } + } + }, + Self::Only6 => TcpListener::bind((Ipv6Addr::LOCALHOST, port)).await?, + Self::Only4 => TcpListener::bind((Ipv4Addr::LOCALHOST, port)).await?, + }) + } +} + pub struct FlowResult { pub code: String, } @@ -48,24 +96,12 @@ async fn receive( } impl Server { - pub async fn new(port: Option) -> anyhow::Result { + pub async fn new(bind: Bind, port: Option) -> anyhow::Result { let (tx, rx) = oneshot::channel(); let port = port.unwrap_or_default(); - let acceptor = match TcpListener::bind((Ipv6Addr::LOCALHOST, port)).await { - Ok(acceptor) => acceptor, - Err(err) => { - log::info!("Failed to bind to IPv6 localhost, trying IPv4 instead: {err}"); - match TcpListener::bind((Ipv4Addr::LOCALHOST, port)).await { - Ok(acceptor) => acceptor, - Err(err) => { - log::error!("Failed to bind to either IPv6 or IPv4: {err}"); - bail!("Unable to bind to IPv4 or IPv6: {err}"); - } - } - } - }; + let acceptor = bind.into_acceptor(port).await?; let acceptor = acceptor.into_std()?; let port = acceptor.local_addr()?.port();