diff --git a/src/client/conn.rs b/src/client/conn.rs index 7fd2cbbe..486d263b 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -113,7 +113,14 @@ impl Connection { #[cfg(not(feature = "proxy"))] async fn new_stream(config: &Config) -> error::Result { - Ok(TcpStream::connect((config.server()?, config.port())).await?) + let server = config.server()?; + let port = config.port(); + + if let Some(bind_addr) = config.bind_address() { + return Self::connect_with_bind(server, port, bind_addr, config).await; + } + + Ok(TcpStream::connect((server, port)).await?) } #[cfg(feature = "proxy")] @@ -123,8 +130,18 @@ impl Connection { let address = (server, port); match config.proxy_type() { - ProxyType::None => Ok(TcpStream::connect(address).await?), + ProxyType::None => { + if let Some(bind_addr) = config.bind_address() { + return Self::connect_with_bind(server, port, bind_addr, config).await; + } + Ok(TcpStream::connect(address).await?) + } ProxyType::Socks5 => { + if config.bind_address().is_some() { + log::warn!( + "bind_address is not supported with SOCKS5 proxy and will be ignored." + ); + } let proxy_server = config.proxy_server(); let proxy_port = config.proxy_port(); let proxy = (proxy_server, proxy_port); @@ -149,6 +166,48 @@ impl Connection { } } + async fn connect_with_bind( + server: &str, + port: u16, + bind_addr: &str, + config: &Config, + ) -> error::Result { + use std::net::{IpAddr, SocketAddr}; + use tokio::net::TcpSocket; + + let bind_ip: IpAddr = bind_addr.parse().map_err(|_| error::Error::InvalidConfig { + path: config.path(), + cause: error::ConfigError::InvalidBindAddress { + address: bind_addr.to_string(), + }, + })?; + + let socket = if bind_ip.is_ipv4() { + TcpSocket::new_v4()? + } else { + TcpSocket::new_v6()? + }; + + socket.bind(SocketAddr::new(bind_ip, 0))?; + + let remote_addr = tokio::net::lookup_host((server, port)) + .await? + .find(|addr| addr.is_ipv4() == bind_ip.is_ipv4()) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::AddrNotAvailable, + format!( + "no {} address found for {}", + if bind_ip.is_ipv4() { "IPv4" } else { "IPv6" }, + server + ), + ) + })?; + + log::info!("Binding to {} for connection to {}.", bind_addr, server); + Ok(socket.connect(remote_addr).await?) + } + async fn new_unsecured_transport( config: &Config, tx: UnboundedSender, diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 9f65b2bc..55db36c4 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -97,6 +97,11 @@ pub struct Config { /// The port to connect on. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub port: Option, + /// The local address to bind to when connecting to the server. + /// This is useful for machines with multiple network interfaces or IP addresses. + /// The value should be a valid IPv4 or IPv6 address (e.g. "192.168.1.100" or "::1"). + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub bind_address: Option, /// The password to connect to the server. #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub password: Option, @@ -461,6 +466,12 @@ impl Config { self.port.as_ref().cloned().unwrap_or(6667) } + /// Gets the local address to bind to when connecting. + /// This defaults to `None`, meaning the OS will choose the local address. + pub fn bind_address(&self) -> Option<&str> { + self.bind_address.as_deref() + } + /// Gets the server password specified in the configuration. /// This defaults to an empty string when not specified. pub fn password(&self) -> &str { diff --git a/src/error.rs b/src/error.rs index 21b25b86..5318756b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -175,6 +175,13 @@ pub enum ConfigError { /// The supposed location of the file. file: String, }, + + /// The specified bind address could not be parsed as a valid IP address. + #[error("invalid bind address: {}", address)] + InvalidBindAddress { + /// The invalid address string. + address: String, + }, } /// A wrapper that combines toml's serialization and deserialization errors.