Compare commits

..

57 Commits

Author SHA1 Message Date
Tommy Verrall 1ecc2a8dda Merge pull request #6158 from nymtech/simon/registration_fail
[bugfix] Disconnect mixnet client if registration fails
2025-10-31 10:05:41 +01:00
Simon Wicky f9659ef42c disconnect mixnet client if registration fails 2025-10-30 16:06:53 +01:00
import this c74494a21d [Feature/operators]: QUIC bridge deployment script v2 (#6145)
* new quick deployment script

* docs tweak

* update script to use .deb postinst

* final clean - ready to go

* correct nym-node config dir search with a fallback
2025-10-30 12:22:55 +00:00
Simon Wicky 54f6c98c22 remove unused deps (#6151) 2025-10-29 11:48:49 +01:00
Simon Wicky 846484bbb4 use typed builder (#6150) 2025-10-27 17:49:50 +01:00
Tommy Verrall fb3f5501ba Merge pull request #6143 from nymtech/bugfix/mix-tx-closed-v2
Bugfix: Add circuit breaker
2025-10-27 16:45:41 +01:00
Tommy Verrall e8a607f520 Merge pull request #6149 from nymtech/simon/tommy_too_quick
tommy is too quick
2025-10-27 13:52:55 +01:00
Simon Wicky f5f6df9eaf tommy is too quick 2025-10-27 13:50:49 +01:00
Tommy Verrall c647ab5605 Merge pull request #6148 from nymtech/simon/registration_client_timeout
configurable mixnet client startup timeout
2025-10-27 13:47:48 +01:00
Simon Wicky 416c21a42e configurable mixnet client startup timeout 2025-10-27 13:35:46 +01:00
Simon Wicky fd5a95fa4d allow overwriting existing sdk shutdown manager 2025-10-24 16:17:29 +02:00
Simon Wicky c61df79182 typo 2025-10-24 14:11:56 +02:00
Simon Wicky 08559a7660 calling for shutdown from the MixTrafficController 2025-10-24 14:07:15 +02:00
Jędrzej Stuczyński 6dce55a99b using same hierarchy of trackers for client shutdown control 2025-10-24 14:03:18 +02:00
Tommy Verrall bc0b89b31c Internal comments 2025-10-24 12:44:10 +02:00
Tommy Verrall 67c32faa11 Fix comments 2025-10-23 19:22:26 +02:00
Tommy Verrall aa0d15ee67 Better message to come in the PR description 2025-10-23 19:06:27 +02:00
p17o 4f0974fcf1 Update quic_bridge_deployment.sh for IPv4 and .deb package (#6138)
Updated ping commands to explicitly use IPv4 and adjusted file permission checks with sudo. Changed the forward address prompt to specify IPv4 and modified the binary download process to fetch and install the latest .deb release URL automatically.
2025-10-22 15:46:23 +00:00
Jędrzej Stuczyński bd2174641e bugfix: update internal owner address in transferred share (#6139) 2025-10-22 10:42:26 +01:00
Tommy Verrall 59b62fabc9 Merge pull request #6134 from nymtech/feature/domain-fronting-v2
Domain fronting
2025-10-22 11:08:21 +02:00
Tommy Verrall e6f4bae895 Last failing test - fix 2025-10-21 19:34:20 +02:00
Tommy Verrall d71742af32 Use explicit Vec<ApiUrl> handling in BaseClientBuilder
- Replace NymNetworkDetails with explicit API URL handling
- Fix deprecated from_network() usage and improve fallback logic
- Add URL validation and remove unused backwards compatibility
2025-10-21 19:15:24 +02:00
Tommy Verrall 3b7c07e249 Actually commit the recommended changes 2025-10-21 18:12:38 +02:00
Tommy Verrall 3b429dde69 Fix broken tests in CI 2025-10-21 16:29:26 +02:00
Tommy Verrall 3a29c296da Replace deprecated from_network() with new_with_fronted_urls() 2025-10-21 16:05:41 +02:00
Tommy Verrall 8544c54f8f Merge develop into feature/domain-fronting-v2
- Use new_with_fronted_urls() for explicit domain fronting
- Deprecate from_network() in favor of explicit method
2025-10-21 15:58:20 +02:00
Jędrzej Stuczyński 9f9639950b feat: expose more explicit new_with_fronted_urls builder for http API client (#6136) 2025-10-21 14:47:58 +01:00
Jędrzej Stuczyński 111a0b20b6 bugfix: update stored epoch share when changing ownership (#6135) 2025-10-21 14:10:20 +01:00
Tommy Verrall 67b300d0b8 Fix new_from_env() to populate nym_api_urls for domain fronting 2025-10-21 12:22:51 +02:00
Jędrzej Stuczyński 88c4e0ce6c bugfix: update stored epoch share when changing announce address (#6131)
* bugfix: update stored epoch share when changing announce address

* chore: remove placeholder legacy mixnode bonding test [mixnet contract]
2025-10-21 10:59:17 +01:00
Tommy Verrall f6800aff0a fix all clippy messages 2025-10-21 11:32:47 +02:00
Tommy Verrall 0c7c927ca2 Add more tests for retry logic 2025-10-21 11:32:47 +02:00
Tommy Verrall a69c8b1660 Fix confusing tracing logs 2025-10-21 11:32:47 +02:00
Tommy Verrall f3ea310a46 Fix retries - this is working 2025-10-21 11:32:46 +02:00
Tommy Verrall 92f9ff035d Add configuration-based domain fronting support
Changes:
- Add network_details field to BaseClientBuilder (optional, backwards compatible)
- Add with_network_details() method for opt-in domain fronting
- Update construct_nym_api_client() to use from_network() when network_details provided
- Enable network-defaults feature in nym-client-core Cargo.toml
- SDK passes network_details to BaseClientBuilder
2025-10-21 11:32:46 +02:00
Tommy Verrall 5a817e1df1 Merge pull request #6126 from nymtech/multiple-fall-back-urls
Changes:

Multiple URL fallback with configurable retries (defaults to 3)
Infallible URL conversion per Andrews feedback (Url::from() instead of parse().ok())
Non-breaking builder pattern for BuilderConfig per Andrej's "too many arguments" feedback
Reverted redundant node filtering per Andrew's clarification that API already filters by supported_roles.entry
2025-10-21 11:27:37 +02:00
Tommy Verrall a07a24db00 Fix CI issues 2025-10-21 11:01:04 +02:00
Tommy Verrall a0cb812eff Allow clippy::enum_variant_names for BuilderConfigError 2025-10-21 10:35:57 +02:00
Tommy Verrall 923c1fa184 Improve error handling
Changes:
- Replace String error with BuilderConfigError enum in BuilderConfigBuilder
- Update tests to use pattern matching instead of string assertions
2025-10-20 16:57:31 +02:00
Tommy Verrall 35ea7e4926 - Add DEFAULT_NYM_API_RETRIES constant (replaces magic number 3)
- Run cargo fmt on all affected packages
- All clippy warnings resolved
2025-10-20 16:51:07 +02:00
Tommy Verrall d1cb9afaf0 not sure what happened but it's fixed 2025-10-20 15:20:24 +02:00
Tommy Verrall 79d4b4b2e3 Merge branch 'develop' into multiple-fall-back-urls 2025-10-20 15:16:36 +02:00
Tommy Verrall 8460b33946 Merge branch 'multiple-fall-back-urls' of https://github.com/nymtech/nym into multiple-fall-back-urls 2025-10-20 15:16:17 +02:00
Tommy Verrall ae6539e07c Merge resolution 2025-10-20 15:14:48 +02:00
Tommy Verrall 18cebdfedc Add accessor methods for Url internals
Add inner_url() and fronts() accessor methods to nym_http_api_client::Url
for VPN client integration
2025-10-20 14:33:57 +02:00
Tommy Verrall c448ec823a Remove tests for removed with_nym_api_client method
These tests were referencing with_nym_api_client() which was removed when
cleaning domain fronting code from this branch
2025-10-20 11:52:04 +02:00
Tommy Verrall a266137278 Add optional builder pattern for BuilderConfig (non-breaking)
Addresses @jstuczyn's feedback about too many arguments by adding
BuilderConfigBuilder as an alternative to the existing new() method.
2025-10-20 11:39:50 +02:00
Tommy Verrall 6f4dfd1dab fix conversion type && make the retry count configurable 2025-10-20 11:15:31 +02:00
Andy Duplain 57719787db Merge pull request #6130 from nymtech/andy/url_fronts
VPN-4262: Update `Url` to return `url` and `front` fields.
2025-10-17 15:44:08 +01:00
Andy Duplain 29a57bf172 VPN-4262: Update Url to return url and front fields.
The VPN client is using the `Url` type alot now and in order to avoid
double URL-parsing we would like the content of the `Url` type exposed.
2025-10-17 15:37:07 +01:00
Tommy Verrall db813b6e3e Revert node filtering changes per Andrew's feedback
Andrew clarified that get_basic_entry_assigned_nodes_v2() already filters by
supported_roles.entry
2025-10-17 15:18:28 +02:00
Tommy Verrall 1be5ba310a Remove domain fronting code to keep gateway changes only
This branch now contains only gateway registration improvements:
- Multiple URL fallback support in gateways_for_init()
- Get all entry-capable nodes for registration
- Performance and code quality improvements
2025-10-17 14:27:31 +02:00
Tommy Verrall 41ff3f7824 Address PR feedback: simplify code and reduce log noise
- Reverted all changes to topology_control/nym_api_provider.rs
- Changed info/warn logs to debug for custom client messages
- Removed unused _rng parameter from gateways_for_init()
- Simplified URL builder to always use new_with_urls()
2025-10-17 14:20:12 +02:00
Tommy Verrall c9d4d62446 Fix clippy warnings: use arrays instead of vec! in tests 2025-10-17 13:30:30 +02:00
Tommy Verrall e839a0d80e Merge develop into multiple-fall-back-urls
Resolved conflicts:
- Added event_tx field to MixnetClientBuilder alongside custom_nym_api_client
- Both features are independent and coexist:
  * custom_nym_api_client: for domain fronting support
  * event_tx: for event handling
- Updated all constructors and methods to properly handle both fields
2025-10-17 13:23:04 +02:00
Tommy Verrall cd61f930bf feat: pass custom HTTP client through SDK stack for domain fronting
- Add with_nym_api_client() to BaseClientBuilder, MixnetClientBuilder, and RegistrationClientBuilderConfig

- Modify nym_api_provider to fetch all nodes then filter by supported_roles.entry (fixes metadata inconsistency)

- Update helpers.rs to build HTTP client with all nym_apis URLs and retries for fallback support

- Fix SDK to use entry_capable_nodes() instead of entry_gateways() for broader gateway selection

This enables domain fronting and URL rotation throughout the entire SDK stack, improving censorship resistance and connection reliability. All changes are backward compatible - custom client is optional.
2025-10-17 08:36:23 +02:00
Tommy Verrall edcf2b1204 enable URL rotation and retries for mixnet gateway init 2025-10-16 16:22:57 +02:00
33 changed files with 1134 additions and 1117 deletions
Generated
+22 -1
View File
@@ -2262,7 +2262,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -6798,6 +6798,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"typed-builder",
"url",
]
@@ -11391,6 +11392,26 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typed-builder"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d0dd654273fc253fde1df4172c31fb6615cf8b041d3a4008a028ef8b1119e66"
dependencies = [
"typed-builder-macro",
]
[[package]]
name = "typed-builder-macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c26257f448222014296978b2c8456e2cad4de308c35bdb1e383acd569ef5b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "typenum"
version = "1.18.0"
+1 -12
View File
@@ -215,7 +215,6 @@ base64 = "0.22.1"
base85rs = "0.1.3"
bincode = "1.3.3"
bip39 = { version = "2.0.0", features = ["zeroize"] }
bit-vec = "0.7.0" # can we unify those?
bitvec = "1.0.0"
blake3 = "1.7.0"
bloomfilter = "3.0.1"
@@ -243,13 +242,11 @@ criterion = "0.5"
csv = "1.3.1"
ctr = "0.9.1"
cupid = "0.6.1"
curve25519-dalek = "4.1"
dashmap = "5.5.3"
# We want https://github.com/DefGuard/wireguard-rs/pull/64 , but there's no crates.io release being pushed out anymore
defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.4.7" }
digest = "0.10.7"
dirs = "6.0"
doc-comment = "0.3"
dotenvy = "0.15.6"
dyn-clone = "1.0.19"
ecdsa = "0.16"
@@ -265,11 +262,8 @@ futures = "0.3.31"
futures-util = "0.3"
generic-array = "0.14.7"
getrandom = "0.2.10"
getset = "0.1.5"
handlebars = "3.5.5"
headers = "0.4.0"
hex = "0.4.3"
hex-literal = "0.3.3"
hickory-resolver = "0.25"
hkdf = "0.12.3"
hmac = "0.12.1"
@@ -293,12 +287,10 @@ lazy_static = "1.5.0"
ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0"
log = "0.4"
maxminddb = "0.23.0"
mime = "0.3.17"
moka = { version = "0.12", features = ["future"] }
nix = "0.27.1"
notify = "5.1.0"
okapi = "0.7.0"
once_cell = "1.21.3"
opentelemetry = "0.19.0"
opentelemetry-jaeger = "0.18.0"
@@ -307,7 +299,6 @@ pem = "0.8"
petgraph = "0.6.5"
pin-project = "1.1"
pnet_packet = "0.35.0"
pin-project-lite = "0.2.16"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
@@ -315,13 +306,10 @@ rand = "0.8.5"
rand_chacha = "0.3"
rand_core = "0.6.3"
rand_distr = "0.4"
rand_pcg = "0.3.1"
rand_seeder = "0.2.3"
rayon = "1.5.1"
regex = "1.10.6"
reqwest = { version = "0.12.15", default-features = false }
rs_merkle = "1.5.0"
safer-ffi = "0.1.13"
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
@@ -368,6 +356,7 @@ tracing-indicatif = "0.3.9"
tracing-test = "0.2.5"
ts-rs = "10.1.0"
tungstenite = { version = "0.20.1", default-features = false }
typed-builder = "0.23.0"
uniffi = "0.29.2"
uniffi_build = "0.29.0"
url = "2.5"
+1 -1
View File
@@ -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" }
@@ -114,13 +114,12 @@ where
})?;
hardcoded_topology.entry_capable_nodes().cloned().collect()
} else {
let mut rng = rand::thread_rng();
crate::init::helpers::gateways_for_init(
&mut rng,
&core.client.nym_api_urls,
user_agent,
core.debug.topology.minimum_gateway_performance,
core.debug.topology.ignore_ingress_epoch_role,
None,
)
.await?
};
@@ -173,13 +173,12 @@ where
})?;
hardcoded_topology.entry_capable_nodes().cloned().collect()
} else {
let mut rng = rand::thread_rng();
crate::init::helpers::gateways_for_init(
&mut rng,
&core.client.nym_api_urls,
user_agent,
core.debug.topology.minimum_gateway_performance,
core.debug.topology.ignore_ingress_epoch_role,
None,
)
.await?
};
+134 -16
View File
@@ -73,6 +73,10 @@ use url::Url;
#[cfg(debug_assertions)]
use wasm_utils::console_log;
/// Default number of retries for Nym API requests when using network details with domain fronting.
/// This allows the client to try alternative URLs if the primary endpoint is unavailable.
const DEFAULT_NYM_API_RETRIES: usize = 3;
#[cfg(all(
not(target_arch = "wasm32"),
feature = "fs-surb-storage",
@@ -212,6 +216,9 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
client_store: S,
dkg_query_client: Option<C>,
// Optional API URLs for domain fronting support
nym_api_urls: Option<Vec<nym_network_defaults::ApiUrl>>,
wait_for_gateway: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
@@ -241,6 +248,7 @@ where
config: base_config,
client_store,
dkg_query_client,
nym_api_urls: None,
wait_for_gateway: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
@@ -263,6 +271,16 @@ where
self
}
/// Set Nym API URLs for domain fronting support.
///
/// When provided, the client will use these API URLs (which include front_hosts)
/// to construct HTTP clients with domain fronting enabled.
#[must_use]
pub fn with_nym_api_urls(mut self, nym_api_urls: Vec<nym_network_defaults::ApiUrl>) -> Self {
self.nym_api_urls = Some(nym_api_urls);
self
}
#[must_use]
pub fn with_forget_me(mut self, forget_me: &ForgetMe) -> Self {
self.config.debug.forget_me = *forget_me;
@@ -783,7 +801,7 @@ where
event_tx,
);
let mix_tx = mix_traffic_controller.mix_rx();
let mix_tx = mix_traffic_controller.mix_tx();
let client_tx = mix_traffic_controller.client_tx();
shutdown_tracker.try_spawn_named(
@@ -863,21 +881,67 @@ where
}
fn construct_nym_api_client(
nym_api_urls: Option<&Vec<nym_network_defaults::ApiUrl>>,
config: &Config,
user_agent: Option<UserAgent>,
) -> Result<nym_http_api_client::Client, ClientCoreError> {
tracing::debug!(
"construct_nym_api_client called with nym_api_urls: {}",
nym_api_urls.is_some()
);
// If API URLs are provided, use new_with_fronted_urls() which handles domain fronting
if let Some(nym_api_urls) = nym_api_urls {
if nym_api_urls.is_empty() {
tracing::warn!("Provided nym_api_urls is empty, falling back to config endpoints");
} else {
tracing::info!(
"Building nym-api client from provided URLs (with domain fronting support): {} URLs",
nym_api_urls.len()
);
let mut builder =
nym_http_api_client::ClientBuilder::new_with_fronted_urls(nym_api_urls.clone())
.map_err(ClientCoreError::from)?
.with_retries(DEFAULT_NYM_API_RETRIES);
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
}
return builder.build().map_err(ClientCoreError::from);
}
}
// Fallback to basic client for backwards compatibility
tracing::debug!("Building basic nym-api HTTP client from config endpoints");
let mut nym_api_urls = config.get_nym_api_endpoints();
if nym_api_urls.is_empty() {
tracing::warn!("No API endpoints configured in config, this may cause issues");
}
nym_api_urls.shuffle(&mut thread_rng());
let mut builder = nym_http_api_client::Client::builder(nym_api_urls[0].clone())
.map_err(ClientCoreError::from)?;
// Convert config URLs to ApiUrl format for consistency
let api_urls: Vec<nym_network_defaults::ApiUrl> = nym_api_urls
.into_iter()
.map(|url| nym_network_defaults::ApiUrl {
url: url.to_string(),
front_hosts: None,
})
.collect();
tracing::debug!("Using {} config API endpoints", api_urls.len());
let mut builder = nym_http_api_client::ClientBuilder::new_with_fronted_urls(api_urls)
.map_err(ClientCoreError::from)?
.with_retries(DEFAULT_NYM_API_RETRIES)
.with_bincode();
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
}
builder = builder.with_bincode();
builder.build().map_err(ClientCoreError::from)
}
@@ -940,8 +1004,8 @@ where
// Create a shutdown tracker for this client - either as a child of provided tracker
// or get one from the registry
let shutdown_tracker = match self.shutdown {
Some(parent_tracker) => parent_tracker.child_tracker(),
None => nym_task::get_sdk_shutdown_tracker()?,
Some(parent_tracker) => parent_tracker.clone(),
None => nym_task::create_sdk_shutdown_tracker()?,
};
Self::start_event_control(self.event_tx, event_receiver, &shutdown_tracker);
@@ -961,7 +1025,11 @@ where
.dkg_query_client
.map(|client| BandwidthController::new(credential_store, client));
let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone())?;
let nym_api_client = Self::construct_nym_api_client(
self.nym_api_urls.as_ref(),
&self.config,
self.user_agent.clone(),
)?;
let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?;
let topology_provider = Self::setup_topology_provider(
@@ -976,7 +1044,7 @@ where
self.user_agent.clone(),
generate_client_stats_id(*self_address.identity()),
input_sender.clone(),
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
);
// needs to be started as the first thing to block if required waiting for the gateway
@@ -986,7 +1054,7 @@ where
shared_topology_accessor.clone(),
self_address.gateway(),
self.wait_for_gateway,
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
)
.await?;
@@ -1006,7 +1074,7 @@ where
stats_reporter.clone(),
#[cfg(unix)]
self.connection_fd_callback,
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
)
.await?;
let gateway_ws_fd = gateway_transceiver.ws_fd();
@@ -1014,7 +1082,7 @@ where
let reply_storage = Self::setup_persistent_reply_storage(
reply_storage_backend,
key_rotation_config,
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
)
.await?;
@@ -1025,7 +1093,7 @@ where
reply_storage.key_storage(),
reply_controller_sender.clone(),
stats_reporter.clone(),
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
);
// The message_sender is the transmitter for any component generating sphinx packets
@@ -1035,7 +1103,7 @@ where
let (message_sender, client_request_sender) = Self::start_mix_traffic_controller(
gateway_transceiver,
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
EventSender(event_sender),
);
@@ -1066,7 +1134,7 @@ where
shared_lane_queue_lengths.clone(),
client_connection_rx,
stats_reporter.clone(),
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
);
if !self
@@ -1082,7 +1150,7 @@ where
shared_topology_accessor.clone(),
message_sender,
stats_reporter.clone(),
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
);
}
@@ -1136,3 +1204,53 @@ pub struct BaseClient {
pub forget_me: ForgetMe,
pub remember_me: RememberMe,
}
#[cfg(test)]
mod tests {
use super::*;
use nym_network_defaults::{ApiUrl, NymNetworkDetails};
#[test]
fn test_network_details_with_multiple_urls() {
// Verify that network details can be configured with multiple API URLs
let mut network_details = NymNetworkDetails::new_empty();
network_details.nym_api_urls = Some(vec![
ApiUrl {
url: "https://validator.nymtech.net/api/".to_string(),
front_hosts: None,
},
ApiUrl {
url: "https://nym-frontdoor.vercel.app/api/".to_string(),
front_hosts: Some(vec!["vercel.app".to_string(), "vercel.com".to_string()]),
},
]);
assert_eq!(network_details.nym_api_urls.as_ref().unwrap().len(), 2);
assert!(network_details.nym_api_urls.as_ref().unwrap()[1]
.front_hosts
.is_some());
}
#[test]
fn test_network_details_with_front_hosts() {
// Verify that ApiUrl can store domain fronting configuration
let api_url = ApiUrl {
url: "https://nym-frontdoor.vercel.app/api/".to_string(),
front_hosts: Some(vec!["vercel.app".to_string(), "vercel.com".to_string()]),
};
assert_eq!(api_url.url, "https://nym-frontdoor.vercel.app/api/");
assert_eq!(api_url.front_hosts.as_ref().unwrap().len(), 2);
assert!(api_url
.front_hosts
.as_ref()
.unwrap()
.contains(&"vercel.app".to_string()));
}
#[test]
fn test_default_nym_api_retries_constant() {
// Verify the retry constant is set correctly
assert_eq!(DEFAULT_NYM_API_RETRIES, 3);
}
}
@@ -205,7 +205,7 @@ impl LoopCoverTrafficStream<OsRng> {
TrySendError::Full(_) => {
// This isn't a problem, if the channel is full means we're already sending the
// max amount of messages downstream can handle.
tracing::debug!("Failed to send cover message - channel full");
tracing::trace!("Failed to send cover message - channel full");
}
TrySendError::Closed(_) => {
tracing::warn!("Failed to send cover message - channel closed");
@@ -20,7 +20,10 @@ pub mod transceiver;
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
const MAX_FAILURE_COUNT: usize = 100;
/// Reduced from 100 to 20 to fail fast (~1-2 seconds instead of ~6 seconds).
/// If we can't send 20 packets in a row, the gateway is unreachable.
const MAX_FAILURE_COUNT: usize = 20;
// that's also disgusting.
pub struct Empty;
@@ -84,7 +87,7 @@ impl MixTrafficController {
self.client_tx.clone()
}
pub fn mix_rx(&self) -> BatchMixMessageSender {
pub fn mix_tx(&self) -> BatchMixMessageSender {
self.mix_tx.clone()
}
@@ -156,6 +159,11 @@ impl MixTrafficController {
// Do we need to handle the embedded mixnet client case
// separately?
self.event_tx.send(MixnetClientEvent::Traffic(MixTrafficEvent::FailedSendingSphinx));
// IMO it shouldn't be signalled from there but it is how it is
// TODO : report the failure upwards and shutdown from upwards
// Gateway is dead, we have to shut down currently
error!("Signalling shutdown from the MixTrafficController");
self.shutdown_token.cancel();
break;
}
}
@@ -298,6 +298,8 @@ where
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
);
}
// Early return to avoid further processing when channel is closed
return;
}
Ok(_) => {
let event = if fragment_id.is_some() {
+77 -20
View File
@@ -45,6 +45,7 @@ type WsConn = JSWebsocket;
const CONCURRENT_GATEWAYS_MEASURED: usize = 20;
const MEASUREMENTS: usize = 3;
const DEFAULT_NYM_API_RETRIES: usize = 3;
#[cfg(not(target_arch = "wasm32"))]
const CONN_TIMEOUT: Duration = Duration::from_millis(1500);
@@ -132,25 +133,27 @@ impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> {
}
}
pub async fn gateways_for_init<R: Rng>(
rng: &mut R,
pub async fn gateways_for_init(
nym_apis: &[Url],
user_agent: Option<UserAgent>,
minimum_performance: u8,
ignore_epoch_roles: bool,
retry_count: Option<usize>,
) -> Result<Vec<RoutingNode>, ClientCoreError> {
let nym_api = nym_apis
.choose(rng)
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
// Build client with ALL URLs for fallback support
let nym_api_urls: Vec<nym_http_api_client::Url> = nym_apis
.iter()
.map(|url| nym_http_api_client::Url::from(url.clone()))
.collect();
// Use the unified HTTP client directly with optional user agent
let mut builder = nym_http_api_client::Client::builder(nym_api.clone())
.map_err(|e| {
ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(
e,
))
})?
.with_bincode(); // Use bincode for better performance
if nym_api_urls.is_empty() {
return Err(ClientCoreError::ListOfNymApisIsEmpty);
}
let retry_count = retry_count.unwrap_or(DEFAULT_NYM_API_RETRIES);
let mut builder = nym_http_api_client::ClientBuilder::new_with_urls(nym_api_urls.clone())?
.with_retries(retry_count)
.with_bincode();
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
@@ -160,7 +163,7 @@ pub async fn gateways_for_init<R: Rng>(
ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(e))
})?;
tracing::debug!("Fetching list of gateways from: {nym_api}");
tracing::debug!("Fetching list of gateways from: {:?}", nym_api_urls);
// Use our helper to handle pagination
let gateways = get_all_basic_entry_nodes_with_metadata(&client, true)
@@ -172,17 +175,15 @@ pub async fn gateways_for_init<R: Rng>(
// filter out gateways below minimum performance and ones that could operate as a mixnode
// (we don't want instability)
let valid_gateways = gateways
let valid_gateways: Vec<RoutingNode> = gateways
.iter()
.filter(|g| ignore_epoch_roles || !g.supported_roles.mixnode)
.filter(|g| g.performance.round_to_integer() >= minimum_performance)
.filter_map(|gateway| gateway.try_into().ok())
.collect::<Vec<_>>();
tracing::debug!("After checking validity: {}", valid_gateways.len());
tracing::trace!("Valid gateways: {valid_gateways:#?}");
.collect();
tracing::info!(
"and {} after validity and performance filtering",
"Found {} valid gateways after filtering",
valid_gateways.len()
);
@@ -345,13 +346,20 @@ pub(super) fn get_specified_gateway(
must_use_tls: bool,
) -> Result<RoutingNode, ClientCoreError> {
tracing::debug!("Requesting specified gateway: {gateway_identity}");
let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity)
.map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?;
let gateway = gateways
.iter()
.find(|gateway| gateway.identity_key == user_gateway)
.ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_identity.to_string()))?;
.ok_or_else(|| {
tracing::debug!(
"Gateway {gateway_identity} not found in {} available gateways",
gateways.len()
);
ClientCoreError::NoGatewayWithId(gateway_identity.to_string())
})?;
let Some(entry_details) = gateway.entry.as_ref() else {
return Err(ClientCoreError::UnsupportedEntry {
@@ -414,3 +422,52 @@ pub(super) async fn register_with_gateway(
authenticated_ephemeral_client: gateway_client,
})
}
#[cfg(test)]
mod tests {
use url::Url;
#[test]
fn test_single_url_builds_without_retries() {
let urls = [Url::parse("https://api.nym.com").unwrap()];
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
.iter()
.map(|url| nym_http_api_client::Url::from(url.clone()))
.collect();
assert_eq!(nym_api_urls.len(), 1, "Should have exactly one URL");
}
#[test]
fn test_multiple_urls_prepared_for_retries() {
let urls = vec![
Url::parse("https://api1.nym.com").unwrap(),
Url::parse("https://api2.nym.com").unwrap(),
Url::parse("https://api3.nym.com").unwrap(),
];
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
.iter()
.map(|url| nym_http_api_client::Url::from(url.clone()))
.collect();
assert_eq!(nym_api_urls.len(), 3, "Should have all three URLs");
assert!(
nym_api_urls.len() > 1,
"Multiple URLs trigger retry behavior"
);
}
#[test]
fn test_empty_url_list_is_detected() {
let urls: Vec<Url> = vec![];
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
.iter()
.map(|url| nym_http_api_client::Url::from(url.clone()))
.collect();
assert!(nym_api_urls.is_empty(), "Empty list should remain empty");
}
}
+36 -20
View File
@@ -296,6 +296,9 @@ impl std::error::Error for ReqwestErrorWrapper {}
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum HttpClientError {
#[error("did not provide any valid client URLs")]
NoUrlsProvided,
#[error("failed to construct inner reqwest client: {source}")]
ReqwestBuildError {
#[source]
@@ -582,24 +585,29 @@ impl ClientBuilder {
Self::new(alt)
} else {
let url = url.to_url()?;
Ok(Self::new_with_urls(vec![url]))
Self::new_with_urls(vec![url])
}
}
/// Create a client builder from network details with sensible defaults
#[cfg(feature = "network-defaults")]
// deprecating function since it's not clear from its signature whether the client
// would be constructed using `nym_api_urls` or `nym_vpn_api_urls`
#[deprecated(note = "use explicit Self::new_with_fronted_urls instead")]
pub fn from_network(
network: &nym_network_defaults::NymNetworkDetails,
) -> Result<Self, HttpClientError> {
let urls = network
.nym_api_urls
.as_ref()
.ok_or_else(|| {
HttpClientError::GenericRequestFailure(
"No API URLs configured in network details".to_string(),
)
})?
.iter()
let urls = network.nym_api_urls.as_ref().cloned().unwrap_or_default();
Self::new_with_fronted_urls(urls.clone())
}
/// Create a client builder using the provided set of domain-fronted URLs
#[cfg(feature = "network-defaults")]
pub fn new_with_fronted_urls(
urls: Vec<nym_network_defaults::ApiUrl>,
) -> Result<Self, HttpClientError> {
let urls = urls
.into_iter()
.map(|api_url| {
// Convert ApiUrl to our Url type with fronting support
let mut url = Url::parse(&api_url.url)?;
@@ -611,15 +619,19 @@ 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(|source| {
HttpClientError::MalformedUrl {
raw: api_url.url.clone(),
source,
}
})?;
}
Ok(url)
})
.collect::<Result<Vec<_>, HttpClientError>>()?;
let mut builder = Self::new_with_urls(urls);
let mut builder = Self::new_with_urls(urls)?;
// Enable domain fronting by default (on retry)
#[cfg(feature = "tunneling")]
@@ -631,7 +643,11 @@ impl ClientBuilder {
}
/// Constructs a new http `ClientBuilder` from a valid url.
pub fn new_with_urls(urls: Vec<Url>) -> Self {
pub fn new_with_urls(urls: Vec<Url>) -> Result<Self, HttpClientError> {
if urls.is_empty() {
return Err(HttpClientError::NoUrlsProvided);
}
let urls = Self::check_urls(urls);
#[cfg(target_arch = "wasm32")]
@@ -640,7 +656,7 @@ impl ClientBuilder {
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client_builder = default_builder();
ClientBuilder {
Ok(ClientBuilder {
urls,
timeout: None,
custom_user_agent: false,
@@ -651,7 +667,7 @@ impl ClientBuilder {
retry_limit: 0,
serialization: SerializationFormat::Json,
}
})
}
/// Add an additional URL to the set usable by this constructed `Client`
@@ -948,13 +964,13 @@ impl Client {
return (url.as_str(), url.front_str());
} else {
warn!(
"Domain fronting is enabled, but no host_url is defined! Domain fronting WILL NOT WORK"
tracing::debug!(
"Domain fronting is enabled, but no host_url is defined for current URL"
)
}
} else {
warn!(
"Domain fronting is enabled, but no front_url is defined! Domain fronting WILL NOT WORK"
tracing::debug!(
"Domain fronting is enabled, but current URL has no front_hosts configured"
)
}
}
+9 -1
View File
@@ -21,6 +21,10 @@ inventory::collect!(ConfigRecord);
/// Returns the default builder with all registered configurations applied.
pub fn default_builder() -> ReqwestClientBuilder {
let mut b = ReqwestClientBuilder::new();
#[cfg(feature = "debug-inventory")]
let mut test_client = ReqwestClientBuilder::new();
let mut records: Vec<&'static ConfigRecord> =
inventory::iter::<ConfigRecord>.into_iter().collect();
records.sort_by_key(|r| r.priority); // lower runs first
@@ -35,6 +39,10 @@ pub fn default_builder() -> ReqwestClientBuilder {
for r in records {
b = (r.apply)(b);
#[cfg(feature = "debug-inventory")]
{
test_client = (r.apply)(test_client);
}
}
#[cfg(feature = "debug-inventory")]
@@ -47,7 +55,7 @@ pub fn default_builder() -> ReqwestClientBuilder {
eprintln!("[HTTP-INVENTORY] Building test client to verify configuration...");
// Try to build a client to see if it works
match b.try_clone().unwrap().build() {
match test_client.build() {
Ok(client) => {
eprintln!("[HTTP-INVENTORY] ✓ Client built successfully");
eprintln!("[HTTP-INVENTORY] Client debug info: {:#?}", client);
+110 -49
View File
@@ -2,77 +2,77 @@ use super::*;
#[test]
fn sanitizing_urls() {
let base_url: Url = "http://foomp.com".parse().unwrap();
let base_url: Url = "http://api.test".parse().unwrap();
// works with a full string
assert_eq!(
"http://foomp.com/foo/bar",
"http://api.test/foo/bar",
sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str()
);
// (and leading slash doesn't matter)
assert_eq!(
"http://foomp.com/foo/bar",
"http://api.test/foo/bar",
sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str()
);
// works with 1 segment
assert_eq!(
"http://foomp.com/foo",
"http://api.test/foo",
sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str()
);
// works with 2 segments
assert_eq!(
"http://foomp.com/foo/bar",
"http://api.test/foo/bar",
sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str()
);
// works with leading slash
assert_eq!(
"http://foomp.com/foo",
"http://api.test/foo",
sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
"http://api.test/foo/bar",
sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
"http://api.test/foo/bar",
sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str()
);
// works with trailing slash
assert_eq!(
"http://foomp.com/foo",
"http://api.test/foo",
sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
"http://api.test/foo/bar",
sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
"http://api.test/foo/bar",
sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str()
);
// works with both leading and trailing slash
assert_eq!(
"http://foomp.com/foo",
"http://api.test/foo",
sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
"http://api.test/foo/bar",
sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str()
);
// adds params
assert_eq!(
"http://foomp.com/foo/bar?foomp=baz",
"http://api.test/foo/bar?foomp=baz",
sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar?arg1=val1&arg2=val2",
"http://api.test/foo/bar?arg1=val1&arg2=val2",
sanitize_url(
&base_url,
&["/foo/", "/bar/"],
@@ -91,83 +91,87 @@ fn sanitizing_urls() {
#[tokio::test]
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new_with_urls(vec![
"http://broken.nym.badurl".parse()?,
"http://example.com/".parse()?,
])
"http://broken.nym.test".parse()?, // This will fail
"https://httpbin.org/status/200".parse()?, // This will succeed
])?
.with_retries(3)
.build()?;
let req = client.create_get_request(&["/"], NO_PARAMS).unwrap();
let req = client.create_get_request(&[], NO_PARAMS).unwrap();
let resp = client.send(req).await?;
assert_eq!(resp.status(), 200);
// The main test is that we successfully retried and switched to the working URL
// We accept any response from the working endpoint since external services can be unreliable
assert_eq!(
client.current_url().as_str(),
"https://httpbin.org/status/200"
);
// check that the url was updated
assert_eq!(client.current_url().as_str(), "http://example.com/");
println!("Response status: {}", resp.status());
Ok(())
}
#[test]
fn host_updating() {
let url = Url::new("http://example.com", None).unwrap();
let url = Url::new("http://nym-api1.test", None).unwrap();
let mut client = ClientBuilder::new(url).unwrap().build().unwrap();
// check that the url is set correctly
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.as_str(), "http://nym-api1.test/");
assert_eq!(current_url.front_str(), None);
// update the url
client.update_host();
// check that the url is still the same since there is one URL
assert_eq!(client.current_url().as_str(), "http://example.com/");
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
// =======================================
// we rotate through urls when available
let new_urls = vec![
Url::new("http://example.com", None).unwrap(),
Url::new("http://example.org", None).unwrap(),
Url::new("http://nym-api1.test", None).unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
];
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://example.com/");
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
client.update_host();
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://example.org/");
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
assert_eq!(client.current_url().front_str(), None);
client.update_host();
assert_eq!(client.current_url().as_str(), "http://example.com/");
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
// =======================================
// we rotate through urls when available if fronting is disabled
let new_urls = vec![
Url::new(
"http://example.com",
Some(vec!["http://front1.com", "http://front2.com"]),
"http://nym-api1.test",
Some(vec!["http://cdn1.test", "http://cdn2.test"]),
)
.unwrap(),
Url::new("http://example.org", None).unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
];
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://example.com/");
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
client.update_host();
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://example.org/");
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
}
#[test]
#[cfg(feature = "tunneling")]
fn fronted_host_updating() {
let url = Url::new("http://example.com", Some(vec!["http://front1.com"])).unwrap();
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
let mut client = ClientBuilder::new(url)
.unwrap()
.with_fronting(crate::fronted::FrontPolicy::Always)
@@ -176,46 +180,103 @@ fn fronted_host_updating() {
// check that the url is set correctly
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
// update the url
client.update_host();
// check that the url is still the same since there is one URL and one front
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
// =======================================
// we rotate through front urls when available if fronting is enabled
let new_urls = vec![
Url::new(
"http://example.com",
Some(vec!["http://front1.com", "http://front2.com"]),
"http://nym-api.test",
Some(vec!["http://cdn1.test", "http://cdn2.test"]),
)
.unwrap(),
Url::new("http://example.org", None).unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
];
client.change_base_urls(new_urls);
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
// update the url - this should keep the same host but change the front
client.update_host();
let current_url = client.current_url();
// check that the url is still the same since there is one URL
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front2.com"));
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn2.test"));
// update the url - this should wrap around to the first front as the second url is not fronted
client.update_host();
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
}
#[test]
#[cfg(feature = "network-defaults")]
fn from_network_configures_multiple_urls_and_retries() {
use nym_network_defaults::{ApiUrl, NymNetworkDetails};
// Create network details with multiple URLs and fronting
let mut network_details = NymNetworkDetails::new_empty();
network_details.nym_api_urls = Some(vec![
ApiUrl {
url: "https://validator.nymtech.net/api/".to_string(),
front_hosts: None,
},
ApiUrl {
url: "https://nym-frontdoor.vercel.app/api/".to_string(),
front_hosts: Some(vec!["vercel.app".to_string(), "vercel.com".to_string()]),
},
ApiUrl {
url: "https://nym-frontdoor.global.ssl.fastly.net/api/".to_string(),
front_hosts: Some(vec!["yelp.global.ssl.fastly.net".to_string()]),
},
]);
// Build client from network details
let client = ClientBuilder::new_with_fronted_urls(
network_details.nym_api_urls.clone().unwrap_or_default(),
)
.expect("Failed to create client from network")
.build()
.expect("Failed to build client");
// Verify all URLs were configured
assert_eq!(
client.base_urls().len(),
3,
"Expected 3 URLs to be configured from network details"
);
// Verify the URLs have fronting configured where appropriate
assert_eq!(
client.base_urls()[0].as_str(),
"https://validator.nymtech.net/api/"
);
assert!(client.base_urls()[0].front_str().is_none());
assert_eq!(
client.base_urls()[1].as_str(),
"https://nym-frontdoor.vercel.app/api/"
);
assert!(client.base_urls()[1].front_str().is_some());
assert_eq!(
client.base_urls()[2].as_str(),
"https://nym-frontdoor.global.ssl.fastly.net/api/"
);
assert!(client.base_urls()[2].front_str().is_some());
}
+10
View File
@@ -183,6 +183,11 @@ impl Url {
})
}
/// Returns the underlying URL
pub fn inner_url(&self) -> &url::Url {
&self.url
}
/// Returns true if the URL has a front domain set
pub fn has_front(&self) -> bool {
if let Some(fronts) = &self.fronts {
@@ -201,6 +206,11 @@ impl Url {
.and_then(|url| url.host_str())
}
/// Returns the fronts
pub fn fronts(&self) -> Option<&[url::Url]> {
self.fronts.as_deref()
}
/// Return the string representation of the host (domain or IP address) for this URL, if any.
pub fn host_str(&self) -> Option<&str> {
self.url.host_str()
+13 -1
View File
@@ -124,6 +124,8 @@ impl NymNetworkDetails {
}
}
let nym_api = var(var_names::NYM_API).expect("nym api not set");
NymNetworkDetails::new_empty()
.with_network_name(var(var_names::NETWORK_NAME).expect("network name not set"))
.with_bech32_account_prefix(
@@ -149,7 +151,7 @@ impl NymNetworkDetails {
})
.with_additional_validator_endpoint(ValidatorDetails::new(
var(var_names::NYXD).expect("nyxd validator not set"),
Some(var(var_names::NYM_API).expect("nym api not set")),
Some(nym_api.clone()),
get_optional_env(var_names::NYXD_WEBSOCKET),
))
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
@@ -159,6 +161,10 @@ impl NymNetworkDetails {
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
.with_nym_api_urls(Some(vec![ApiUrl {
url: nym_api,
front_hosts: None,
}]))
}
pub fn new_mainnet() -> Self {
@@ -348,6 +354,12 @@ impl NymNetworkDetails {
self
}
#[must_use]
pub fn with_nym_api_urls(mut self, urls: Option<Vec<ApiUrl>>) -> Self {
self.nym_api_urls = urls;
self
}
pub fn nym_vpn_api_url(&self) -> Option<Url> {
self.nym_vpn_api_url.as_ref().map(|url| {
url.parse()
+2 -2
View File
@@ -24,6 +24,6 @@ pub use crate::runtime_registry::RegistryAccessError;
/// Get or create a ShutdownTracker for SDK use.
/// This provides automatic task management without requiring manual setup.
pub fn get_sdk_shutdown_tracker() -> Result<ShutdownTracker, RegistryAccessError> {
Ok(runtime_registry::RuntimeRegistry::get_or_create_sdk()?.shutdown_tracker_owned())
pub fn create_sdk_shutdown_tracker() -> Result<ShutdownTracker, RegistryAccessError> {
Ok(runtime_registry::RuntimeRegistry::create_sdk()?.shutdown_tracker_owned())
}
+34 -16
View File
@@ -19,30 +19,45 @@ pub(crate) struct RuntimeRegistry {
pub enum RegistryAccessError {
#[error("the runtime registry is poisoned")]
Poisoned,
#[error("The SDK ShutdownManager already exists")]
ExistingShutdownManager,
#[error("No existing SDK ShutdownManager")]
MissingShutdownManager,
}
impl RuntimeRegistry {
/// Get or create a ShutdownManager for SDK use.
/// Create a ShutdownManager for SDK use.
/// This manager doesn't listen to OS signals, making it suitable for library use.
pub(crate) fn get_or_create_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
/// This function overwrite any existing manager!
pub(crate) fn create_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
let mut guard = REGISTRY
.sdk_manager
.write()
.map_err(|_| RegistryAccessError::Poisoned)?;
Ok(guard
.insert(Arc::new(
ShutdownManager::new_without_signals().with_cancel_on_panic(),
))
.clone())
}
/// Get the ShutdownManager for SDK use.
/// This manager doesn't listen to OS signals, making it suitable for library use.
/// Not yet used, but maybe in the future
#[allow(dead_code)]
pub(crate) fn get_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
let guard = REGISTRY
.sdk_manager
.read()
.map_err(|_| RegistryAccessError::Poisoned)?;
if let Some(manager) = guard.as_ref() {
return Ok(manager.clone());
Ok(manager.clone())
} else {
Err(RegistryAccessError::MissingShutdownManager)
}
drop(guard);
let mut guard = REGISTRY
.sdk_manager
.write()
.map_err(|_| RegistryAccessError::Poisoned)?;
Ok(guard
.get_or_insert_with(|| {
Arc::new(ShutdownManager::new_without_signals().with_cancel_on_panic())
})
.clone())
}
/// Check if an SDK manager has been created.
@@ -85,10 +100,13 @@ mod tests {
assert!(!RuntimeRegistry::has_sdk_manager().unwrap());
let manager1 = RuntimeRegistry::get_or_create_sdk().unwrap();
// Error if nothing was created
assert!(RuntimeRegistry::get_sdk().is_err());
let manager1 = RuntimeRegistry::create_sdk().unwrap();
assert!(RuntimeRegistry::has_sdk_manager().unwrap());
let manager2 = RuntimeRegistry::get_or_create_sdk().unwrap();
let manager2 = RuntimeRegistry::get_sdk().unwrap();
// Should return the same instance
assert!(Arc::ptr_eq(&manager1, &manager2));
+2 -4
View File
@@ -160,13 +160,12 @@ pub async fn setup_gateway_from_api(
minimum_performance: u8,
ignore_epoch_roles: bool,
) -> Result<InitialisationResult, WasmCoreError> {
let mut rng = thread_rng();
let gateways = gateways_for_init(
&mut rng,
nym_apis,
None,
minimum_performance,
ignore_epoch_roles,
None,
)
.await?;
setup_gateway_wasm(client_store, force_tls, chosen_gateway, gateways).await
@@ -178,13 +177,12 @@ pub async fn current_gateways_wasm(
minimum_performance: u8,
ignore_epoch_roles: bool,
) -> Result<Vec<RoutingNode>, ClientCoreError> {
let mut rng = thread_rng();
gateways_for_init(
&mut rng,
nym_apis,
user_agent,
minimum_performance,
ignore_epoch_roles,
None,
)
.await
}
+1 -1
View File
@@ -259,7 +259,7 @@ pub fn migrate(deps: DepsMut<'_>, env: Env, _msg: MigrateMsg) -> Result<Response
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// crate::queued_migrations::introduce_historical_epochs(deps, env)?;
crate::queued_migrations::introduce_historical_epochs(deps, env)?;
Ok(Response::new())
}
@@ -110,7 +110,7 @@ pub fn try_transfer_ownership(
DEALERS_INDICES.save(deps.storage, &transfer_to, &current_index)?;
DEALERS_INDICES.remove(deps.storage, &info.sender);
// update registration detail for every epoch the current dealer has participated in the protocol
// update registration detail and share information for every epoch the current dealer has participated in the protocol
// ideally, we'd have only updated the current epoch, but the way the contract is constructed
// forbids that otherwise we'd have introduced inconsistency
for epoch_id in 0..=epoch.epoch_id {
@@ -118,6 +118,11 @@ pub fn try_transfer_ownership(
EPOCH_DEALERS_MAP.remove(deps.storage, (epoch_id, &info.sender));
EPOCH_DEALERS_MAP.save(deps.storage, (epoch_id, &transfer_to), &details)?;
}
if let Some(mut vk_share) = vk_shares().may_load(deps.storage, (&info.sender, epoch_id))? {
vk_shares().remove(deps.storage, (&info.sender, epoch_id))?;
vk_share.owner = transfer_to.clone();
vk_shares().save(deps.storage, (&transfer_to, epoch_id), &vk_share)?;
}
}
let Some(transaction_info) = env.transaction else {
@@ -262,6 +267,7 @@ mod tests_with_mock {
contract.run_initial_dummy_dkg();
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
let old_details = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
let old_share = vk_shares().load(&contract, (&group_member, 0))?;
let not_group_member = contract.addr_make("not_group_member");
let (deps, env) = contract.deps_mut_env();
@@ -291,13 +297,20 @@ mod tests_with_mock {
assert!(EPOCH_DEALERS_MAP
.may_load(&contract, (0, &group_member))?
.is_none());
assert!(vk_shares()
.may_load(&contract, (&group_member, 0))?
.is_none());
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
let new_details = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
let new_share = vk_shares().load(&contract, (&new_group_member, 0))?;
// the underlying info hasn't changed
assert_eq!(old_index, new_index);
assert_eq!(old_details, new_details);
assert_ne!(old_share, new_share);
assert_eq!(old_share.owner, group_member);
assert_eq!(new_share.owner, new_group_member);
assert_eq!(
OWNERSHIP_TRANSFER_LOG.load(
@@ -15,6 +15,7 @@ Operators can use [Nym Bridge Configuration Tool](https://github.com/nymtech/nym
<Steps>
###### 1. Download [`quic_bridge_deployment.sh`](https://github.com/nymtech/nym/blob/develop/scripts/nym-node-setup/quic_bridge_deployment.sh) script
- SSH to your server
- **Run as root**
- Download the script and make executable
```sh
wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh && \
@@ -26,7 +27,7 @@ chmod +x quic_bridge_deployment.sh
- Optional: open `tmux` in case you will need to run another commands on the VPS
- Run the script with a command `full_bridge_setup`
```sh
./nym-node-setup/quic_bridge_deployment.sh full_bridge_setup
./quic_bridge_deployment.sh full_bridge_setup
```
###### 3. Follow the interactive prompts
@@ -50,7 +50,7 @@ impl AuthClientMixnetListener {
}
}
async fn run(mut self) -> Self {
async fn run(mut self) {
let mixnet_cancel_token = self.mixnet_client.cancellation_token();
self.shutdown_token.run_until_cancelled(async {
loop {
@@ -95,12 +95,8 @@ impl AuthClientMixnetListener {
tracing::debug!("AuthClientMixnetListener is shutting down");
}).await;
self
}
// Disconnects the mixnet client and effectively drop itself, since it doesn't work without one, and reconnecting isn't supported
pub async fn disconnect_mixnet_client(self) {
if !self.mixnet_client.cancellation_token().is_cancelled() {
tracing::debug!("AuthClientMixnetListener: Disconnect mixnet client");
if !mixnet_cancel_token.is_cancelled() {
self.mixnet_client.disconnect().await;
}
}
@@ -128,7 +124,7 @@ pub struct AuthClientMixnetListenerHandle {
message_sender: MixnetMessageInputSender,
cancellation_token: CancellationToken,
mixnet_cancellation_token: CancellationToken,
handle: JoinHandle<AuthClientMixnetListener>,
handle: JoinHandle<()>,
}
impl AuthClientMixnetListenerHandle {
@@ -148,13 +144,8 @@ impl AuthClientMixnetListenerHandle {
// If shutdown was externally called, that call is a no-op
// If we're only stopping this, it is very much needed
self.cancellation_token.cancel();
match self.handle.await {
Ok(auth_client_mixnet_listener) => {
auth_client_mixnet_listener.disconnect_mixnet_client().await;
}
Err(e) => {
tracing::error!("Error waiting for auth clients mixnet listener to stop: {e}");
}
if let Err(e) = self.handle.await {
tracing::error!("Error waiting for auth clients mixnet listener to stop: {e}")
}
}
}
+1 -1
View File
@@ -765,7 +765,7 @@ async fn connect_exit(
);
// The IPR supports cancellation, but it's unused in the gateway probe
let cancel_token = CancellationToken::new();
let mut ipr_client = IprClientConnect::new(mixnet_client, cancel_token).await;
let mut ipr_client = IprClientConnect::new(mixnet_client, cancel_token);
let maybe_ip_pair = ipr_client.connect(exit_router_address).await;
let mixnet_client = ipr_client.into_mixnet_client();
+1 -1
View File
@@ -43,7 +43,7 @@ pub struct IprClientConnect {
}
impl IprClientConnect {
pub async fn new(mixnet_client: MixnetClient, cancel_token: CancellationToken) -> Self {
pub fn new(mixnet_client: MixnetClient, cancel_token: CancellationToken) -> Self {
Self {
mixnet_client,
connected: ConnectionState::Disconnected,
@@ -57,7 +57,7 @@ async fn run(
.clone()
.expect("rust sdk mainnet default missing api_url");
let nym_api = nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()])
let nym_api = nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()])?
.no_hickory_dns()
.with_timeout(nym_api_client_timeout)
.build()?;
@@ -98,7 +98,7 @@ impl Monitor {
.expect("rust sdk mainnet default missing api_url");
let nym_api =
nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()])
nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()])?
.no_hickory_dns()
.with_timeout(self.nym_api_client_timeout)
.build()?;
+1
View File
@@ -17,6 +17,7 @@ thiserror.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tracing.workspace = true
typed-builder.workspace = true
url.workspace = true
nym-authenticator-client = { path = "../nym-authenticator-client" }
@@ -15,10 +15,12 @@ use nym_sdk::{
use std::os::fd::RawFd;
use std::{path::PathBuf, sync::Arc, time::Duration};
use tokio_util::sync::CancellationToken;
use typed_builder::TypedBuilder;
use crate::error::RegistrationClientError;
const VPN_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(15);
const MIXNET_CLIENT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone)]
pub struct NymNodeWithKeys {
@@ -26,11 +28,14 @@ pub struct NymNodeWithKeys {
pub keys: Arc<KeyPair>,
}
#[derive(TypedBuilder)]
pub struct BuilderConfig {
pub entry_node: NymNodeWithKeys,
pub exit_node: NymNodeWithKeys,
pub data_path: Option<PathBuf>,
pub mixnet_client_config: MixnetClientConfig,
#[builder(default = MIXNET_CLIENT_STARTUP_TIMEOUT)]
pub mixnet_client_startup_timeout: Duration,
pub two_hops: bool,
pub user_agent: UserAgent,
pub custom_topology_provider: Box<dyn TopologyProvider + Send + Sync>,
@@ -116,6 +121,7 @@ impl BuilderConfig {
.credentials_mode(true)
.with_remember_me(remember_me)
.custom_topology_provider(self.custom_topology_provider);
#[cfg(unix)]
let builder = builder.with_connection_fd_callback(self.connection_fd_callback);
@@ -205,3 +211,17 @@ fn log_mixnet_client_config(debug_config: &DebugConfig) {
fn true_to_disabled(val: bool) -> &'static str {
if val { "disabled" } else { "enabled" }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mixnet_client_config_default_values() {
let config = MixnetClientConfig::default();
assert!(!config.disable_poisson_rate);
assert!(!config.disable_background_cover_traffic);
assert_eq!(config.min_mixnode_performance, None);
assert_eq!(config.min_gateway_performance, None);
}
}
+2 -5
View File
@@ -12,15 +12,12 @@ use nym_validator_client::{
QueryHttpRpcNyxdClient,
nyxd::{Config as NyxdClientConfig, NyxdClient},
};
use std::time::Duration;
use crate::{RegistrationClient, config::RegistrationClientConfig, error::RegistrationClientError};
use config::BuilderConfig;
pub(crate) mod config;
pub(crate) const MIXNET_CLIENT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
pub struct RegistrationClientBuilder {
pub config: BuilderConfig,
}
@@ -49,7 +46,7 @@ impl RegistrationClientBuilder {
let builder = MixnetClientBuilder::new_with_storage(mixnet_client_storage)
.event_tx(EventSender(event_tx));
let mixnet_client = tokio::time::timeout(
MIXNET_CLIENT_STARTUP_TIMEOUT,
self.config.mixnet_client_startup_timeout,
self.config.build_and_connect_mixnet_client(builder),
)
.await??;
@@ -59,7 +56,7 @@ impl RegistrationClientBuilder {
} else {
let builder = MixnetClientBuilder::new_ephemeral().event_tx(EventSender(event_tx));
let mixnet_client = tokio::time::timeout(
MIXNET_CLIENT_STARTUP_TIMEOUT,
self.config.mixnet_client_startup_timeout,
self.config.build_and_connect_mixnet_client(builder),
)
.await??;
+100 -49
View File
@@ -9,6 +9,7 @@ use nym_credentials_interface::TicketType;
use nym_ip_packet_client::IprClientConnect;
use nym_registration_common::AssignedAddresses;
use nym_sdk::mixnet::{EventReceiver, MixnetClient, Recipient};
use tracing::debug;
use crate::config::RegistrationClientConfig;
@@ -34,23 +35,49 @@ pub struct RegistrationClient {
event_rx: EventReceiver,
}
// Bundle of an actual error and the underlying mixnet client so it can be shutdown correctly if needed
struct RegistrationError {
mixnet_client: Option<MixnetClient>,
source: crate::RegistrationClientError,
}
impl RegistrationClient {
async fn register_mix_exit(self) -> Result<RegistrationResult, RegistrationClientError> {
async fn register_mix_exit(self) -> Result<RegistrationResult, RegistrationError> {
let entry_mixnet_gateway_ip = self.config.entry.node.ip_address;
let exit_mixnet_gateway_ip = self.config.exit.node.ip_address;
let ipr_address = self.config.exit.node.ipr_address.ok_or(
RegistrationClientError::NoIpPacketRouterAddress {
node_id: self.config.exit.node.identity.to_base58_string(),
},
)?;
let Some(ipr_address) = self.config.exit.node.ipr_address else {
return Err(RegistrationError {
mixnet_client: Some(self.mixnet_client),
source: RegistrationClientError::NoIpPacketRouterAddress {
node_id: self.config.exit.node.identity.to_base58_string(),
},
});
};
let mut ipr_client =
IprClientConnect::new(self.mixnet_client, self.cancel_token.clone()).await;
let interface_addresses = ipr_client
.connect(ipr_address)
IprClientConnect::new(self.mixnet_client, self.cancel_token.child_token());
let interface_addresses = match self
.cancel_token
.run_until_cancelled(ipr_client.connect(ipr_address))
.await
.map_err(RegistrationClientError::ConnectToIpPacketRouter)?;
{
Some(Ok(addr)) => addr,
Some(Err(e)) => {
return Err(RegistrationError {
mixnet_client: Some(ipr_client.into_mixnet_client()),
source: RegistrationClientError::ConnectToIpPacketRouter(e),
});
}
None => {
return Err(RegistrationError {
mixnet_client: Some(ipr_client.into_mixnet_client()),
source: RegistrationClientError::Cancelled,
});
}
};
Ok(RegistrationResult::Mixnet(Box::new(
MixnetRegistrationResult {
@@ -67,18 +94,24 @@ impl RegistrationClient {
)))
}
async fn register_wg(self) -> Result<RegistrationResult, RegistrationClientError> {
let entry_auth_address = self.config.entry.node.authenticator_address.ok_or(
RegistrationClientError::AuthenticationNotPossible {
node_id: self.config.entry.node.identity.to_base58_string(),
},
)?;
async fn register_wg(self) -> Result<RegistrationResult, RegistrationError> {
let Some(entry_auth_address) = self.config.entry.node.authenticator_address else {
return Err(RegistrationError {
mixnet_client: Some(self.mixnet_client),
source: RegistrationClientError::AuthenticationNotPossible {
node_id: self.config.entry.node.identity.to_base58_string(),
},
});
};
let exit_auth_address = self.config.exit.node.authenticator_address.ok_or(
RegistrationClientError::AuthenticationNotPossible {
node_id: self.config.exit.node.identity.to_base58_string(),
},
)?;
let Some(exit_auth_address) = self.config.exit.node.authenticator_address else {
return Err(RegistrationError {
mixnet_client: Some(self.mixnet_client),
source: RegistrationClientError::AuthenticationNotPossible {
node_id: self.config.exit.node.identity.to_base58_string(),
},
});
};
let entry_version = self.config.entry.node.version;
tracing::debug!("Entry gateway version: {entry_version}");
@@ -87,8 +120,10 @@ impl RegistrationClient {
// Start the auth client mixnet listener, which will listen for incoming messages from the
// mixnet and rebroadcast them to the auth clients.
// From this point on, we don't need to care about the mixnet client anymore
let mixnet_listener =
AuthClientMixnetListener::new(self.mixnet_client, self.cancel_token.clone()).start();
AuthClientMixnetListener::new(self.mixnet_client, self.cancel_token.child_token())
.start();
let mut entry_auth_client = AuthenticatorClient::new(
mixnet_listener.subscribe(),
@@ -115,24 +150,33 @@ impl RegistrationClient {
let exit_fut = exit_auth_client
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardExit);
let (entry, exit) = Box::pin(async { tokio::join!(entry_fut, exit_fut) }).await;
let (entry, exit) = Box::pin(
self.cancel_token
.run_until_cancelled(async { tokio::join!(entry_fut, exit_fut) }),
)
.await
.ok_or(RegistrationError {
mixnet_client: None,
source: RegistrationClientError::Cancelled,
})?;
let entry =
entry.map_err(
|source| RegistrationClientError::EntryGatewayRegisterWireguard {
gateway_id: self.config.entry.node.identity.to_base58_string(),
authenticator_address: Box::new(entry_auth_address),
source: Box::new(source),
},
)?;
let exit =
exit.map_err(
|source| RegistrationClientError::ExitGatewayRegisterWireguard {
gateway_id: self.config.exit.node.identity.to_base58_string(),
authenticator_address: Box::new(exit_auth_address),
source: Box::new(source),
},
)?;
let entry = entry.map_err(|source| RegistrationError {
mixnet_client: None,
source: RegistrationClientError::EntryGatewayRegisterWireguard {
gateway_id: self.config.entry.node.identity.to_base58_string(),
authenticator_address: Box::new(entry_auth_address),
source: Box::new(source),
},
})?;
let exit = exit.map_err(|source| RegistrationError {
mixnet_client: None,
source: RegistrationClientError::EntryGatewayRegisterWireguard {
gateway_id: self.config.exit.node.identity.to_base58_string(),
authenticator_address: Box::new(exit_auth_address),
source: Box::new(source),
},
})?;
Ok(RegistrationResult::Wireguard(Box::new(
WireguardRegistrationResult {
@@ -147,16 +191,23 @@ impl RegistrationClient {
}
pub async fn register(self) -> Result<RegistrationResult, RegistrationClientError> {
self.cancel_token
.clone()
.run_until_cancelled(async {
if self.config.two_hops {
self.register_wg().await
} else {
self.register_mix_exit().await
let registration_result = if self.config.two_hops {
self.register_wg().await
} else {
self.register_mix_exit().await
};
// If we failed to register, and we were the owner of the mixnet client, shut it down
match registration_result {
Ok(result) => Ok(result),
Err(error) => {
debug!("Registration failed");
if let Some(mixnet_client) = error.mixnet_client {
debug!("Shutting down mixnet client");
mixnet_client.disconnect().await;
}
})
.await
.ok_or(RegistrationClientError::Cancelled)?
Err(error.source)
}
}
}
}
File diff suppressed because it is too large Load Diff
+39 -13
View File
@@ -555,21 +555,22 @@ where
async fn available_gateways(&mut self) -> Result<Vec<RoutingNode>, ClientCoreError> {
if let Some(ref mut custom_provider) = self.custom_topology_provider {
if let Some(topology) = custom_provider.get_new_topology().await {
return Ok(topology.entry_gateways().cloned().collect());
// Use entry_capable_nodes() instead of entry_gateways() to include
// all entry-capable nodes, not just actively assigned ones
return Ok(topology.entry_capable_nodes().cloned().collect());
}
}
let nym_api_endpoints = self.get_api_endpoints();
let topology_cfg = &self.config.debug_config.topology;
let user_agent = self.user_agent.clone();
let mut rng = OsRng;
gateways_for_init(
&mut rng,
&nym_api_endpoints,
user_agent,
topology_cfg.minimum_gateway_performance,
topology_cfg.ignore_ingress_epoch_role,
None,
)
.await
}
@@ -705,6 +706,16 @@ where
.config
.as_base_client_config(nyxd_endpoints, nym_api_endpoints.clone());
tracing::debug!(
"SDK: Passing nym_api_urls to BaseClientBuilder (has {} nym_api_urls)",
self.config
.network_details
.nym_api_urls
.as_ref()
.map(|urls| urls.len())
.unwrap_or(0)
);
let mut base_builder: BaseClientBuilder<_, _> =
BaseClientBuilder::new(base_config, self.storage, self.dkg_query_client)
.with_wait_for_gateway(self.wait_for_gateway)
@@ -712,6 +723,11 @@ where
.with_remember_me(&self.remember_me)
.with_derivation_material(self.derivation_material);
// Add nym_api_urls if available in network_details
if let Some(nym_api_urls) = self.config.network_details.nym_api_urls.clone() {
base_builder = base_builder.with_nym_api_urls(nym_api_urls);
}
if let Some(user_agent) = self.user_agent {
base_builder = base_builder.with_user_agent(user_agent);
}
@@ -720,15 +736,11 @@ where
base_builder = base_builder.with_topology_provider(topology_provider);
}
// Use custom shutdown if provided, otherwise get from registry
let shutdown_tracker = match self.custom_shutdown {
Some(custom) => custom,
None => {
// Auto-create from registry for SDK use
nym_task::get_sdk_shutdown_tracker()?
}
};
base_builder = base_builder.with_shutdown(shutdown_tracker);
// Use custom shutdown if provided, otherwise the sdk one will be used later down the line
if let Some(shutdown_tracker) = self.custom_shutdown {
base_builder = base_builder.with_shutdown(shutdown_tracker);
}
if let Some(event_tx) = self.event_tx {
base_builder = base_builder.with_event_tx(event_tx);
}
@@ -793,7 +805,7 @@ where
client_output,
client_state.clone(),
nym_address,
started_client.shutdown_handle.child_tracker(),
started_client.shutdown_handle.clone(),
packet_type,
);
@@ -880,3 +892,17 @@ impl IncludedSurbs {
Self::ExposeSelfAddress
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mixnet_builder_default_no_custom_client() {
let builder = MixnetClientBuilder::new_ephemeral();
assert!(
builder.build().is_ok(),
"Builder should succeed without custom client"
);
}
}
+4 -2
View File
@@ -226,7 +226,8 @@ mod tests {
error!("{err}");
// this is not an ideal way of checking it, but if test fails due to networking failures
// it should be fine to progress
if err.to_string().contains("nym api request failed") {
let err_str = err.to_string();
if err_str.contains("nym api") || err_str.contains("failed to connect") {
return Ok(());
}
return Err(err);
@@ -291,7 +292,8 @@ mod tests {
error!("{err}");
// this is not an ideal way of checking it, but if test fails due to networking failures
// it should be fine to progress
if err.to_string().contains("nym api request failed") {
let err_str = err.to_string();
if err_str.contains("nym api") || err_str.contains("failed to connect") {
return Ok(());
}
return Err(err);