From 62967f736db45b3adf7b83ce0be2e06853a2fd3c Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Fri, 17 Oct 2025 15:11:18 +0200 Subject: [PATCH] Refactor domain fronting to use configuration-based approach How it works: 1. VPN configures NymNetworkDetails with front_hosts 2. SDK passes network_details to BaseClientBuilder via with_network_details() 3. BaseClientBuilder uses ClientBuilder::from_network() when available 4. from_network() automatically enables domain fronting if front_hosts configured Benefits: - Configuration-based (network_details), not object-based (cleaner) - No breaking changes for existing clients Changes: - Added network_details: Option to BaseClientBuilder - Updated tests --- Cargo.lock | 1 - common/client-core/Cargo.toml | 2 +- .../client-core/src/client/base_client/mod.rs | 41 +++++++--- common/http-api-client/src/lib.rs | 15 ++-- nym-registration-client/Cargo.toml | 1 - nym-registration-client/src/builder/config.rs | 37 +-------- sdk/rust/nym-sdk/src/mixnet/client.rs | 79 ++----------------- 7 files changed, 47 insertions(+), 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 917fe3c6a0..04f9da4092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6790,7 +6790,6 @@ dependencies = [ "nym-bandwidth-controller", "nym-credential-storage", "nym-credentials-interface", - "nym-http-api-client", "nym-ip-packet-client", "nym-registration-common", "nym-sdk", diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index fd1e82d7ac..2f6d87dbc4 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -36,7 +36,7 @@ nym-bandwidth-controller = { path = "../bandwidth-controller" } nym-crypto = { path = "../crypto" } nym-gateway-client = { path = "../client-libs/gateway-client" } nym-gateway-requests = { path = "../gateway-requests" } -nym-http-api-client = { path = "../http-api-client" } +nym-http-api-client = { path = "../http-api-client", features = ["network-defaults"] } nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nym-sphinx = { path = "../nymsphinx" } nym-statistics-common = { path = "../statistics" } diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 584288b78a..54b462ebca 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -46,6 +46,7 @@ use nym_gateway_client::client::config::GatewayClientConfig; use nym_gateway_client::{ AcknowledgementReceiver, GatewayClient, GatewayConfig, MixnetMessageReceiver, PacketRouter, }; +use nym_network_defaults::NymNetworkDetails; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::nodes::NodeIdentity; @@ -212,13 +213,15 @@ pub struct BaseClientBuilder { client_store: S, dkg_query_client: Option, + // Optional network details for domain fronting support + network_details: Option, + wait_for_gateway: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, shutdown: Option, event_tx: Option, user_agent: Option, - custom_nym_api_client: Option, setup_method: GatewaySetup, @@ -242,13 +245,13 @@ where config: base_config, client_store, dkg_query_client, + network_details: None, wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, shutdown: None, event_tx: None, user_agent: None, - custom_nym_api_client: None, setup_method: GatewaySetup::MustLoad { gateway_id: None }, #[cfg(unix)] connection_fd_callback: None, @@ -256,9 +259,13 @@ where } } + /// Provide network details for domain fronting support + /// + /// When provided, the SDK will use `from_network()` to build the nym-api client, + /// which automatically handles domain fronting if configured in the network details. #[must_use] - pub fn with_nym_api_client(mut self, client: nym_http_api_client::Client) -> Self { - self.custom_nym_api_client = Some(client); + pub fn with_network_details(mut self, network_details: NymNetworkDetails) -> Self { + self.network_details = Some(network_details); self } @@ -871,19 +878,29 @@ where } fn construct_nym_api_client( + network_details: Option<&NymNetworkDetails>, config: &Config, user_agent: Option, - custom_client: Option, ) -> Result { - // If a custom client was provided (e.g., with domain fronting support), use it - if let Some(client) = custom_client { - tracing::debug!("Using custom nym-api HTTP client"); - return Ok(client); + // If network details are provided, use from_network() which handles domain fronting + if let Some(network_details) = network_details { + tracing::debug!( + "Building nym-api client from network details (with domain fronting support)" + ); + + let mut builder = nym_http_api_client::ClientBuilder::from_network(network_details) + .map_err(ClientCoreError::from)?; + + if let Some(user_agent) = user_agent { + builder = builder.with_user_agent(user_agent); + } + + return builder.build().map_err(ClientCoreError::from); } - tracing::debug!("Creating default nym-api HTTP client from config"); + // Fallback to basic client for backwards compatibility + tracing::debug!("Building basic nym-api HTTP client from config endpoints"); - // Otherwise, create a basic client let mut nym_api_urls = config.get_nym_api_endpoints(); nym_api_urls.shuffle(&mut thread_rng()); @@ -980,9 +997,9 @@ where .map(|client| BandwidthController::new(credential_store, client)); let nym_api_client = Self::construct_nym_api_client( + self.network_details.as_ref(), &self.config, self.user_agent.clone(), - self.custom_nym_api_client, )?; let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?; diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 3ac1ef6ea9..0e61a1cd56 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -594,10 +594,9 @@ impl ClientBuilder { let urls = network .nym_api_urls .as_ref() - .ok_or_else(|| { - HttpClientError::GenericRequestFailure( - "No API URLs configured in network details".to_string(), - ) + .ok_or_else(|| HttpClientError::InternalResponseInconsistency { + url: ::url::Url::parse("about:blank").unwrap(), + details: "No API URLs configured in network details".to_string(), })? .iter() .map(|api_url| { @@ -611,8 +610,12 @@ impl ClientBuilder { .iter() .map(|host| format!("https://{}", host)) .collect(); - url = Url::new(api_url.url.clone(), Some(fronts)) - .map_err(|e| HttpClientError::GenericRequestFailure(e.to_string()))?; + url = Url::new(api_url.url.clone(), Some(fronts)).map_err(|e| { + HttpClientError::MalformedUrl { + raw: api_url.url.clone(), + source: e, + } + })?; } Ok(url) diff --git a/nym-registration-client/Cargo.toml b/nym-registration-client/Cargo.toml index dbff96026c..e468af0998 100644 --- a/nym-registration-client/Cargo.toml +++ b/nym-registration-client/Cargo.toml @@ -23,7 +23,6 @@ nym-authenticator-client = { path = "../nym-authenticator-client" } nym-bandwidth-controller = { path = "../common/bandwidth-controller" } nym-credential-storage = { path = "../common/credential-storage" } nym-credentials-interface = { path = "../common/credentials-interface" } -nym-http-api-client = { path = "../common/http-api-client" } nym-ip-packet-client = { path = "../nym-ip-packet-client" } nym-registration-common = { path = "../common/registration" } nym-sdk = { path = "../sdk/rust/nym-sdk" } diff --git a/nym-registration-client/src/builder/config.rs b/nym-registration-client/src/builder/config.rs index b7070d1c5b..47f5f7c8e8 100644 --- a/nym-registration-client/src/builder/config.rs +++ b/nym-registration-client/src/builder/config.rs @@ -34,7 +34,6 @@ pub struct BuilderConfig { pub two_hops: bool, pub user_agent: UserAgent, pub custom_topology_provider: Box, - pub custom_nym_api_client: Option, pub network_env: NymNetworkDetails, pub cancel_token: CancellationToken, #[cfg(unix)] @@ -57,9 +56,6 @@ pub struct MixnetClientConfig { } impl BuilderConfig { - /// Create a new BuilderConfig without domain fronting support - /// - /// For domain fronting support, set `custom_nym_api_client` to Some(client) after creation #[allow(clippy::too_many_arguments)] pub fn new( entry_node: NymNodeWithKeys, @@ -81,7 +77,6 @@ impl BuilderConfig { two_hops, user_agent, custom_topology_provider, - custom_nym_api_client: None, network_env, cancel_token, #[cfg(unix)] @@ -89,12 +84,6 @@ impl BuilderConfig { } } - /// Set a custom nym-api HTTP client (for domain fronting support) - pub fn with_nym_api_client(mut self, client: nym_http_api_client::Client) -> Self { - self.custom_nym_api_client = Some(client); - self - } - pub fn mixnet_client_debug_config(&self) -> DebugConfig { if self.two_hops { two_hop_debug_config(&self.mixnet_client_config) @@ -147,7 +136,7 @@ impl BuilderConfig { RememberMe::new_mixnet() }; - let mut builder = builder + let builder = builder .with_user_agent(self.user_agent) .request_gateway(self.entry_node.node.identity.to_string()) .network_details(self.network_env) @@ -156,11 +145,6 @@ impl BuilderConfig { .with_remember_me(remember_me) .custom_topology_provider(self.custom_topology_provider); - if let Some(nym_api_client) = self.custom_nym_api_client { - tracing::debug!("Using custom nym-api HTTP client"); - builder = builder.with_nym_api_client(nym_api_client); - } - #[cfg(unix)] let builder = builder.with_connection_fd_callback(self.connection_fd_callback); @@ -263,23 +247,4 @@ mod tests { assert_eq!(config.min_mixnode_performance, None); assert_eq!(config.min_gateway_performance, None); } - - #[test] - fn test_builder_config_has_custom_client_field() { - // Verify that BuilderConfig has the custom_nym_api_client field - // by creating a simple HTTP client - let http_client = nym_http_api_client::Client::builder( - nym_http_api_client::Url::parse("https://validator.nymtech.net/api").unwrap(), - ) - .expect("Failed to create client builder") - .build() - .expect("Failed to build client"); - - // Verify the client works - let urls = http_client.base_urls(); - assert!( - !urls.is_empty() && urls[0].as_str().contains("validator.nymtech.net"), - "HTTP client should be configured with correct URL" - ); - } } diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index e333f4e656..04ca7a7b25 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -54,7 +54,6 @@ pub struct MixnetClientBuilder { custom_topology_provider: Option>, custom_gateway_transceiver: Option>, custom_shutdown: Option, - custom_nym_api_client: Option, event_tx: Option, force_tls: bool, user_agent: Option, @@ -94,7 +93,6 @@ impl MixnetClientBuilder { socks5_config: None, wait_for_gateway: false, custom_topology_provider: None, - custom_nym_api_client: None, storage: storage_paths .initialise_default_persistent_storage() .await?, @@ -133,7 +131,6 @@ where wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, - custom_nym_api_client: None, custom_shutdown: None, event_tx: None, force_tls: false, @@ -158,7 +155,6 @@ where wait_for_gateway: self.wait_for_gateway, custom_topology_provider: self.custom_topology_provider, custom_gateway_transceiver: self.custom_gateway_transceiver, - custom_nym_api_client: self.custom_nym_api_client, custom_shutdown: self.custom_shutdown, event_tx: self.event_tx, force_tls: self.force_tls, @@ -298,12 +294,6 @@ where self } - #[must_use] - pub fn with_nym_api_client(mut self, client: nym_http_api_client::Client) -> Self { - self.custom_nym_api_client = Some(client); - self - } - #[must_use] pub fn with_statistics_reporting(mut self, config: StatsReporting) -> Self { self.config.debug_config.stats_reporting = config; @@ -348,7 +338,6 @@ where client.custom_gateway_transceiver = self.custom_gateway_transceiver; client.custom_topology_provider = self.custom_topology_provider; - client.custom_nym_api_client = self.custom_nym_api_client; client.custom_shutdown = self.custom_shutdown; client.wait_for_gateway = self.wait_for_gateway; client.force_tls = self.force_tls; @@ -398,9 +387,6 @@ where /// advanced usage of custom gateways custom_gateway_transceiver: Option>, - /// Custom nym-api HTTP client (for domain fronting support) - custom_nym_api_client: Option, - /// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded. wait_for_gateway: bool, @@ -473,7 +459,6 @@ where storage, custom_topology_provider: None, custom_gateway_transceiver: None, - custom_nym_api_client: None, wait_for_gateway: false, force_tls: false, custom_shutdown: None, @@ -722,6 +707,7 @@ where let mut base_builder: BaseClientBuilder<_, _> = BaseClientBuilder::new(base_config, self.storage, self.dkg_query_client) + .with_network_details(self.config.network_details.clone()) .with_wait_for_gateway(self.wait_for_gateway) .with_forget_me(&self.forget_me) .with_remember_me(&self.remember_me) @@ -731,11 +717,6 @@ where base_builder = base_builder.with_user_agent(user_agent); } - if let Some(nym_api_client) = self.custom_nym_api_client { - tracing::debug!("Using custom nym-api HTTP client"); - base_builder = base_builder.with_nym_api_client(nym_api_client); - } - if let Some(topology_provider) = self.custom_topology_provider { base_builder = base_builder.with_topology_provider(topology_provider); } @@ -906,70 +887,24 @@ mod tests { use super::*; #[test] - fn test_mixnet_builder_default_no_custom_client() { + fn test_mixnet_builder_default() { let builder = MixnetClientBuilder::new_ephemeral(); assert!( builder.build().is_ok(), - "Builder should succeed without custom client" + "Builder should succeed with default configuration" ); } #[test] - fn test_mixnet_builder_with_custom_client() { - let http_client = nym_http_api_client::Client::builder( - nym_http_api_client::Url::parse("https://validator.nymtech.net/api").unwrap(), - ) - .expect("Failed to create client builder") - .build() - .expect("Failed to build client"); + fn test_mixnet_builder_with_network_details() { + use nym_network_defaults::NymNetworkDetails; - let builder = MixnetClientBuilder::new_ephemeral().with_nym_api_client(http_client); - - assert!( - builder.build().is_ok(), - "Builder should succeed with custom client" - ); - } - - // Note: Tests for entry_capable_nodes() vs entry_gateways() are in nym-topology crate - // These tests verify the builder functionality only - - #[test] - fn test_custom_client_transfer_through_build() { - let http_client = nym_http_api_client::Client::builder( - nym_http_api_client::Url::parse("https://validator.nymtech.net/api").unwrap(), - ) - .expect("Failed to create client builder") - .build() - .expect("Failed to build client"); - - let builder = MixnetClientBuilder::new_ephemeral().with_nym_api_client(http_client); + let builder = MixnetClientBuilder::new_ephemeral(); let disconnected_client = builder.build(); assert!( disconnected_client.is_ok(), - "Build should transfer custom_nym_api_client successfully" - ); - } - - #[test] - fn test_builder_storage_transfer_includes_custom_client() { - use nym_client_core::client::base_client::storage::Ephemeral; - - let http_client = nym_http_api_client::Client::builder( - nym_http_api_client::Url::parse("https://validator.nymtech.net/api").unwrap(), - ) - .expect("Failed to create client builder") - .build() - .expect("Failed to build client"); - - let builder = MixnetClientBuilder::new_ephemeral().with_nym_api_client(http_client); - let new_storage = Ephemeral::default(); - let builder_with_new_storage = builder.set_storage(new_storage); - - assert!( - builder_with_new_storage.build().is_ok(), - "set_storage should preserve custom_nym_api_client" + "Builder should succeed and domain fronting is handled via network_details" ); } }