diff --git a/Cargo.lock b/Cargo.lock index 784ed126c4..c7bba3af0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5254,7 +5254,9 @@ dependencies = [ "cosmrs", "nym-crypto", "nym-gateway-requests", + "nym-topology", "serde", + "serde_json", "sqlx", "thiserror 2.0.12", "time", @@ -5872,6 +5874,7 @@ dependencies = [ "nym-sphinx", "nym-statistics-common", "nym-task", + "nym-topology", "nym-validator-client", "rand 0.8.5", "serde", diff --git a/common/client-core/gateways-storage/Cargo.toml b/common/client-core/gateways-storage/Cargo.toml index 938a6db5dc..f069869b09 100644 --- a/common/client-core/gateways-storage/Cargo.toml +++ b/common/client-core/gateways-storage/Cargo.toml @@ -11,6 +11,7 @@ rust-version.workspace = true async-trait.workspace = true cosmrs.workspace = true serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true thiserror.workspace = true time.workspace = true tokio = { workspace = true, features = ["sync"] } @@ -20,6 +21,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } nym-crypto = { path = "../../crypto", features = ["asymmetric"] } nym-gateway-requests = { path = "../../gateway-requests" } +nym-topology = { path = "../../topology", features = ["persistence"] } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true diff --git a/common/client-core/gateways-storage/src/error.rs b/common/client-core/gateways-storage/src/error.rs index 2c251f513d..74874c9a96 100644 --- a/common/client-core/gateways-storage/src/error.rs +++ b/common/client-core/gateways-storage/src/error.rs @@ -48,6 +48,6 @@ pub enum BadGateway { raw_listener: String, #[source] - source: url::ParseError, + source: serde_json::Error, }, } diff --git a/common/client-core/gateways-storage/src/types.rs b/common/client-core/gateways-storage/src/types.rs index 477e9df99f..a2c162acc3 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -5,13 +5,13 @@ use crate::BadGateway; use cosmrs::AccountId; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; +use nym_topology::EntryDetails; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; use time::OffsetDateTime; -use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; pub const REMOTE_GATEWAY_TYPE: &str = "remote"; @@ -67,7 +67,7 @@ impl GatewayDetails { gateway_id: ed25519::PublicKey, shared_key: Arc, gateway_owner_address: Option, - gateway_listener: Url, + gateway_listener: EntryDetails, ) -> Self { GatewayDetails::Remote(RemoteGatewayDetails { gateway_id, @@ -164,8 +164,7 @@ pub struct RegisteredGateway { pub gateway_type: GatewayType, } -#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] -#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +#[derive(Debug, Serialize, Deserialize)] pub struct RawRemoteGatewayDetails { pub gateway_id_bs58: String, pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, @@ -174,6 +173,16 @@ pub struct RawRemoteGatewayDetails { pub gateway_listener: String, } +impl Zeroize for RawRemoteGatewayDetails { + fn zeroize(&mut self) { + self.gateway_id_bs58.zeroize(); + self.derived_aes128_ctr_blake3_hmac_keys_bs58.zeroize(); + self.derived_aes256_gcm_siv_key.zeroize(); + self.gateway_owner_address.zeroize(); + } +} +impl ZeroizeOnDrop for RawRemoteGatewayDetails {} + impl TryFrom for RemoteGatewayDetails { type Error = BadGateway; @@ -230,7 +239,7 @@ impl TryFrom for RemoteGatewayDetails { }) .transpose()?; - let gateway_listener = Url::parse(&value.gateway_listener).map_err(|source| { + let gateway_listener = serde_json::from_str(&value.gateway_listener).map_err(|source| { BadGateway::MalformedListener { gateway_id: value.gateway_id_bs58.clone(), raw_listener: value.gateway_listener.clone(), @@ -255,12 +264,16 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None), }; + #[allow(clippy::expect_used)] + let gateway_listener = serde_json::to_string(&value.gateway_listener) + .expect("serialize for gateway details should never fail"); + RawRemoteGatewayDetails { gateway_id_bs58: value.gateway_id.to_base58_string(), derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, gateway_owner_address: value.gateway_owner_address.as_ref().map(|o| o.to_string()), - gateway_listener: value.gateway_listener.to_string(), + gateway_listener, } } } @@ -273,7 +286,7 @@ pub struct RemoteGatewayDetails { pub gateway_owner_address: Option, - pub gateway_listener: Url, + pub gateway_listener: EntryDetails, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index e414e81ed5..2903a91a3d 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -561,7 +561,7 @@ where .gateway_owner_address .as_ref() .map(|o| o.to_string()), - details.gateway_listener.to_string(), + details.gateway_listener, ); GatewayClient::new( GatewayClientConfig::new_default() diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index 1b90208b6d..efcda552a2 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -436,7 +436,7 @@ where } } - if let Some(ref mut next_delay) = &mut self.next_delay { + if let Some(next_delay) = &mut self.next_delay { // it is not yet time to return a message if next_delay.as_mut().poll(cx).is_pending() { return Poll::Pending; diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index eea24910d0..c7b4c3ee1b 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -40,8 +40,8 @@ pub enum ClientCoreError { #[error("no gateway with id: {0}")] NoGatewayWithId(String), - #[error("Invalid URL: {0}")] - InvalidUrl(String), + #[error("Invalid Endpoint: {0}")] + InvalidEndpoint(String), #[cfg(not(target_arch = "wasm32"))] #[error("resolution failed: {0}")] diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 72d310bfcc..95b685826c 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -21,7 +21,7 @@ use url::Url; #[cfg(not(target_arch = "wasm32"))] use crate::init::websockets::connect_async; -use nym_topology::NodeId; +use nym_topology::{EntryDetails, NodeId}; #[cfg(not(target_arch = "wasm32"))] use tokio::net::TcpStream; #[cfg(not(target_arch = "wasm32"))] @@ -55,7 +55,7 @@ const PING_TIMEOUT: Duration = Duration::from_millis(1000); pub trait ConnectableGateway { fn node_id(&self) -> NodeId; fn identity(&self) -> ed25519::PublicKey; - fn clients_address(&self, prefer_ipv6: bool) -> Option; + fn endpoint_details(&self) -> Option; fn is_wss(&self) -> bool; } @@ -68,8 +68,8 @@ impl ConnectableGateway for RoutingNode { self.identity_key } - fn clients_address(&self, prefer_ipv6: bool) -> Option { - self.ws_entry_address(prefer_ipv6) + fn endpoint_details(&self) -> Option { + self.entry.clone() } fn is_wss(&self) -> bool { @@ -191,7 +191,7 @@ pub async fn gateways_for_init( } #[cfg(not(target_arch = "wasm32"))] -async fn connect(endpoint: &str) -> Result { +async fn connect(endpoint: &EntryDetails) -> Result { match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await { Err(_elapsed) => Err(ClientCoreError::GatewayConnectionTimeout), Ok(Err(conn_failure)) => Err(conn_failure), @@ -208,17 +208,17 @@ async fn measure_latency(gateway: &G) -> Result, Cl where G: ConnectableGateway, { - let Some(addr) = gateway.clients_address(false) else { + let Some(endpoint) = gateway.endpoint_details() else { return Err(ClientCoreError::UnsupportedEntry { id: gateway.node_id(), identity: gateway.identity().to_string(), }); }; trace!( - "establishing connection to {} ({addr})...", + "establishing connection to {} ({endpoint})...", gateway.identity(), ); - let mut stream = connect(&addr).await?; + let mut stream = connect(&endpoint).await?; let mut results = Vec::new(); for _ in 0..MEASUREMENTS { @@ -379,7 +379,7 @@ pub(super) fn get_specified_gateway( pub(super) async fn register_with_gateway( gateway_id: ed25519::PublicKey, - gateway_listener: Url, + gateway_listener: EntryDetails, our_identity: Arc, #[cfg(unix)] connection_fd_callback: Option>, ) -> Result { diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 8f09980a43..f3ce124bd6 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -14,6 +14,7 @@ use nym_gateway_client::client::InitGatewayClient; use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_sphinx::addressing::clients::Recipient; use nym_topology::node::RoutingNode; +use nym_topology::EntryDetails; use nym_validator_client::client::IdentityKey; use nym_validator_client::nyxd::AccountId; use serde::Serialize; @@ -22,7 +23,6 @@ use std::fmt::{Debug, Display}; use std::os::fd::RawFd; use std::sync::Arc; use time::OffsetDateTime; -use url::Url; pub enum SelectedGateway { Remote { @@ -30,7 +30,7 @@ pub enum SelectedGateway { gateway_owner_address: Option, - gateway_listener: Url, + gateway_listener: EntryDetails, }, Custom { gateway_id: ed25519::PublicKey, @@ -46,25 +46,25 @@ impl SelectedGateway { // 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(), - })? - } else { - node.ws_entry_address(prefer_ipv6) - .ok_or(ClientCoreError::UnsupportedEntry { - id: node.node_id, - identity: node.identity_key.to_base58_string(), - })? - }; + let gateway_listener = node.entry.ok_or(ClientCoreError::UnsupportedEntry { + id: node.node_id, + identity: node.identity_key.to_base58_string(), + })?; - let gateway_listener = - Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener { - gateway_id: node.identity_key.to_base58_string(), - raw_listener: gateway_listener, - source, - })?; + if must_use_tls + && (gateway_listener.hostname.is_none() || gateway_listener.clients_wss_port.is_none()) + { + return Err(ClientCoreError::UnsupportedWssProtocol { + gateway: node.identity_key.to_base58_string(), + }); + } + + if prefer_ipv6 && gateway_listener.ip_addresses.iter().all(|i| i.is_ipv4()) { + return Err(ClientCoreError::UnsupportedEntry { + id: node.node_id, + identity: node.identity_key.to_base58_string(), + }); + } Ok(SelectedGateway::Remote { gateway_id: node.identity_key, diff --git a/common/client-core/src/init/websockets.rs b/common/client-core/src/init/websockets.rs index ba407623a6..eab418dddd 100644 --- a/common/client-core/src/init/websockets.rs +++ b/common/client-core/src/init/websockets.rs @@ -1,43 +1,60 @@ use crate::error::ClientCoreError; -use nym_http_api_client::HickoryDnsResolver; +#[cfg(not(target_arch = "wasm32"))] +use nym_topology::EntryDetails; use tokio::net::TcpStream; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tungstenite::handshake::client::Response; -use url::{Host, Url}; +use url::Url; use std::net::SocketAddr; #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn connect_async( - endpoint: &str, + endpoint: &EntryDetails, ) -> Result<(WebSocketStream>, Response), ClientCoreError> { - let resolver = HickoryDnsResolver::default(); - let uri = Url::parse(endpoint).map_err(|_| ClientCoreError::InvalidUrl(endpoint.to_owned()))?; + let uri = ws_entry_address(endpoint, false) + .ok_or(ClientCoreError::InvalidEndpoint(endpoint.to_string()))?; let port: u16 = uri.port_or_known_default().unwrap_or(443); - let host = uri - .host() - .ok_or(ClientCoreError::InvalidUrl(endpoint.to_owned()))?; - - // Get address for tcp connection, if a domain is provided use our preferred resolver rather than - // the default std resolve - let sock_addrs: Vec = match host { - Host::Ipv4(addr) => vec![SocketAddr::new(addr.into(), port)], - Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)], - Host::Domain(domain) => { - // Do a DNS lookup for the domain using our custom DNS resolver - resolver - .resolve_str(domain) - .await? - .map(|a| SocketAddr::new(a, port)) - .collect() - } - }; + let sock_addrs: Vec = endpoint + .ip_addresses + .iter() + .map(|addr| SocketAddr::new(addr.clone(), port)) + .collect(); let stream = TcpStream::connect(&sock_addrs[..]).await?; - tokio_tungstenite::client_async_tls(endpoint, stream) + tokio_tungstenite::client_async_tls(uri.to_string(), stream) .await .map_err(Into::into) } + +pub fn ws_entry_address_tls(entry: &EntryDetails) -> Option { + let hostname = entry.hostname.as_ref()?; + let wss_port = entry.clients_wss_port?; + + Url::parse(&format!("wss://{hostname}:{wss_port}")).ok() +} + +pub fn ws_entry_address_no_tls(entry: &EntryDetails, prefer_ipv6: bool) -> Option { + if let Some(hostname) = entry.hostname.as_ref() { + return Url::parse(&format!("ws://{hostname}:{}", entry.clients_ws_port)).ok(); + } + + if prefer_ipv6 { + if let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) { + return Url::parse(&format!("ws://{ipv6}:{}", entry.clients_ws_port)).ok(); + } + } + + let any_ip = entry.ip_addresses.first()?; + Url::parse(&format!("ws://{any_ip}:{}", entry.clients_ws_port)).ok() +} + +pub fn ws_entry_address(entry: &EntryDetails, prefer_ipv6: bool) -> Option { + if let Some(tls) = ws_entry_address_tls(entry) { + return Some(tls); + } + ws_entry_address_no_tls(entry, prefer_ipv6) +} diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index efc699473a..c2e73d074c 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -32,6 +32,7 @@ nym-network-defaults = { path = "../../network-defaults" } nym-sphinx = { path = "../../nymsphinx" } nym-statistics-common = { path = "../../statistics" } nym-pemstore = { path = "../../pemstore" } +nym-topology = { path = "../../topology", features = ["persistence"] } nym-validator-client = { path = "../validator-client", default-features = false } nym-task = { path = "../../task" } serde = { workspace = true, features = ["derive"] } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 52e5833eb2..33936b1812 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -28,13 +28,13 @@ use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::clients::connection::ConnectionStatsEvent; use nym_statistics_common::clients::ClientStatsSender; use nym_task::ShutdownToken; +use nym_topology::EntryDetails; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use rand::rngs::OsRng; use std::sync::Arc; use tracing::instrument; use tracing::*; use tungstenite::protocol::Message; -use url::Url; #[cfg(unix)] use std::os::fd::RawFd; @@ -62,14 +62,14 @@ pub struct GatewayConfig { // currently a dead field pub gateway_owner: Option, - pub gateway_listener: String, + pub gateway_listener: EntryDetails, } impl GatewayConfig { pub fn new( gateway_identity: ed25519::PublicKey, gateway_owner: Option, - gateway_listener: String, + gateway_listener: EntryDetails, ) -> Self { GatewayConfig { gateway_identity, @@ -92,7 +92,7 @@ pub struct GatewayClient { authenticated: bool, bandwidth: ClientBandwidth, - gateway_address: String, + gateway_address: EntryDetails, gateway_identity: ed25519::PublicKey, local_identity: Arc, shared_key: Option>, @@ -201,7 +201,7 @@ impl GatewayClient { #[cfg(not(target_arch = "wasm32"))] pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { debug!( - "Attempting to establish connection to gateway at: {}", + "Attempting to establish connection to gateway at: {:?}", self.gateway_address ); let (ws_stream, _) = connect_async( @@ -467,7 +467,9 @@ impl GatewayClient { // right now there are no failure cases here, but this might change in the future match gateway_protocol { None => { - warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); + warn!( + "the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0" + ); // note: in +1.2.0 we will have to return a hard error here Ok(()) } @@ -481,7 +483,9 @@ impl GatewayClient { } Some(_) => { - debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); + debug!( + "the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!" + ); Ok(()) } } @@ -527,7 +531,7 @@ impl GatewayClient { status, } => (status, protocol_version), ServerResponse::Error { message } => { - return Err(GatewayClientError::GatewayError(message)) + return Err(GatewayClientError::GatewayError(message)); } other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }; @@ -589,7 +593,7 @@ impl GatewayClient { { ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce), ServerResponse::Error { message } => { - return Err(GatewayClientError::GatewayError(message)) + return Err(GatewayClientError::GatewayError(message)); } other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }; @@ -858,7 +862,9 @@ impl GatewayClient { warn!("Not enough bandwidth. Trying to get more bandwidth, this might take a while"); if !self.cfg.bandwidth.require_tickets { - info!("The client is running in disabled credentials mode - attempting to claim bandwidth without a credential"); + info!( + "The client is running in disabled credentials mode - attempting to claim bandwidth without a credential" + ); return self.try_claim_testnet_bandwidth().await; } @@ -896,7 +902,9 @@ impl GatewayClient { Err(err) => { error!("failed to claim ecash bandwidth with the gateway...: {err}"); if err.is_ticket_replay() { - warn!("this was due to our ticket being replayed! have you messed with the database file?") + warn!( + "this was due to our ticket being replayed! have you messed with the database file?" + ) } else { // TODO: tracing span info!("attempting to revert ticket withdrawal..."); @@ -1112,7 +1120,9 @@ impl GatewayClient { self.cfg .bandwidth .ensure_above_cutoff(bandwidth_remaining)?; - info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen."); + info!( + "Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen." + ); self.claim_bandwidth().await?; } Ok(()) @@ -1128,7 +1138,7 @@ pub struct InitOnly; impl GatewayClient { // for initialisation we do not need credential storage. Though it's still a bit weird we have to set the generic... pub fn new_init( - gateway_listener: Url, + gateway_listener: EntryDetails, gateway_identity: ed25519::PublicKey, local_identity: Arc, #[cfg(unix)] connection_fd_callback: Option>, @@ -1147,7 +1157,7 @@ impl GatewayClient { cfg: GatewayClientConfig::default().with_disabled_credentials_mode(true), authenticated: false, bandwidth: ClientBandwidth::new_empty(), - gateway_address: gateway_listener.to_string(), + gateway_address: gateway_listener.clone(), gateway_identity, local_identity, shared_key: None, diff --git a/common/client-libs/gateway-client/src/client/websockets.rs b/common/client-libs/gateway-client/src/client/websockets.rs index 2336549d5e..38e42ef86b 100644 --- a/common/client-libs/gateway-client/src/client/websockets.rs +++ b/common/client-libs/gateway-client/src/client/websockets.rs @@ -1,51 +1,37 @@ use crate::error::GatewayClientError; -use nym_http_api_client::HickoryDnsResolver; +#[cfg(not(target_arch = "wasm32"))] +use nym_topology::EntryDetails; #[cfg(unix)] use std::{ os::fd::{AsRawFd, RawFd}, sync::Arc, }; +use tokio::net::TcpSocket; use tokio::net::TcpStream; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tungstenite::handshake::client::Response; -use url::{Host, Url}; +use url::Url; use std::net::SocketAddr; #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn connect_async( - endpoint: &str, + endpoint: &EntryDetails, #[cfg(unix)] connection_fd_callback: Option>, ) -> Result<(WebSocketStream>, Response), GatewayClientError> { - use tokio::net::TcpSocket; - - let resolver = HickoryDnsResolver::default(); - let uri = - Url::parse(endpoint).map_err(|_| GatewayClientError::InvalidUrl(endpoint.to_owned()))?; + let uri = ws_entry_address(endpoint, false) + .ok_or(GatewayClientError::InvalidEndpoint(endpoint.to_string()))?; let port: u16 = uri.port_or_known_default().unwrap_or(443); - let host = uri - .host() - .ok_or(GatewayClientError::InvalidUrl(endpoint.to_owned()))?; - - // Get address for tcp connection, if a domain is provided use our preferred resolver rather than - // the default std resolve - let sock_addrs: Vec = match host { - Host::Ipv4(addr) => vec![SocketAddr::new(addr.into(), port)], - Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)], - Host::Domain(domain) => { - // Do a DNS lookup for the domain using our custom DNS resolver - resolver - .resolve_str(domain) - .await? - .map(|a| SocketAddr::new(a, port)) - .collect() - } - }; + let sock_addrs = endpoint + .ip_addresses + .iter() + .map(|addr| SocketAddr::new(*addr, port)); + let uri_str = uri.to_string(); let mut stream = Err(GatewayClientError::NoEndpointForConnection { - address: endpoint.to_owned(), + address: uri_str.clone(), }); for sock_addr in sock_addrs { let socket = if sock_addr.is_ipv4() { @@ -54,7 +40,7 @@ pub(crate) async fn connect_async( TcpSocket::new_v6() } .map_err(|err| GatewayClientError::NetworkConnectionFailed { - address: endpoint.to_owned(), + address: uri_str.clone(), source: Box::new(tungstenite::Error::from(err)), })?; @@ -70,7 +56,7 @@ pub(crate) async fn connect_async( } Err(err) => { stream = Err(GatewayClientError::NetworkConnectionFailed { - address: endpoint.to_owned(), + address: uri_str.clone(), source: Box::new(tungstenite::Error::from(err)), }); continue; @@ -78,10 +64,39 @@ pub(crate) async fn connect_async( } } - tokio_tungstenite::client_async_tls(endpoint, stream?) + tokio_tungstenite::client_async_tls(uri.clone(), stream?) .await .map_err(|error| GatewayClientError::NetworkConnectionFailed { - address: endpoint.to_owned(), + address: uri_str.clone(), source: Box::new(error), }) } + +pub fn ws_entry_address_tls(entry: &EntryDetails) -> Option { + let hostname = entry.hostname.as_ref()?; + let wss_port = entry.clients_wss_port?; + + Url::parse(&format!("wss://{hostname}:{wss_port}")).ok() +} + +pub fn ws_entry_address_no_tls(entry: &EntryDetails, prefer_ipv6: bool) -> Option { + if let Some(hostname) = entry.hostname.as_ref() { + return Url::parse(&format!("ws://{hostname}:{}", entry.clients_ws_port)).ok(); + } + + if prefer_ipv6 { + if let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) { + return Url::parse(&format!("ws://{ipv6}:{}", entry.clients_ws_port)).ok(); + } + } + + let any_ip = entry.ip_addresses.first()?; + Url::parse(&format!("ws://{any_ip}:{}", entry.clients_ws_port)).ok() +} + +pub fn ws_entry_address(entry: &EntryDetails, prefer_ipv6: bool) -> Option { + if let Some(tls) = ws_entry_address_tls(entry) { + return Some(tls); + } + ws_entry_address_no_tls(entry, prefer_ipv6) +} diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index eaea37c586..4d7e70bdf0 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -49,8 +49,8 @@ pub enum GatewayClientError { #[error("no socket address for endpoint: {address}")] NoEndpointForConnection { address: String }, - #[error("Invalid URL: {0}")] - InvalidUrl(String), + #[error("Invalid Endpoint: {0}")] + InvalidEndpoint(String), #[cfg(not(target_arch = "wasm32"))] #[error("resolution failed: {0}")] diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index c542d93e04..d79f84349e 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -28,6 +28,12 @@ pub struct EntryDetails { pub clients_wss_port: Option, } +impl std::fmt::Display for EntryDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct SupportedRoles { pub mixnode: bool, @@ -59,36 +65,6 @@ pub struct RoutingNode { } impl RoutingNode { - pub fn ws_entry_address_tls(&self) -> Option { - let entry = self.entry.as_ref()?; - let hostname = entry.hostname.as_ref()?; - let wss_port = entry.clients_wss_port?; - - Some(format!("wss://{hostname}:{wss_port}")) - } - - pub fn ws_entry_address_no_tls(&self, prefer_ipv6: bool) -> Option { - let entry = self.entry.as_ref()?; - - if let Some(hostname) = entry.hostname.as_ref() { - return Some(format!("ws://{hostname}:{}", entry.clients_ws_port)); - } - - if prefer_ipv6 && let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) { - return Some(format!("ws://{ipv6}:{}", entry.clients_ws_port)); - } - - let any_ip = entry.ip_addresses.first()?; - Some(format!("ws://{any_ip}:{}", entry.clients_ws_port)) - } - - pub fn ws_entry_address(&self, prefer_ipv6: bool) -> Option { - if let Some(tls) = self.ws_entry_address_tls() { - return Some(tls); - } - self.ws_entry_address_no_tls(prefer_ipv6) - } - pub fn identity(&self) -> ed25519::PublicKey { self.identity_key }