diff --git a/common/client-core/src/cli_helpers/client_add_gateway.rs b/common/client-core/src/cli_helpers/client_add_gateway.rs index dc4280ba296..67600477162 100644 --- a/common/client-core/src/cli_helpers/client_add_gateway.rs +++ b/common/client-core/src/cli_helpers/client_add_gateway.rs @@ -87,6 +87,7 @@ where user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(common_args.latency_based_selection), common_args.force_tls_gateway, + false, ); tracing::debug!("Gateway selection specification: {selection_spec:?}"); diff --git a/common/client-core/src/cli_helpers/client_init.rs b/common/client-core/src/cli_helpers/client_init.rs index feda3ab8d11..7c20777480f 100644 --- a/common/client-core/src/cli_helpers/client_init.rs +++ b/common/client-core/src/cli_helpers/client_init.rs @@ -136,6 +136,7 @@ where user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(common_args.latency_based_selection), common_args.force_tls_gateway, + false, ); tracing::debug!("Gateway selection specification: {selection_spec:?}"); diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index eea24910d02..24b8dd36598 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -43,6 +43,9 @@ pub enum ClientCoreError { #[error("Invalid URL: {0}")] InvalidUrl(String), + #[error("node doesn't advertise ip addresses : {0}")] + MissingIpAddress(String), + #[cfg(not(target_arch = "wasm32"))] #[error("resolution failed: {0}")] ResolutionFailed(#[from] nym_http_api_client::ResolveError), diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 5bece573fdc..8a371c40707 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -71,21 +71,28 @@ where let mut rng = OsRng; let selected_gateway = match selection_specification { - GatewaySelectionSpecification::UniformRemote { must_use_tls } => { + GatewaySelectionSpecification::UniformRemote { + must_use_tls, + no_hostname, + } => { let gateway = uniformly_random_gateway(&mut rng, &available_gateways, must_use_tls)?; - SelectedGateway::from_topology_node(gateway, must_use_tls)? + SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)? } - GatewaySelectionSpecification::RemoteByLatency { must_use_tls } => { + GatewaySelectionSpecification::RemoteByLatency { + must_use_tls, + no_hostname, + } => { let gateway = choose_gateway_by_latency(&mut rng, &available_gateways, must_use_tls).await?; - SelectedGateway::from_topology_node(gateway, must_use_tls)? + SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)? } GatewaySelectionSpecification::Specified { must_use_tls, + no_hostname, identity, } => { let gateway = get_specified_gateway(&identity, &available_gateways, must_use_tls)?; - SelectedGateway::from_topology_node(gateway, must_use_tls)? + SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)? } GatewaySelectionSpecification::Custom { gateway_identity, diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 8f09980a43e..4bd8b2c72b4 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -42,24 +42,32 @@ impl SelectedGateway { pub fn from_topology_node( node: RoutingNode, must_use_tls: bool, + no_hostname: bool, ) -> Result { // for now, let's use 'old' behaviour, if you want to change it, you can pass it up the enum stack yourself : ) let prefer_ipv6 = false; - let gateway_listener = if must_use_tls { - node.ws_entry_address_tls() - .ok_or(ClientCoreError::UnsupportedWssProtocol { - gateway: node.identity_key.to_base58_string(), - })? + let (gateway_listener, _) = if must_use_tls { + // WSS main, no fallback + let primary = + node.ws_entry_address_tls() + .ok_or(ClientCoreError::UnsupportedWssProtocol { + gateway: node.identity_key.to_base58_string(), + })?; + (primary, None) } else { - node.ws_entry_address(prefer_ipv6) - .ok_or(ClientCoreError::UnsupportedEntry { + let (maybe_primary, fallback) = + node.ws_entry_address_with_fallback(prefer_ipv6, no_hostname); + ( + maybe_primary.ok_or(ClientCoreError::UnsupportedEntry { id: node.node_id, identity: node.identity_key.to_base58_string(), - })? + })?, + fallback, + ) }; - let gateway_listener = + let gateway_listener_url = Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener { gateway_id: node.identity_key.to_base58_string(), raw_listener: gateway_listener, @@ -69,7 +77,7 @@ impl SelectedGateway { Ok(SelectedGateway::Remote { gateway_id: node.identity_key, gateway_owner_address: None, - gateway_listener, + gateway_listener: gateway_listener_url, }) } @@ -150,15 +158,22 @@ impl InitialisationResult { #[derive(Clone, Debug)] pub enum GatewaySelectionSpecification { /// Uniformly choose a random remote gateway. - UniformRemote { must_use_tls: bool }, + UniformRemote { + must_use_tls: bool, + no_hostname: bool, + }, /// Should the new, remote, gateway be selected based on latency. - RemoteByLatency { must_use_tls: bool }, + RemoteByLatency { + must_use_tls: bool, + no_hostname: bool, + }, /// Gateway with this specific identity should be chosen. // JS: I don't really like the name of this enum variant but couldn't think of anything better at the time Specified { must_use_tls: bool, + no_hostname: bool, identity: IdentityKey, }, @@ -174,6 +189,7 @@ impl Default for GatewaySelectionSpecification { fn default() -> Self { GatewaySelectionSpecification::UniformRemote { must_use_tls: false, + no_hostname: false, } } } @@ -183,16 +199,24 @@ impl GatewaySelectionSpecification { gateway_identity: Option, latency_based_selection: Option, must_use_tls: bool, + no_hostname: bool, ) -> Self { if let Some(identity) = gateway_identity { GatewaySelectionSpecification::Specified { identity, must_use_tls, + no_hostname, } } else if let Some(true) = latency_based_selection { - GatewaySelectionSpecification::RemoteByLatency { must_use_tls } + GatewaySelectionSpecification::RemoteByLatency { + must_use_tls, + no_hostname, + } } else { - GatewaySelectionSpecification::UniformRemote { must_use_tls } + GatewaySelectionSpecification::UniformRemote { + must_use_tls, + no_hostname, + } } } } diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index c542d93e04b..85cb81b51f0 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -89,6 +89,45 @@ impl RoutingNode { self.ws_entry_address_no_tls(prefer_ipv6) } + pub fn ws_entry_address_with_fallback( + &self, + prefer_ipv6: bool, + no_hostname: bool, + ) -> (Option, Option) { + let Some(entry) = &self.entry else { + return (None, None); + }; + + // Put hostname first if we want it + let maybe_hostname = if !no_hostname { + entry.hostname.clone() + } else { + None + }; + + // Put ipv6 first or keep them as is + let ips: Vec<&IpAddr> = if prefer_ipv6 { + entry + .ip_addresses + .iter() + .filter(|ip| ip.is_ipv6()) + .chain(entry.ip_addresses.iter().filter(|ip| ip.is_ipv4())) + .collect() + } else { + entry.ip_addresses.iter().collect() + }; + + // chain everything and keep the top two as ws addresses + let ws_addresses: Vec<_> = maybe_hostname + .into_iter() + .chain(ips.into_iter().map(|ip| ip.to_string())) + .take(2) + .map(|host| format!("ws://{host}:{}", entry.clients_ws_port)) + .collect(); + + (ws_addresses.first().cloned(), ws_addresses.get(1).cloned()) + } + pub fn identity(&self) -> ed25519::PublicKey { self.identity_key } diff --git a/nym-registration-client/src/builder/config.rs b/nym-registration-client/src/builder/config.rs index f5fac943618..d566c397f0f 100644 --- a/nym-registration-client/src/builder/config.rs +++ b/nym-registration-client/src/builder/config.rs @@ -119,6 +119,7 @@ impl BuilderConfig { .network_details(self.network_env) .debug_config(debug_config) .credentials_mode(true) + .no_hostname(true) .with_remember_me(remember_me) .custom_topology_provider(self.custom_topology_provider); diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 27c9983f7fd..a456bd5dfbb 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -56,6 +56,7 @@ pub struct MixnetClientBuilder { custom_shutdown: Option, event_tx: Option, force_tls: bool, + no_hostname: bool, user_agent: Option, #[cfg(unix)] connection_fd_callback: Option>, @@ -101,6 +102,7 @@ impl MixnetClientBuilder { event_tx: None, custom_gateway_transceiver: None, force_tls: false, + no_hostname: false, user_agent: None, #[cfg(unix)] connection_fd_callback: None, @@ -134,6 +136,7 @@ where custom_shutdown: None, event_tx: None, force_tls: false, + no_hostname: false, user_agent: None, #[cfg(unix)] connection_fd_callback: None, @@ -158,6 +161,7 @@ where custom_shutdown: self.custom_shutdown, event_tx: self.event_tx, force_tls: self.force_tls, + no_hostname: self.no_hostname, user_agent: self.user_agent, #[cfg(unix)] connection_fd_callback: self.connection_fd_callback, @@ -229,6 +233,13 @@ where self } + /// Attempt to only choose a gateway with its IP address only, ignored if force_tls is set + #[must_use] + pub fn no_hostname(mut self, no_hostname: bool) -> Self { + self.no_hostname = no_hostname; + self + } + /// Enable paid coconut bandwidth credentials mode. #[must_use] pub fn enable_credentials_mode(mut self) -> Self { @@ -341,6 +352,7 @@ where client.custom_shutdown = self.custom_shutdown; client.wait_for_gateway = self.wait_for_gateway; client.force_tls = self.force_tls; + client.no_hostname = self.no_hostname; client.user_agent = self.user_agent; #[cfg(unix)] if self.connection_fd_callback.is_some() { @@ -393,6 +405,9 @@ where /// Force the client to connect using wss protocol with the gateway. force_tls: bool, + /// Force the client to pick gateway IP and not hostname, ignored if force_tls is set + no_hostname: bool, + /// Allows passing an externally controlled shutdown handle. custom_shutdown: Option, @@ -461,6 +476,7 @@ where custom_gateway_transceiver: None, wait_for_gateway: false, force_tls: false, + no_hostname: false, custom_shutdown: None, event_tx, user_agent: None, @@ -580,6 +596,7 @@ where self.config.user_chosen_gateway.clone(), None, self.force_tls, + self.no_hostname, ); let available_gateways = self.available_gateways().await?;