Compare commits

..

3 Commits

Author SHA1 Message Date
benedettadavico 867acfef26 .. 2025-10-22 10:02:17 +02:00
benedettadavico e4996dc0ce comment out migration 2025-10-21 14:28:01 +02:00
Jędrzej Stuczyński a6e23a210b bugfix: update stored epoch share when changing ownership 2025-10-21 11:10:24 +01:00
72 changed files with 1634 additions and 2134 deletions
-3
View File
@@ -8,13 +8,10 @@ on:
- 'gateway/**'
- 'integrations/**'
- 'nym-api/**'
- 'nym-authenticator-client/**'
- 'nym-credential-proxy/**'
- 'nym-ip-packet-client/**'
- 'nym-network-monitor/**'
- 'nym-node/**'
- 'nym-node-status-api/**'
- 'nym-registration-client/**'
- 'nym-statistics-api/**'
- 'nym-outfox/**'
- 'nym-validator-rewarder/**'
+30 -3
View File
@@ -3,6 +3,11 @@ name: Build and upload Node Status agent container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
gateway_probe_git_ref:
type: string
default: nym-vpn-core-v1.4.0
required: true
description: Which gateway probe git ref to build the image with
release_image:
description: 'Tag image as a release'
required: true
@@ -38,6 +43,16 @@ jobs:
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: cleanup-gateway-probe-ref
id: cleanup_gateway_probe_ref
run: |
GATEWAY_PROBE_GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }}
GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}"
echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
@@ -46,12 +61,24 @@ jobs:
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
# - name: Remove existing tag if exists
# run: |
# if git rev-parse $${{ env.GIT_TAG }} >/dev/null 2>&1; then
# git push --delete origin $${{ env.GIT_TAG }}
# git tag -d $${{ env.GIT_TAG }}
# fi
# - name: Create tag
# run: |
# git tag -a $${{ env.GIT_TAG }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}"
# git push origin $${{ env.GIT_TAG }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
-72
View File
@@ -4,78 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.20-leerdammer] (2025-11-12)
- Max/tweak ts sdk actions ([#6185])
- chore: resolve clippy 1.91 warnings ([#6168])
- [chore] Remove unused dependencies ([#6151])
- Use typed-builder for registration client builder config ([#6150])
- tommy is too quick ([#6149])
- configurable mixnet client startup timeout ([#6148])
- [Feature/operators]: QUIC bridge deployment script v2 ([#6145])
- Bugfix: Add circuit breaker ([#6143])
- bugfix: update internal owner address in transferred share ([#6139])
- Update quic_bridge_deployment.sh for IPv4 and .deb package ([#6138])
- feat: expose more explicit new_with_fronted_urls builder for http API client ([#6136])
- bugfix: update stored epoch share when changing ownership ([#6135])
- Domain fronting ([#6134])
- bugfix: update stored epoch share when changing announce address ([#6131])
[#6185]: https://github.com/nymtech/nym/pull/6185
[#6168]: https://github.com/nymtech/nym/pull/6168
[#6151]: https://github.com/nymtech/nym/pull/6151
[#6150]: https://github.com/nymtech/nym/pull/6150
[#6149]: https://github.com/nymtech/nym/pull/6149
[#6148]: https://github.com/nymtech/nym/pull/6148
[#6145]: https://github.com/nymtech/nym/pull/6145
[#6143]: https://github.com/nymtech/nym/pull/6143
[#6139]: https://github.com/nymtech/nym/pull/6139
[#6138]: https://github.com/nymtech/nym/pull/6138
[#6136]: https://github.com/nymtech/nym/pull/6136
[#6135]: https://github.com/nymtech/nym/pull/6135
[#6134]: https://github.com/nymtech/nym/pull/6134
[#6131]: https://github.com/nymtech/nym/pull/6131
## [2025.19-kase] (2025-10-30)
- update ns agent workflow ([#6154])
- Cherry pick - request #6143 from nymtech/bugfix/mix-tx-closed-v2 ([#6153])
- bugfix: nym-credential-proxy query params parsing regression ([#6121])
- bugfix: revert some dep updates introduced in #6043 ([#6120])
- Skip ipv6 metadata endpoint request ([#6118])
- update to no longer use 1mb files ([#6117])
- chore: restore pending dkg contract state migration ([#6116])
- Revert "Propagate cancel token to mixnet client" ([#6115])
- Update dirs to 6.0 ([#6109])
- Propagate cancel token to mixnet client ([#6105])
- bugfix: retrieve and update ticketbook in the same query ([#6101])
- bugfix: include network name in the default gateway probe config path ([#6100])
- Bugfix/incompatibility fixes ([#6099])
- [DOCs/operators] QUIC deployment script & docs ([#6098])
- bugfix: testnet manager 02sql migration ([#6096])
- feat: move gateway probe to monorepo (and update to rust edition 2024) ([#6094])
- bugfix: use custom topology provider for list of init gateways ([#6092])
- Max/fix wasm client + build commands ([#6043])
[#6154]: https://github.com/nymtech/nym/pull/6154
[#6153]: https://github.com/nymtech/nym/pull/6153
[#6121]: https://github.com/nymtech/nym/pull/6121
[#6120]: https://github.com/nymtech/nym/pull/6120
[#6118]: https://github.com/nymtech/nym/pull/6118
[#6117]: https://github.com/nymtech/nym/pull/6117
[#6116]: https://github.com/nymtech/nym/pull/6116
[#6115]: https://github.com/nymtech/nym/pull/6115
[#6109]: https://github.com/nymtech/nym/pull/6109
[#6105]: https://github.com/nymtech/nym/pull/6105
[#6101]: https://github.com/nymtech/nym/pull/6101
[#6100]: https://github.com/nymtech/nym/pull/6100
[#6099]: https://github.com/nymtech/nym/pull/6099
[#6098]: https://github.com/nymtech/nym/pull/6098
[#6096]: https://github.com/nymtech/nym/pull/6096
[#6094]: https://github.com/nymtech/nym/pull/6094
[#6092]: https://github.com/nymtech/nym/pull/6092
[#6043]: https://github.com/nymtech/nym/pull/6043
## [2025.18-jarlsberg] (2025-10-14)
- ns-api: add descriptions to dVPN gateway responses ([#6102])
Generated
+11 -33
View File
@@ -133,9 +133,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ammonia"
version = "4.1.2"
version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f"
dependencies = [
"cssparser",
"html5ever",
@@ -2262,7 +2262,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -4824,7 +4824,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.69"
version = "1.1.67"
dependencies = [
"anyhow",
"async-trait",
@@ -5050,7 +5050,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.66"
version = "1.1.64"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5133,7 +5133,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.66"
version = "1.1.64"
dependencies = [
"bs58",
"clap",
@@ -6050,7 +6050,6 @@ dependencies = [
"thiserror 2.0.12",
"tokio",
"tracing",
"tracing-subscriber",
"url",
"wasmtimer",
]
@@ -6355,7 +6354,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.67"
version = "1.1.65"
dependencies = [
"addr",
"anyhow",
@@ -6405,7 +6404,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.21.0"
version = "1.19.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6547,7 +6546,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.0.11-rc1"
version = "4.0.10"
dependencies = [
"ammonia",
"anyhow",
@@ -6799,7 +6798,6 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"typed-builder",
"url",
]
@@ -6932,7 +6930,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.66"
version = "1.1.64"
dependencies = [
"bs58",
"clap",
@@ -7671,7 +7669,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.31"
version = "0.1.29"
dependencies = [
"anyhow",
"bytes",
@@ -11393,26 +11391,6 @@ 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"
+12 -1
View File
@@ -215,6 +215,7 @@ 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"
@@ -242,11 +243,13 @@ 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"
@@ -262,8 +265,11 @@ 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"
@@ -287,10 +293,12 @@ 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"
@@ -299,6 +307,7 @@ 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"
@@ -306,10 +315,13 @@ 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"
@@ -356,7 +368,6 @@ 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
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.66"
version = "1.1.64"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.66"
version = "1.1.64"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+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", features = ["network-defaults"] }
nym-http-api-client = { path = "../http-api-client" }
nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" }
nym-sphinx = { path = "../nymsphinx" }
nym-statistics-common = { path = "../statistics" }
+16 -134
View File
@@ -73,10 +73,6 @@ 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",
@@ -216,9 +212,6 @@ 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>>,
@@ -248,7 +241,6 @@ 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,
@@ -271,16 +263,6 @@ 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;
@@ -801,7 +783,7 @@ where
event_tx,
);
let mix_tx = mix_traffic_controller.mix_tx();
let mix_tx = mix_traffic_controller.mix_rx();
let client_tx = mix_traffic_controller.client_tx();
shutdown_tracker.try_spawn_named(
@@ -881,67 +863,21 @@ 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());
// 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();
let mut builder = nym_http_api_client::Client::builder(nym_api_urls[0].clone())
.map_err(ClientCoreError::from)?;
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
}
builder = builder.with_bincode();
builder.build().map_err(ClientCoreError::from)
}
@@ -1004,8 +940,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.clone(),
None => nym_task::create_sdk_shutdown_tracker()?,
Some(parent_tracker) => parent_tracker.child_tracker(),
None => nym_task::get_sdk_shutdown_tracker()?,
};
Self::start_event_control(self.event_tx, event_receiver, &shutdown_tracker);
@@ -1025,11 +961,7 @@ where
.dkg_query_client
.map(|client| BandwidthController::new(credential_store, client));
let nym_api_client = Self::construct_nym_api_client(
self.nym_api_urls.as_ref(),
&self.config,
self.user_agent.clone(),
)?;
let nym_api_client = Self::construct_nym_api_client(&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(
@@ -1044,7 +976,7 @@ where
self.user_agent.clone(),
generate_client_stats_id(*self_address.identity()),
input_sender.clone(),
&shutdown_tracker.clone(),
&shutdown_tracker.child_tracker(),
);
// needs to be started as the first thing to block if required waiting for the gateway
@@ -1054,7 +986,7 @@ where
shared_topology_accessor.clone(),
self_address.gateway(),
self.wait_for_gateway,
&shutdown_tracker.clone(),
&shutdown_tracker.child_tracker(),
)
.await?;
@@ -1074,7 +1006,7 @@ where
stats_reporter.clone(),
#[cfg(unix)]
self.connection_fd_callback,
&shutdown_tracker.clone(),
&shutdown_tracker.child_tracker(),
)
.await?;
let gateway_ws_fd = gateway_transceiver.ws_fd();
@@ -1082,7 +1014,7 @@ where
let reply_storage = Self::setup_persistent_reply_storage(
reply_storage_backend,
key_rotation_config,
&shutdown_tracker.clone(),
&shutdown_tracker.child_tracker(),
)
.await?;
@@ -1093,7 +1025,7 @@ where
reply_storage.key_storage(),
reply_controller_sender.clone(),
stats_reporter.clone(),
&shutdown_tracker.clone(),
&shutdown_tracker.child_tracker(),
);
// The message_sender is the transmitter for any component generating sphinx packets
@@ -1103,7 +1035,7 @@ where
let (message_sender, client_request_sender) = Self::start_mix_traffic_controller(
gateway_transceiver,
&shutdown_tracker.clone(),
&shutdown_tracker.child_tracker(),
EventSender(event_sender),
);
@@ -1134,7 +1066,7 @@ where
shared_lane_queue_lengths.clone(),
client_connection_rx,
stats_reporter.clone(),
&shutdown_tracker.clone(),
&shutdown_tracker.child_tracker(),
);
if !self
@@ -1150,7 +1082,7 @@ where
shared_topology_accessor.clone(),
message_sender,
stats_reporter.clone(),
&shutdown_tracker.clone(),
&shutdown_tracker.child_tracker(),
);
}
@@ -1204,53 +1136,3 @@ 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::trace!("Failed to send cover message - channel full");
tracing::debug!("Failed to send cover message - channel full");
}
TrySendError::Closed(_) => {
tracing::warn!("Failed to send cover message - channel closed");
@@ -20,10 +20,7 @@ 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;
/// 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;
const MAX_FAILURE_COUNT: usize = 100;
// that's also disgusting.
pub struct Empty;
@@ -87,7 +84,7 @@ impl MixTrafficController {
self.client_tx.clone()
}
pub fn mix_tx(&self) -> BatchMixMessageSender {
pub fn mix_rx(&self) -> BatchMixMessageSender {
self.mix_tx.clone()
}
@@ -159,11 +156,6 @@ 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,8 +298,6 @@ 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() {
+1 -1
View File
@@ -45,7 +45,7 @@ pub enum ClientCoreError {
#[cfg(not(target_arch = "wasm32"))]
#[error("resolution failed: {0}")]
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
#[error("no gateways on network")]
NoGatewaysOnNetwork,
+2 -2
View File
@@ -151,7 +151,7 @@ pub async fn gateways_for_init(
}
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())?
let mut builder = nym_http_api_client::ClientBuilder::new_with_urls(nym_api_urls.clone())
.with_retries(retry_count)
.with_bincode();
@@ -441,7 +441,7 @@ mod tests {
#[test]
fn test_multiple_urls_prepared_for_retries() {
let urls = [
let urls = vec![
Url::parse("https://api1.nym.com").unwrap(),
Url::parse("https://api2.nym.com").unwrap(),
Url::parse("https://api3.nym.com").unwrap(),
@@ -30,6 +30,7 @@ pub(crate) async fn connect_async(
resolver
.resolve_str(domain)
.await?
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect()
}
@@ -39,6 +39,7 @@ pub(crate) async fn connect_async(
resolver
.resolve_str(domain)
.await?
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect()
}
@@ -54,7 +54,7 @@ pub enum GatewayClientError {
#[cfg(not(target_arch = "wasm32"))]
#[error("resolution failed: {0}")]
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
#[error("No shared key was provided or obtained")]
NoSharedKeyAvailable,
@@ -241,28 +241,23 @@ impl Epoch {
//
// Note: It's important that the variant ordering is not changed otherwise it would mess up the derived `PartialOrd`
#[cw_serde]
#[derive(Copy, Default)]
#[derive(Copy)]
pub enum EpochState {
#[default]
WaitingInitialisation,
PublicKeySubmission {
resharing: bool,
},
DealingExchange {
resharing: bool,
},
VerificationKeySubmission {
resharing: bool,
},
VerificationKeyValidation {
resharing: bool,
},
VerificationKeyFinalization {
resharing: bool,
},
PublicKeySubmission { resharing: bool },
DealingExchange { resharing: bool },
VerificationKeySubmission { resharing: bool },
VerificationKeyValidation { resharing: bool },
VerificationKeyFinalization { resharing: bool },
InProgress,
}
impl Default for EpochState {
fn default() -> Self {
Self::WaitingInitialisation
}
}
impl Display for EpochState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
@@ -246,7 +246,7 @@ mod tests {
let _exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[signing_keys.secret_key()],
&[signing_keys.verification_key()],
&vec![signing_keys.verification_key()],
&signing_keys.verification_key(),
&[1],
)?;
@@ -263,7 +263,7 @@ mod tests {
let wallet = issuance.aggregate_signature_shares(
&signing_keys.verification_key(),
&[partial_wallet],
&vec![partial_wallet],
sig_req,
)?;
+3 -3
View File
@@ -82,7 +82,7 @@ mod tests {
let exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[keypair.secret_key()],
&[keypair.verification_key()],
&vec![keypair.verification_key()],
&keypair.verification_key(),
&[keypair.index.unwrap()],
)
@@ -106,14 +106,14 @@ mod tests {
.unwrap();
let wallet = issuance
.aggregate_signature_shares(&keypair.verification_key(), &[partial_wallet], sig_req)
.aggregate_signature_shares(&keypair.verification_key(), &vec![partial_wallet], sig_req)
.unwrap();
let mut issued = issuance.into_issued_ticketbook(wallet, 1);
let coin_indices_signatures = generate_coin_indices_signatures(
nym_credentials_interface::ecash_parameters(),
&[keypair.secret_key()],
&[keypair.verification_key()],
&vec![keypair.verification_key()],
&keypair.verification_key(),
&[keypair.index.unwrap()],
)
+3 -2
View File
@@ -32,7 +32,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
itertools = { workspace = true }
inventory = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
# used for decoding text responses (they were already implicitly included)
bytes = { workspace = true }
encoding_rs = { workspace = true }
@@ -52,4 +52,5 @@ workspace = true
features = ["tokio"]
[dev-dependencies]
tracing-subscriber.workspace = true
tokio = { workspace = true, features = ["rt", "macros"] }
+92 -371
View File
@@ -30,26 +30,19 @@
use crate::ClientBuilder;
use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
str::FromStr,
net::SocketAddr,
sync::{Arc, LazyLock},
time::Duration,
};
use hickory_resolver::{
TokioResolver,
ResolveError, TokioResolver,
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ServerOrderingStrategy},
lookup_ip::LookupIpIntoIter,
lookup_ip::{LookupIp, LookupIpIntoIter},
name_server::TokioConnectionProvider,
};
use once_cell::sync::OnceCell;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use tracing::*;
mod constants;
mod static_resolver;
pub use static_resolver::*;
use tracing::warn;
impl ClientBuilder {
/// Override the DNS resolver implementation used by the underlying http client.
@@ -66,6 +59,10 @@ impl ClientBuilder {
}
}
struct SocketAddrs {
iter: LookupIpIntoIter,
}
// n.b. static items do not call [`Drop`] on program termination, so this won't be deallocated.
// this is fine, as the OS can deallocate the terminated program faster than we can free memory
// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional.
@@ -75,17 +72,11 @@ static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
});
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
#[error("hickory-dns resolver error: {hickory_error}")]
/// Error occurring while resolving a hostname into an IP address.
pub enum ResolveError {
#[error("invalid name: {0}")]
InvalidNameError(String),
#[error("hickory-dns resolver error: {0}")]
ResolveError(#[from] hickory_resolver::ResolveError),
#[error("high level lookup timed out")]
Timeout,
#[error("hostname not found in static lookup table")]
StaticLookupMiss,
pub struct HickoryDnsError {
#[from]
hickory_error: ResolveError,
}
/// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait.
@@ -96,120 +87,71 @@ pub enum ResolveError {
/// The default initialization uses a shared underlying `AsyncResolver`. If a thread local resolver
/// is required use `thread_resolver()` to build a resolver with an independently instantiated
/// internal `AsyncResolver`.
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct HickoryDnsResolver {
// Since we might not have been called in the context of a
// Tokio Runtime in initialization, so we must delay the actual
// construction of the resolver.
state: Arc<OnceCell<TokioResolver>>,
fallback: Option<Arc<OnceCell<TokioResolver>>>,
static_base: Option<Arc<OnceCell<StaticResolver>>>,
dont_use_shared: bool,
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
/// use of retries, server_ordering_strategy, etc. ends absolutely if this timeout is reached.
overall_dns_timeout: Duration,
}
impl Default for HickoryDnsResolver {
fn default() -> Self {
Self {
state: Default::default(),
// Disable system resolver fallback by default - often blocked by firewalls in VPN environments
// Enable static fallback for known domains
fallback: None,
static_base: Some(Default::default()),
dont_use_shared: Default::default(),
overall_dns_timeout: Duration::from_secs(10),
}
}
}
impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let resolver = self.state.clone();
let maybe_fallback = self.fallback.clone();
let maybe_static = self.static_base.clone();
let independent = self.dont_use_shared;
let overall_dns_timeout = self.overall_dns_timeout;
Box::pin(async move {
resolve(
name,
resolver,
maybe_fallback,
maybe_static,
independent,
overall_dns_timeout,
)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
let resolver = resolver.get_or_try_init(|| {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if independent {
new_resolver()
} else {
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
}
})?;
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
let lookup = match resolver.lookup_ip(name.as_str()).await {
Ok(res) => res,
Err(e) => {
if let Some(ref fallback) = maybe_fallback {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
let resolver = fallback.get_or_try_init(|| {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if independent {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.as_ref()
.ok_or(e)? // if the shared resolver has no fallback return the original error
.get_or_try_init(new_resolver_system)?
.clone())
}
})?;
resolver.lookup_ip(name.as_str()).await?
} else {
return Err(e.into());
}
}
};
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
Ok(addrs)
})
}
}
async fn resolve(
name: Name,
resolver: Arc<OnceCell<TokioResolver>>,
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
independent: bool,
overall_dns_timeout: Duration,
) -> Result<Addrs, ResolveError> {
let resolver = resolver.get_or_try_init(|| HickoryDnsResolver::new_resolver(independent))?;
// Attempt a lookup using the primary resolver
let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
let primary_err = match resolve_fut.await {
Err(_) => ResolveError::Timeout,
Ok(Ok(lookup)) => {
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
return Ok(addrs);
}
Ok(Err(e)) => {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error: {e}");
}
e.into()
}
};
// If the primary resolver encountered an error, attempt a lookup using the fallback
// resolver if one is configured.
if let Some(ref fallback) = maybe_fallback {
let resolver =
fallback.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(independent))?;
let resolve_fut =
tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
if let Ok(Ok(lookup)) = resolve_fut.await {
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
return Ok(addrs);
}
}
// If no record has been found and a static map of fallback addresses is configured
// check the table for our entry
if let Some(ref static_resolver) = maybe_static {
debug!("checking static");
let resolver =
static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
if let Ok(addrs) = resolver.resolve(name).await {
return Ok(addrs);
}
}
Err(primary_err)
}
struct SocketAddrs {
iter: LookupIpIntoIter,
}
impl Iterator for SocketAddrs {
type Item = SocketAddr;
@@ -220,22 +162,28 @@ impl Iterator for SocketAddrs {
impl HickoryDnsResolver {
/// Attempt to resolve a domain name to a set of ['IpAddr']s
pub async fn resolve_str(
&self,
name: &str,
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
let n =
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
resolve(
n,
self.state.clone(),
self.fallback.clone(),
self.static_base.clone(),
self.dont_use_shared,
self.overall_dns_timeout,
)
.await
.map(|addrs| addrs.map(|socket_addr| socket_addr.ip()))
pub async fn resolve_str(&self, name: &str) -> Result<LookupIp, HickoryDnsError> {
let resolver = self.state.get_or_try_init(|| self.new_resolver())?;
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
let lookup = match resolver.lookup_ip(name).await {
Ok(res) => res,
Err(e) => {
if let Some(ref fallback) = self.fallback {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
let resolver = fallback.get_or_try_init(|| self.new_resolver_system())?;
resolver.lookup_ip(name).await?
} else {
return Err(e.into());
}
}
};
Ok(lookup)
}
/// Create a (lazy-initialized) resolver that is not shared across threads.
@@ -246,20 +194,16 @@ impl HickoryDnsResolver {
}
}
fn new_resolver(dont_use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if dont_use_shared {
fn new_resolver(&self) -> Result<TokioResolver, HickoryDnsError> {
if self.dont_use_shared {
new_resolver()
} else {
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
}
}
fn new_resolver_system(dont_use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
fn new_resolver_system(&self) -> Result<TokioResolver, HickoryDnsError> {
if self.dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
@@ -271,18 +215,8 @@ impl HickoryDnsResolver {
}
}
fn new_static_fallback(dont_use_shared: bool) -> StaticResolver {
if !dont_use_shared && let Some(ref shared_resolver) = SHARED_RESOLVER.static_base {
shared_resolver
.get_or_init(new_default_static_fallback)
.clone()
} else {
new_default_static_fallback()
}
}
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
pub fn enable_system_fallback(&mut self) -> Result<(), HickoryDnsError> {
self.fallback = Some(Default::default());
let _ = self
.fallback
@@ -297,52 +231,22 @@ impl HickoryDnsResolver {
pub fn disable_system_fallback(&mut self) {
self.fallback = None;
}
/// Get the current map of hostname to address in use by the fallback static lookup if one
/// exists.
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
Some(self.static_base.as_ref()?.get()?.get_addrs())
}
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
pub fn set_static_fallbacks(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
let cell = OnceCell::new();
cell.set(StaticResolver::new(addrs))
.expect("infallible assign");
self.static_base = Some(Arc::new(cell));
}
}
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
/// up for both IPv4 and IPv6 addresses to work with "happy eyeballs" algorithm.
///
/// Timeout Defaults to 5 seconds
/// Number of retries after lookup failure before giving up Defaults to 2
///
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
fn new_resolver() -> Result<TokioResolver, ResolveError> {
info!("building new configured resolver");
fn new_resolver() -> Result<TokioResolver, HickoryDnsError> {
let mut name_servers = NameServerConfigGroup::quad9_tls();
name_servers.merge(NameServerConfigGroup::quad9_https());
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
name_servers.merge(NameServerConfigGroup::cloudflare_https());
configure_and_build_resolver(name_servers)
}
fn configure_and_build_resolver(
name_servers: NameServerConfigGroup,
) -> Result<TokioResolver, ResolveError> {
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
let mut resolver_builder =
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
resolver_builder.options_mut().ip_strategy = get_ip_strategy();
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
resolver_builder.options_mut().server_ordering_strategy = ServerOrderingStrategy::RoundRobin;
// Cache successful responses for queries received by this resolver for 30 min minimum.
resolver_builder.options_mut().positive_min_ttl = Some(Duration::from_secs(1800));
Ok(resolver_builder.build())
}
@@ -350,54 +254,20 @@ fn configure_and_build_resolver(
/// Create a new resolver with the default configuration, which reads from the system DNS config
/// (i.e. `/etc/resolve.conf` in unix). The options are overridden to look up for both IPv4 and IPv6
/// addresses to work with "happy eyeballs" algorithm.
fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
fn new_resolver_system() -> Result<TokioResolver, HickoryDnsError> {
let mut resolver_builder = TokioResolver::builder_tokio()?;
resolver_builder.options_mut().ip_strategy = get_ip_strategy();
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
Ok(resolver_builder.build())
}
fn new_default_static_fallback() -> StaticResolver {
StaticResolver::new(constants::default_static_addrs())
}
/// Check if IPv6 stack is available for DNS resolution.
fn should_use_ipv6_dns() -> bool {
use std::net::UdpSocket;
match UdpSocket::bind("[::]:0") {
Ok(_) => {
debug!("IPv6 stack available - enabling dual-stack DNS");
true
}
Err(e) => {
debug!("IPv6 unavailable ({}), using IPv4-only DNS", e);
false
}
}
}
/// Get DNS lookup strategy based on IPv6 availability.
fn get_ip_strategy() -> LookupIpStrategy {
if should_use_ipv6_dns() {
debug!("Using dual-stack DNS (IPv4 + IPv6)");
LookupIpStrategy::Ipv4AndIpv6
} else {
debug!("Using IPv4-only DNS");
LookupIpStrategy::Ipv4Only
}
}
#[cfg(test)]
mod test {
use super::*;
use itertools::Itertools;
use std::collections::HashMap;
#[tokio::test]
async fn reqwest_with_custom_dns() {
let var_name = HickoryDnsResolver::default();
let resolver = var_name;
async fn reqwest_hickory_doh() {
let resolver = HickoryDnsResolver::default();
let client = reqwest::ClientBuilder::new()
.dns_resolver(resolver.into())
.build()
@@ -416,7 +286,7 @@ mod test {
}
#[tokio::test]
async fn dns_lookup() -> Result<(), ResolveError> {
async fn dns_lookup() -> Result<(), HickoryDnsError> {
let resolver = HickoryDnsResolver::default();
let domain = "ifconfig.me";
@@ -426,153 +296,4 @@ mod test {
Ok(())
}
#[tokio::test]
async fn static_resolver_as_fallback() -> Result<(), ResolveError> {
let example_domain = "non-existent.nymvpn.com";
let mut resolver = HickoryDnsResolver {
..Default::default()
};
let result = resolver.resolve_str(example_domain).await;
assert!(result.is_err()); // should be NXDomain
resolver.static_base = Some(Default::default());
let mut addr_map = HashMap::new();
let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
resolver.set_static_fallbacks(addr_map);
let mut addrs = resolver.resolve_str(example_domain).await?;
assert!(addrs.contains(&example_ip4));
assert!(addrs.contains(&example_ip6));
Ok(())
}
#[test]
fn default_resolver_fallback_config() {
let resolver = HickoryDnsResolver::default();
assert!(
resolver.fallback.is_none(),
"system fallback should be disabled by default for VPN environments"
);
assert!(
resolver.static_base.is_some(),
"static fallback should be enabled by default"
);
}
#[test]
fn ipv6_detection_returns_valid_strategy() {
let strategy = get_ip_strategy();
match strategy {
LookupIpStrategy::Ipv4Only | LookupIpStrategy::Ipv4AndIpv6 => {}
_ => panic!("Unexpected IP strategy returned: {:?}", strategy),
}
}
#[test]
fn ipv6_dns_detection_is_consistent() {
let first_result = should_use_ipv6_dns();
let second_result = should_use_ipv6_dns();
assert_eq!(first_result, second_result);
}
}
#[cfg(test)]
mod failure_test {
use super::*;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
/// IP addresses guaranteed to fail attempts to resolve
///
/// Addresses drawn from blocks set off by RFC5737 (ipv4) and RFC3849 (ipv6)
const GUARANTEED_BROKEN_IPS_1: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)),
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1111)),
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1001)),
];
// Create a resolver that behaves the same as the custom configured router, except for the fact
// that it is guaranteed to fail.
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
info!("building new faulty resolver");
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
GUARANTEED_BROKEN_IPS_1,
853,
"cloudflare-dns.com".to_string(),
true,
);
let broken_ns_https = NameServerConfigGroup::from_ips_https(
GUARANTEED_BROKEN_IPS_1,
443,
"cloudflare-dns.com".to_string(),
true,
);
broken_ns_group.merge(broken_ns_https);
configure_and_build_resolver(broken_ns_group)
}
#[tokio::test]
async fn dns_lookup_failures() -> Result<(), ResolveError> {
let time_start = std::time::Instant::now();
let r = OnceCell::new();
r.set(build_broken_resolver().expect("failed to build resolver"))
.expect("broken resolver init error");
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver {
dont_use_shared: true,
state: Arc::new(r),
overall_dns_timeout: Duration::from_secs(5),
..Default::default()
};
build_broken_resolver()?;
let domain = "ifconfig.me";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
let duration = time_start.elapsed();
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
Ok(())
}
#[tokio::test]
async fn fallback_to_static() -> Result<(), ResolveError> {
let r = OnceCell::new();
r.set(build_broken_resolver().expect("failed to build resolver"))
.expect("broken resolver init error");
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver {
dont_use_shared: true,
state: Arc::new(r),
static_base: Some(Default::default()),
overall_dns_timeout: Duration::from_secs(5),
..Default::default()
};
build_broken_resolver()?;
// successful lookup using fallback to static resolver
let domain = "nymvpn.com";
let _ = resolver
.resolve_str(domain)
.await
.expect("failed to resolve address in static lookup");
// unsuccessful lookup - primary times out, and not in
let domain = "non-existent.nymtech.net";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
Ok(())
}
}
@@ -1,73 +0,0 @@
#![allow(missing_docs)]
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
pub const NYM_API_DOMAIN: &str = "validator.nymtech.net";
pub const NYM_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(212, 71, 233, 232))];
pub const NYM_VPN_API_DOMAIN: &str = "nymvpn.com";
pub const NYM_VPN_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 21))];
pub const NYM_FRONTDOOR_VERCEL_DOMAIN: &str = "nym-frontdoor.vercel.app";
pub const NYM_FRONTDOOR_VERCEL_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(64, 29, 17, 195)),
IpAddr::V4(Ipv4Addr::new(216, 198, 79, 195)),
];
pub const NYM_FRONTDOOR_FASTLY_DOMAIN: &str = "nym-frontdoor.global.ssl.fastly.net";
pub const NYM_FRONTDOOR_FASTLY_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
];
pub const NYMVPN_FRONTDOOR_FASTLY_DOMAIN: &str = "nymvpn-frontdoor.global.ssl.fastly.net";
pub const NYMVPN_FRONTDOOR_FASTLY_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
];
pub const VERCEL_APP_DOMAIN: &str = "vercel.app";
pub const VERCEL_APP_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(64, 29, 17, 195)),
IpAddr::V4(Ipv4Addr::new(216, 198, 79, 195)),
];
pub const VERCEL_COM_DOMAIN: &str = "vercel.com";
pub const VERCEL_COM_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(198, 169, 2, 129)),
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
];
pub const NYM_COM_DOMAIN: &str = "nym.com";
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
pub const NYM_STATS_API_DOMAIN: &str = "nym-statistics-api.nymtech.cc";
pub const NYM_STATS_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(91, 92, 153, 96))];
pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
let mut m = HashMap::new();
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec());
m.insert(NYM_VPN_API_DOMAIN.to_string(), NYM_VPN_API_IPS.to_vec());
m.insert(
NYM_FRONTDOOR_VERCEL_DOMAIN.to_string(),
NYM_FRONTDOOR_VERCEL_IPS.to_vec(),
);
m.insert(
NYM_FRONTDOOR_FASTLY_DOMAIN.to_string(),
NYM_FRONTDOOR_FASTLY_IPS.to_vec(),
);
m.insert(
NYMVPN_FRONTDOOR_FASTLY_DOMAIN.to_string(),
NYMVPN_FRONTDOOR_FASTLY_IPS.to_vec(),
);
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
m
}
@@ -1,89 +0,0 @@
use crate::dns::ResolveError;
use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
sync::{Arc, Mutex},
};
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use tracing::*;
#[derive(Debug, Default, Clone)]
pub struct StaticResolver {
static_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
}
impl StaticResolver {
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
debug!("building static resolver");
Self {
static_addr_map: Arc::new(Mutex::new(static_entries)),
}
}
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
self.static_addr_map.lock().unwrap().clone()
}
}
impl Resolve for StaticResolver {
fn resolve(&self, name: Name) -> Resolving {
debug!("looking up {name:?} in static resolver");
let addr_map = self.static_addr_map.clone();
Box::pin(async move {
let addr_map = addr_map.lock().unwrap();
let lookup = match addr_map.get(name.as_str()) {
None => return Err(ResolveError::StaticLookupMiss.into()),
Some(addrs) => addrs,
};
let addrs: Addrs = Box::new(
lookup
.clone()
.into_iter()
.map(|ip_addr| SocketAddr::new(ip_addr, 0)),
);
Ok(addrs)
})
}
}
#[cfg(test)]
mod test {
use itertools::Itertools;
use super::*;
use std::error::Error as StdError;
use std::str::FromStr;
#[tokio::test]
async fn lookup_using_static_resolver() -> Result<(), Box<dyn StdError + Send + Sync>> {
let example_domain = String::from("static.nymvpn.com");
// lookup for domain for which there is no entry
let resolver = StaticResolver::new(HashMap::new());
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let result = resolver.resolve(url).await;
assert!(result.is_err());
match result {
Ok(_) => panic!("lookup with empty map should fail"),
Err(e) => assert_eq!(e.to_string(), ResolveError::StaticLookupMiss.to_string()),
}
// Successful lookup
let mut addr_map = HashMap::new();
let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let resolver = StaticResolver::new(addr_map);
let mut addrs = resolver.resolve(url).await?;
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
Ok(())
}
}
+21 -37
View File
@@ -179,7 +179,7 @@ mod dns;
mod path;
#[cfg(not(target_arch = "wasm32"))]
pub use dns::{HickoryDnsResolver, ResolveError};
pub use dns::{HickoryDnsError, HickoryDnsResolver};
// helper for generating user agent based on binary information
#[cfg(not(target_arch = "wasm32"))]
@@ -296,9 +296,6 @@ 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]
@@ -585,29 +582,24 @@ impl ClientBuilder {
Self::new(alt)
} else {
let url = url.to_url()?;
Self::new_with_urls(vec![url])
Ok(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().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()
let urls = network
.nym_api_urls
.as_ref()
.ok_or_else(|| {
HttpClientError::GenericRequestFailure(
"No API URLs configured in network details".to_string(),
)
})?
.iter()
.map(|api_url| {
// Convert ApiUrl to our Url type with fronting support
let mut url = Url::parse(&api_url.url)?;
@@ -619,19 +611,15 @@ impl ClientBuilder {
.iter()
.map(|host| format!("https://{}", host))
.collect();
url = Url::new(api_url.url.clone(), Some(fronts)).map_err(|source| {
HttpClientError::MalformedUrl {
raw: api_url.url.clone(),
source,
}
})?;
url = Url::new(api_url.url.clone(), Some(fronts))
.map_err(|e| HttpClientError::GenericRequestFailure(e.to_string()))?;
}
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")]
@@ -643,11 +631,7 @@ impl ClientBuilder {
}
/// Constructs a new http `ClientBuilder` from a valid url.
pub fn new_with_urls(urls: Vec<Url>) -> Result<Self, HttpClientError> {
if urls.is_empty() {
return Err(HttpClientError::NoUrlsProvided);
}
pub fn new_with_urls(urls: Vec<Url>) -> Self {
let urls = Self::check_urls(urls);
#[cfg(target_arch = "wasm32")]
@@ -656,7 +640,7 @@ impl ClientBuilder {
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client_builder = default_builder();
Ok(ClientBuilder {
ClientBuilder {
urls,
timeout: None,
custom_user_agent: false,
@@ -667,7 +651,7 @@ impl ClientBuilder {
retry_limit: 0,
serialization: SerializationFormat::Json,
})
}
}
/// Add an additional URL to the set usable by this constructed `Client`
@@ -964,13 +948,13 @@ impl Client {
return (url.as_str(), url.front_str());
} else {
tracing::debug!(
"Domain fronting is enabled, but no host_url is defined for current URL"
warn!(
"Domain fronting is enabled, but no host_url is defined! Domain fronting WILL NOT WORK"
)
}
} else {
tracing::debug!(
"Domain fronting is enabled, but current URL has no front_hosts configured"
warn!(
"Domain fronting is enabled, but no front_url is defined! Domain fronting WILL NOT WORK"
)
}
}
+1 -9
View File
@@ -21,10 +21,6 @@ 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
@@ -39,10 +35,6 @@ 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")]
@@ -55,7 +47,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 test_client.build() {
match b.try_clone().unwrap().build() {
Ok(client) => {
eprintln!("[HTTP-INVENTORY] ✓ Client built successfully");
eprintln!("[HTTP-INVENTORY] Client debug info: {:#?}", client);
+49 -110
View File
@@ -2,77 +2,77 @@ use super::*;
#[test]
fn sanitizing_urls() {
let base_url: Url = "http://api.test".parse().unwrap();
let base_url: Url = "http://foomp.com".parse().unwrap();
// works with a full string
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str()
);
// (and leading slash doesn't matter)
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str()
);
// works with 1 segment
assert_eq!(
"http://api.test/foo",
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str()
);
// works with 2 segments
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str()
);
// works with leading slash
assert_eq!(
"http://api.test/foo",
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str()
);
// works with trailing slash
assert_eq!(
"http://api.test/foo",
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str()
);
// works with both leading and trailing slash
assert_eq!(
"http://api.test/foo",
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str()
);
// adds params
assert_eq!(
"http://api.test/foo/bar?foomp=baz",
"http://foomp.com/foo/bar?foomp=baz",
sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str()
);
assert_eq!(
"http://api.test/foo/bar?arg1=val1&arg2=val2",
"http://foomp.com/foo/bar?arg1=val1&arg2=val2",
sanitize_url(
&base_url,
&["/foo/", "/bar/"],
@@ -91,87 +91,83 @@ 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.test".parse()?, // This will fail
"https://httpbin.org/status/200".parse()?, // This will succeed
])?
"http://broken.nym.badurl".parse()?,
"http://example.com/".parse()?,
])
.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?;
// 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"
);
assert_eq!(resp.status(), 200);
println!("Response status: {}", resp.status());
// check that the url was updated
assert_eq!(client.current_url().as_str(), "http://example.com/");
Ok(())
}
#[test]
fn host_updating() {
let url = Url::new("http://nym-api1.test", None).unwrap();
let url = Url::new("http://example.com", 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://nym-api1.test/");
assert_eq!(current_url.as_str(), "http://example.com/");
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://nym-api1.test/");
assert_eq!(client.current_url().as_str(), "http://example.com/");
// =======================================
// we rotate through urls when available
let new_urls = vec![
Url::new("http://nym-api1.test", None).unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
Url::new("http://example.com", None).unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
assert_eq!(client.current_url().as_str(), "http://example.com/");
client.update_host();
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
assert_eq!(client.current_url().as_str(), "http://example.org/");
assert_eq!(client.current_url().front_str(), None);
client.update_host();
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
assert_eq!(client.current_url().as_str(), "http://example.com/");
// =======================================
// we rotate through urls when available if fronting is disabled
let new_urls = vec![
Url::new(
"http://nym-api1.test",
Some(vec!["http://cdn1.test", "http://cdn2.test"]),
"http://example.com",
Some(vec!["http://front1.com", "http://front2.com"]),
)
.unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
assert_eq!(client.current_url().as_str(), "http://example.com/");
client.update_host();
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
assert_eq!(client.current_url().as_str(), "http://example.org/");
}
#[test]
#[cfg(feature = "tunneling")]
fn fronted_host_updating() {
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
let url = Url::new("http://example.com", Some(vec!["http://front1.com"])).unwrap();
let mut client = ClientBuilder::new(url)
.unwrap()
.with_fronting(crate::fronted::FrontPolicy::Always)
@@ -180,103 +176,46 @@ fn fronted_host_updating() {
// check that the url is set correctly
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// 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://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// =======================================
// we rotate through front urls when available if fronting is enabled
let new_urls = vec![
Url::new(
"http://nym-api.test",
Some(vec!["http://cdn1.test", "http://cdn2.test"]),
"http://example.com",
Some(vec!["http://front1.com", "http://front2.com"]),
)
.unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// 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://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn2.test"));
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front2.com"));
// 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://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());
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
}
+1 -13
View File
@@ -124,8 +124,6 @@ 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(
@@ -151,7 +149,7 @@ impl NymNetworkDetails {
})
.with_additional_validator_endpoint(ValidatorDetails::new(
var(var_names::NYXD).expect("nyxd validator not set"),
Some(nym_api.clone()),
Some(var(var_names::NYM_API).expect("nym api not set")),
get_optional_env(var_names::NYXD_WEBSOCKET),
))
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
@@ -161,10 +159,6 @@ 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 {
@@ -354,12 +348,6 @@ 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 create_sdk_shutdown_tracker() -> Result<ShutdownTracker, RegistryAccessError> {
Ok(runtime_registry::RuntimeRegistry::create_sdk()?.shutdown_tracker_owned())
pub fn get_sdk_shutdown_tracker() -> Result<ShutdownTracker, RegistryAccessError> {
Ok(runtime_registry::RuntimeRegistry::get_or_create_sdk()?.shutdown_tracker_owned())
}
+16 -34
View File
@@ -19,45 +19,30 @@ 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 {
/// Create a ShutdownManager for SDK use.
/// Get or create a ShutdownManager for SDK use.
/// This manager doesn't listen to OS signals, making it suitable for library use.
/// 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> {
pub(crate) fn get_or_create_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
let guard = REGISTRY
.sdk_manager
.read()
.map_err(|_| RegistryAccessError::Poisoned)?;
if let Some(manager) = guard.as_ref() {
Ok(manager.clone())
} else {
Err(RegistryAccessError::MissingShutdownManager)
return Ok(manager.clone());
}
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.
@@ -100,13 +85,10 @@ mod tests {
assert!(!RuntimeRegistry::has_sdk_manager().unwrap());
// Error if nothing was created
assert!(RuntimeRegistry::get_sdk().is_err());
let manager1 = RuntimeRegistry::create_sdk().unwrap();
let manager1 = RuntimeRegistry::get_or_create_sdk().unwrap();
assert!(RuntimeRegistry::has_sdk_manager().unwrap());
let manager2 = RuntimeRegistry::get_sdk().unwrap();
let manager2 = RuntimeRegistry::get_or_create_sdk().unwrap();
// Should return the same instance
assert!(Arc::ptr_eq(&manager1, &manager2));
+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())
}
@@ -15,7 +15,6 @@ 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 && \
@@ -27,7 +26,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
./quic_bridge_deployment.sh full_bridge_setup
./nym-node-setup/quic_bridge_deployment.sh full_bridge_setup
```
###### 3. Follow the interactive prompts
@@ -1 +1 @@
Thursday, October 30th 2025, 13:00:59 UTC
Tuesday, October 14th 2025, 11:34:14 UTC
@@ -11,7 +11,7 @@ options:
--no_routing_history Display node stats without routing history
--no_verloc_metrics Display node stats without verloc metrics
-m, --markdown Display results in markdown format
-o [OUTPUT], --output [OUTPUT]
-o, --output [OUTPUT]
Save results to file (in current dir or supply with
path without filename)
```
@@ -18,23 +18,23 @@
| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 |
| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 |
| [Incognet](https://incognet.io/kansas-city-dedicated-servers) | USA, Netherlands | Yes | nan | nan | 07/2025 |
| [Ionos](https://www.ionos.com/servers/amd-servers) | USA, DE, UK, ESP, FR | nan | No | nan | 07/2025 |
| [Ionos](https://www.ionos.com/servers/amd-servers) | US, DE, UK, ESP, FR | nan | No | nan | 07/2025 |
| [IsHosting](https://ishosting.com/en) | Brazil, Netherlands | Yes, based on ticket | Yes | Expensive | 05/2024 |
| [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) | USA, NL, DE, UK, CA, SG, JP, AUS, HK | nan | No | KYC mandatory | 07/2025 |
| [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) | US, NL, DE, UK, CA, SG, JP, AUS, HK | nan | No | KYC mandatory | 07/2025 |
| [Linode](https://linode.com) | USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy | Yes out of the box | No, only through [BitLAunch](https://bitlaunch.io) | IPv6 sometimes need to be re-added in Networking tab, no reboot needed | 05/2024 |
| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 |
| [Lowendbox](https://lowendbox.com/category/dedicated-servers) | | | | Just an aggregator with good offers | 07/2025 |
| [M247](https://m247.com/eu/services/host/dedicated-servers/) | UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands | Yes | No | nan | 07/2025 |
| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, USA, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 |
| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 |
| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 |
| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 |
| [NiceVPS](https://nicevps.net/) | Netherlands | Yes | nan | nan | 07/2025 |
| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 |
| [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) | USA, DE, FR, UK, PL, CA | | No | Not all locations always available | 07/2025 |
| [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) | PL, FR, NL, UA, USA, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR | Yes | No | nan | 07/2025 |
| [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) | PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR | Yes | No | nan | 07/2025 |
| [PrivateLayer](https://privatelayer.com) | Swiss | Yes | Yes | Slow customer response | 07/2025 |
| [Privex](https://www.privex.io/tor-exit-policy/) | USA, Germany, Sweden | Yes | Yes | nan | 07/2025 |
| [Psychz](https://www.psychz.net) | USA, UK, Brazil, Japan, Russia, South Africa and many more | Yes | nan | nan | 07/2025 |
| [Psychz](https://www.psychz.net) | US, UK, Brazil, Japan, Russia, South Africa and many more | Yes | nan | nan | 07/2025 |
| [RDP](https://rdp.sh) | Netherlands, USA, Poland | Yes, on by default | Yes | German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. | 07/2024 |
| [Servermania](https://www.servermania.com/dedicated-servers-hosting.htm) | USA, Canada | nan | No | nan | 07/2025 |
| [Svea](https://svea.net/vps) | Sweden | Yes | nan | nan | 07/2025 |
+5 -5
View File
@@ -21,11 +21,11 @@
[Lowendbox](https://lowendbox.com/category/dedicated-servers), , , ,Just an aggregator with good offers,07/2025
[Thundervm](https://thundervm.com/en/hosting/dedicated-server),"USA, UK, France, Italy, Switzerland, Netherlands",,Yes, ,07/2025
[OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/),"USA, DE, FR, UK, PL, CA", ,No,Not all locations always available,07/2025
[Mebilcom](https://www.melbicom.net/dedicatedserver/),"NL, USA, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL",,No,,07/2025
[Mebilcom](https://www.melbicom.net/dedicatedserver/),"NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL",,No,,07/2025
[Servermania](https://www.servermania.com/dedicated-servers-hosting.htm),"USA, Canada",,No,,07/2025
[Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6),"PL, FR, NL, UA, USA, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR",Yes,No,,07/2025
[Ionos](https://www.ionos.com/servers/amd-servers),"USA, DE, UK, ESP, FR",,No,,07/2025
[Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134),"USA, NL, DE, UK, CA, SG, JP, AUS, HK",,No,KYC mandatory,07/2025
[Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6),"PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR",Yes,No,,07/2025
[Ionos](https://www.ionos.com/servers/amd-servers),"US, DE, UK, ESP, FR",,No,,07/2025
[Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134),"US, NL, DE, UK, CA, SG, JP, AUS, HK",,No,KYC mandatory,07/2025
[M247](https://m247.com/eu/services/host/dedicated-servers/),"UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands",Yes,No,,07/2025
[Hostroyale](https://hostroyale.com/hosting/dedicated-server/),Various countries with different pricing,, Yes,,07/2025
[DataPacket](https://www.datapacket.com/pricing),"NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP",Yes,,,07/2025
@@ -35,7 +35,7 @@
[Colocall](https://www.colocall.net/),Ukraine,Yes,,,07/2025
[Incognet](https://incognet.io/kansas-city-dedicated-servers),"USA, Netherlands",Yes,,,07/2025
[FranTech](https://my.frantech.ca),USA,Yes,,,07/2025
[Psychz](https://www.psychz.net),"USA, UK, Brazil, Japan, Russia, South Africa and many more",Yes,,,07/2025
[Psychz](https://www.psychz.net),"US, UK, Brazil, Japan, Russia, South Africa and many more",Yes,,,07/2025
[Fsit](https://www.fsit.com/server/vps-vserver-kvm),Swiss,Yes,Yes,,07/2025
[NiceVPS](https://nicevps.net/),Netherlands,Yes,,,07/2025
[Dataclub](https://www.dataclub.eu/),"Latvia, Sweden, Netherlands",Yes,,,07/2027
1 **ISP** **Locations** **Public IPv6** **Crypto Payments** **Comments** **Last Updated**
21 [Lowendbox](https://lowendbox.com/category/dedicated-servers) Just an aggregator with good offers 07/2025
22 [Thundervm](https://thundervm.com/en/hosting/dedicated-server) USA, UK, France, Italy, Switzerland, Netherlands Yes 07/2025
23 [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) USA, DE, FR, UK, PL, CA No Not all locations always available 07/2025
24 [Mebilcom](https://www.melbicom.net/dedicatedserver/) NL, USA, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL No 07/2025
25 [Servermania](https://www.servermania.com/dedicated-servers-hosting.htm) USA, Canada No 07/2025
26 [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) PL, FR, NL, UA, USA, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR Yes No 07/2025
27 [Ionos](https://www.ionos.com/servers/amd-servers) USA, DE, UK, ESP, FR US, DE, UK, ESP, FR No 07/2025
28 [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) USA, NL, DE, UK, CA, SG, JP, AUS, HK US, NL, DE, UK, CA, SG, JP, AUS, HK No KYC mandatory 07/2025
29 [M247](https://m247.com/eu/services/host/dedicated-servers/) UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands Yes No 07/2025
30 [Hostroyale](https://hostroyale.com/hosting/dedicated-server/) Various countries with different pricing Yes 07/2025
31 [DataPacket](https://www.datapacket.com/pricing) NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP Yes 07/2025
35 [Colocall](https://www.colocall.net/) Ukraine Yes 07/2025
36 [Incognet](https://incognet.io/kansas-city-dedicated-servers) USA, Netherlands Yes 07/2025
37 [FranTech](https://my.frantech.ca) USA Yes 07/2025
38 [Psychz](https://www.psychz.net) USA, UK, Brazil, Japan, Russia, South Africa and many more US, UK, Brazil, Japan, Russia, South Africa and many more Yes 07/2025
39 [Fsit](https://www.fsit.com/server/vps-vserver-kvm) Swiss Yes Yes 07/2025
40 [NiceVPS](https://nicevps.net/) Netherlands Yes 07/2025
41 [Dataclub](https://www.dataclub.eu/) Latvia, Sweden, Netherlands Yes 07/2027
@@ -49,90 +49,6 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2025.19-kase`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.19-kase)
- [`nym-node`](nodes/nym-node.mdx) version `1.20.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-10-30T12:43:37.933354749Z
Build Version: 1.20.0
Commit SHA: 75a6d3426bd18dca600ad1cfa39b0a3c4f319c69
Commit Date: 2025-10-30T11:59:32.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.88.0
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
<Callout type="info" emoji="️">
**When this platform release becomes latest, we would like to ask operators ruuning any Gateway mode of `nym-node`, to use new version of [QUIC brige deployment tool](https://github.com/nymtech/nym/blob/develop/scripts/nym-node-setup/quic_bridge_deployment.sh)and install QUIC `nym-bridge` on their server, following [these steps](#quic-transport-bridge-deployment).**
</Callout>
Alongside this platform release we are happy to introduce several improvements and new tools for node operators.
- [Updated version of QUIC brige deployment tool](https://github.com/nymtech/nym/blob/develop/scripts/nym-node-setup/quic_bridge_deployment.sh), **if you run a `nym-node` in any Gateway mode, please install QUIC on your server, following [these steps](#quic-transport-bridge-deployment)**
- [New **Nym Node Status Dashboard**](https://node-status.nym.com)
- [New **Harbourmaster** aka ***Nym Node Status Observatory***](https://harbourmaster.nymtech.net)
### Features
- [Propagate cancel token to mixnet client](https://github.com/nymtech/nym/pull/6105): Ensures cancellation token propagation to mixnet client
- [[DOCs/operators] QUIC deployment script & docs](https://github.com/nymtech/nym/pull/6098): Script and documentation for QUIC deployment, referencing `nym-bridges` repository
- [Move gateway probe to monorepo (Rust edition 2024)](https://github.com/nymtech/nym/pull/6094): Moves `nym-gateway-probe` and related packages into monorepo, updates to Rust 2024 edition
- [Expose reference to Mnemonic from `DirectSecp256k1HdWallet`](https://github.com/nymtech/nym/pull/6083): Adds safer accessors for mnemonic references and deprecates unsafe cloning
### Bugfix
- [Cherry pick - request #6143 from nymtech/bugfix/mix-tx-closed-v2](https://github.com/nymtech/nym/pull/6153): Add circuit breaker
<AccordionTemplate name={<TestingSteps/>}>
**Summary:**
- Network-requester started successfully
- SOCKS5 client started successfully
- Traffic was proxied through the mixnet
- Shutdown was clean
- No 'channel closed (outside of shutdown!)' errors
</AccordionTemplate>
- [`nym-credential-proxy` query params parsing regression](https://github.com/nymtech/nym/pull/6121): Fix query deserialization issue with `serde_urlencoded` breaking compatibility with VPN API
- [Revert some dep updates introduced in #6043](https://github.com/nymtech/nym/pull/6120): Revert dependency updates that broke ANSI escape characters within tracing output
- [Skip IPv6 metadata endpoint request](https://github.com/nymtech/nym/pull/6118): Skip querying IPv4-only metadata endpoints during IPv6 probing tests
- [Revert "Propagate cancel token to mixnet client"](https://github.com/nymtech/nym/pull/6115): Reverts earlier change due to premature mixnet exit issues
- [Retrieve and update ticketbook in the same query](https://github.com/nymtech/nym/pull/6101): Fix concurrency issue with multiple agents retrieving ticketbooks simultaneously
- [Include network name in default gateway probe config path](https://github.com/nymtech/nym/pull/6100): Prevents reuse of credentials across different networks
- [Incompatibility fixes](https://github.com/nymtech/nym/pull/6099): Fixes several incompatibilities, including initialization and build mismatches
- [Testnet manager `02sql` migration](https://github.com/nymtech/nym/pull/6096): Fix invalid FK constraint blocking SQL migration
- [Use custom topology provider for list of init gateways](https://github.com/nymtech/nym/pull/6092): Fixes SDK bug where clients ignored custom topology provider on registration
- [Fix `WASM` client + build commands](https://github.com/nymtech/nym/pull/6043): Fixes WASM client hang and runtime time-related issues; improves internal dev testing stability
### Refactors & Maintenance
- [Update to no longer use 1mb files](https://github.com/nymtech/nym/pull/6117)
- [Restore pending DKG contract state migration](https://github.com/nymtech/nym/pull/6116)
- [Update `dirs` to `6.0`](https://github.com/nymtech/nym/pull/6109): Minor dependency update, safe for compatibility
## `v2025.18-jarlsberg`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.18-jarlsberg)
@@ -21,13 +21,13 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-10-30T12:43:37.933354749Z
Build Version: 1.20.0
Commit SHA: 75a6d3426bd18dca600ad1cfa39b0a3c4f319c69
Commit Date: 2025-10-30T11:59:32.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.88.0
rustc Channel: stable
Build Timestamp: 2025-10-15T09:04:32.043934599Z
Build Version: 1.19.0
Commit SHA: 2235a6e1477bea7368ee5443a298f544deb63504
Commit Date: 2025-10-15T10:22:16.000000000+02:00
Commit Branch: master
rustc Version: 1.92.0-nightly
rustc Channel: nightly
cargo Profile: release
```
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.69"
version = "1.1.67"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
+4 -13
View File
@@ -2,7 +2,7 @@ use nym_credentials_interface::TicketType;
use nym_sdk::mixnet::InputMessage;
#[derive(thiserror::Error, Debug)]
pub enum AuthenticationClientError {
pub enum Error {
#[error("mixnet client stopped returning responses")]
NoMixnetMessagesReceived,
@@ -42,19 +42,10 @@ pub enum AuthenticationClientError {
#[error("unknown authenticator version number")]
UnsupportedAuthenticatorVersion,
}
#[derive(thiserror::Error, Debug)]
pub enum RegistrationError {
#[error(transparent)]
NoCredentialSent(AuthenticationClientError), // This intentionnally doesn't use `from` to avoid random ? operator to land here when they shouldn't
#[error("an error occured after a credential was sent : {source}")]
CredentialSent {
#[source]
source: AuthenticationClientError,
},
#[error("failed to wait on AuthenticatorClientListener")]
FailedToJoinOnTask(#[from] tokio::task::JoinError),
}
// Result type based on our error type
pub(crate) type Result<T> = std::result::Result<T, AuthenticationClientError>;
pub type Result<T> = std::result::Result<T, Error>;
+36 -57
View File
@@ -6,11 +6,11 @@ use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND
use nym_crypto::asymmetric::x25519::KeyPair;
use nym_registration_common::GatewayData;
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, error, trace};
use crate::error::Result;
use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender};
use nym_authenticator_requests::{
AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse,
@@ -25,7 +25,7 @@ mod error;
mod helpers;
mod mixnet_listener;
pub use crate::error::{AuthenticationClientError, RegistrationError};
pub use crate::error::{Error, Result};
pub use crate::mixnet_listener::{AuthClientMixnetListener, AuthClientMixnetListenerHandle};
pub struct AuthenticatorClient {
@@ -91,7 +91,7 @@ impl AuthenticatorClient {
self.mixnet_sender
.send(input_message)
.await
.map_err(|e| AuthenticationClientError::SendMixnetMessage(Box::new(e)))?;
.map_err(|e| Error::SendMixnetMessage(Box::new(e)))?;
Ok(request_id)
}
@@ -104,11 +104,11 @@ impl AuthenticatorClient {
tokio::select! {
_ = &mut timeout => {
error!("Timed out waiting for reply to connect request");
return Err(AuthenticationClientError::TimeoutWaitingForConnectResponse);
return Err(Error::TimeoutWaitingForConnectResponse);
}
msg = self.mixnet_listener.recv() => match msg {
Err(_) => {
return Err(AuthenticationClientError::NoMixnetMessagesReceived);
return Err(Error::NoMixnetMessagesReceived);
}
Ok(msg) => {
let Some(header) = msg.message.first_chunk::<2>() else {
@@ -131,12 +131,12 @@ impl AuthenticatorClient {
// Then we deserialize the message
debug!("AuthClient: got message while waiting for connect response with version {version:?}");
let ret: Result<AuthenticatorResponse> = match version {
AuthenticatorVersion::V1 => Err(AuthenticationClientError::UnsupportedVersion),
AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion),
AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
AuthenticatorVersion::UNKNOWN => Err(AuthenticationClientError::UnknownVersion),
AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion),
};
let Ok(response) = ret else {
// This is ok, it's likely just one of our self-pings
@@ -158,14 +158,10 @@ impl AuthenticatorClient {
&mut self,
controller: &dyn BandwidthTicketProvider,
ticketbook_type: TicketType,
) -> std::result::Result<GatewayData, RegistrationError> {
) -> Result<GatewayData> {
debug!("Registering with the wg gateway...");
let init_message = match self.auth_version {
AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => {
return Err(RegistrationError::NoCredentialSent(
AuthenticationClientError::UnsupportedAuthenticatorVersion,
));
}
AuthenticatorVersion::V1 => return Err(Error::UnsupportedAuthenticatorVersion),
AuthenticatorVersion::V2 => {
ClientMessage::Initial(Box::new(v2::registration::InitMessage {
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
@@ -186,20 +182,16 @@ impl AuthenticatorClient {
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
}))
}
AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion),
};
trace!("sending init msg to {}: {:?}", &self.ip_addr, &init_message);
let response = self
.send_and_wait_for_response(&init_message)
.await
.map_err(RegistrationError::NoCredentialSent)?;
let response = self.send_and_wait_for_response(&init_message).await?;
let registered_data = match response {
AuthenticatorResponse::PendingRegistration(pending_registration_response) => {
// Unwrap since we have already checked that we have the keypair.
debug!("Verifying data");
if let Err(e) = pending_registration_response.verify(self.keypair.private_key()) {
return Err(RegistrationError::NoCredentialSent(
AuthenticationClientError::VerificationFailed(e),
));
return Err(Error::VerificationFailed(e));
}
trace!(
@@ -207,7 +199,6 @@ impl AuthenticatorClient {
&self.ip_addr, &pending_registration_response
);
// This call takes care of updating the credential count in storage, so failure of this must be counted as credential waste
let credential = Some(
controller
.get_ecash_ticket(
@@ -216,21 +207,15 @@ impl AuthenticatorClient {
DEFAULT_TICKETS_TO_SPEND,
)
.await
.map_err(|source| RegistrationError::CredentialSent {
source: AuthenticationClientError::GetTicket {
ticketbook_type,
source,
},
.map_err(|source| Error::GetTicket {
ticketbook_type,
source,
})?
.data,
);
let finalized_message = match self.auth_version {
AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => {
return Err(RegistrationError::CredentialSent {
source: AuthenticationClientError::UnsupportedAuthenticatorVersion,
});
}
AuthenticatorVersion::V1 => return Err(Error::UnsupportedAuthenticatorVersion),
AuthenticatorVersion::V2 => {
ClientMessage::Final(Box::new(v2::registration::FinalMessage {
gateway_client: v2::registration::GatewayClient::new(
@@ -275,29 +260,23 @@ impl AuthenticatorClient {
credential,
}))
}
AuthenticatorVersion::UNKNOWN => {
return Err(Error::UnsupportedAuthenticatorVersion);
}
};
trace!(
"sending final msg to {}: {:?}",
&self.ip_addr, &finalized_message
);
let response = self
.send_and_wait_for_response(&finalized_message)
.await
.map_err(|source| RegistrationError::CredentialSent { source })?;
let response = self.send_and_wait_for_response(&finalized_message).await?;
let AuthenticatorResponse::Registered(registered_response) = response else {
return Err(RegistrationError::CredentialSent {
source: AuthenticationClientError::InvalidGatewayAuthResponse,
});
return Err(Error::InvalidGatewayAuthResponse);
};
registered_response
}
AuthenticatorResponse::Registered(registered_response) => registered_response,
_ => {
return Err(RegistrationError::NoCredentialSent(
AuthenticationClientError::InvalidGatewayAuthResponse,
));
}
_ => return Err(Error::InvalidGatewayAuthResponse),
};
trace!(
@@ -307,7 +286,12 @@ impl AuthenticatorClient {
let gateway_data = GatewayData {
public_key: registered_data.pub_key().inner().into(),
endpoint: SocketAddr::new(self.ip_addr, registered_data.wg_port()),
endpoint: SocketAddr::from_str(&format!(
"{}:{}",
self.ip_addr,
registered_data.wg_port()
))
.map_err(Error::FailedToParseEntryGatewaySocketAddr)?,
private_ipv4: registered_data.private_ips().ipv4,
private_ipv6: registered_data.private_ips().ipv6,
};
@@ -315,12 +299,9 @@ impl AuthenticatorClient {
Ok(gateway_data)
}
// This is up to the caller to know nothing is ever spent there
pub async fn query_bandwidth(&mut self) -> Result<Option<i64>> {
let query_message = match self.auth_version {
AuthenticatorVersion::V1 => {
return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion);
}
AuthenticatorVersion::V1 => return Err(Error::UnsupportedAuthenticatorVersion),
AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
version: AuthenticatorVersion::V2,
@@ -337,9 +318,7 @@ impl AuthenticatorClient {
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
version: AuthenticatorVersion::V5,
})),
AuthenticatorVersion::UNKNOWN => {
return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion);
}
AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion),
};
let response = self.send_and_wait_for_response(&query_message).await?;
@@ -353,7 +332,7 @@ impl AuthenticatorClient {
return Ok(None);
}
}
_ => return Err(AuthenticationClientError::InvalidGatewayAuthResponse),
_ => return Err(Error::InvalidGatewayAuthResponse),
};
let remaining_pretty = if available_bandwidth > 1024 * 1024 {
@@ -368,13 +347,13 @@ impl AuthenticatorClient {
);
if available_bandwidth < 1024 * 1024 {
tracing::warn!(
"Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon"
);
"Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon
"
);
}
Ok(Some(available_bandwidth))
}
// Since the caller provides the credential, it knows it is spent
pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result<i64> {
let top_up_message = match self.auth_version {
AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage {
@@ -392,7 +371,7 @@ impl AuthenticatorClient {
credential,
})),
AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => {
return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion);
return Err(Error::UnsupportedAuthenticatorVersion);
}
};
let response = self.send_and_wait_for_response(&top_up_message).await?;
@@ -401,7 +380,7 @@ impl AuthenticatorClient {
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.available_bandwidth()
}
_ => return Err(AuthenticationClientError::InvalidGatewayAuthResponse),
_ => return Err(Error::InvalidGatewayAuthResponse),
};
Ok(remaining_bandwidth)
@@ -50,7 +50,7 @@ impl AuthClientMixnetListener {
}
}
async fn run(mut self) {
async fn run(mut self) -> Self {
let mixnet_cancel_token = self.mixnet_client.cancellation_token();
self.shutdown_token.run_until_cancelled(async {
loop {
@@ -95,8 +95,12 @@ impl AuthClientMixnetListener {
tracing::debug!("AuthClientMixnetListener is shutting down");
}).await;
tracing::debug!("AuthClientMixnetListener: Disconnect mixnet client");
if !mixnet_cancel_token.is_cancelled() {
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() {
self.mixnet_client.disconnect().await;
}
}
@@ -124,7 +128,7 @@ pub struct AuthClientMixnetListenerHandle {
message_sender: MixnetMessageInputSender,
cancellation_token: CancellationToken,
mixnet_cancellation_token: CancellationToken,
handle: JoinHandle<()>,
handle: JoinHandle<AuthClientMixnetListener>,
}
impl AuthClientMixnetListenerHandle {
@@ -144,8 +148,13 @@ 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();
if let Err(e) = self.handle.await {
tracing::error!("Error waiting for auth clients mixnet listener to stop: {e}")
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}");
}
}
}
}
+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);
let mut ipr_client = IprClientConnect::new(mixnet_client, cancel_token).await;
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 fn new(mixnet_client: MixnetClient, cancel_token: CancellationToken) -> Self {
pub async fn new(mixnet_client: MixnetClient, cancel_token: CancellationToken) -> Self {
Self {
mixnet_client,
connected: ConnectionState::Disconnected,
@@ -1,17 +1,21 @@
# this will only work with VPN, otherwise remove the harbor part
FROM harbor.nymte.ch/dockerhub/rust:latest AS builder
ARG GIT_REF=main
RUN apt update && apt install -yy libdbus-1-dev pkg-config libclang-dev
# Install go
RUN wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz -O go.tar.gz
RUN tar -xzvf go.tar.gz -C /usr/local
ENV PATH=/go/bin:/usr/local/go/bin:$PATH
COPY ./ /usr/src/nym
WORKDIR /usr/src/nym
RUN git clone https://github.com/nymtech/nym-vpn-client /usr/src/nym-vpn-client
RUN cd /usr/src/nym-vpn-client && git checkout $GIT_REF
ENV PATH=/go/bin:/usr/local/go/bin:$PATH
WORKDIR /usr/src/nym-vpn-client/nym-vpn-core
RUN cargo build --release --package nym-gateway-probe
COPY ./ /usr/src/nym
WORKDIR /usr/src/nym/nym-node-status-api/nym-node-status-agent
RUN cargo build --release
@@ -31,7 +35,7 @@ RUN apt-get update && apt-get install -y ca-certificates
WORKDIR /nym
COPY --from=builder /usr/src/nym/target/release/nym-node-status-agent ./
COPY --from=builder /usr/src/nym/target/release/nym-gateway-probe ./
COPY --from=builder /usr/src/nym-vpn-client/nym-vpn-core/target/release/nym-gateway-probe ./
COPY --from=builder /usr/src/nym/nym-node-status-api/nym-node-status-agent/entrypoint.sh ./
RUN chmod +x /nym/entrypoint.sh
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n node_id,\n ed25519_identity_pubkey,\n total_stake,\n ip_addresses as \"ip_addresses!: serde_json::Value\",\n mix_port,\n x25519_sphinx_pubkey,\n node_role as \"node_role: serde_json::Value\",\n supported_roles as \"supported_roles: serde_json::Value\",\n entry as \"entry: serde_json::Value\",\n performance,\n self_described as \"self_described: serde_json::Value\",\n bond_info as \"bond_info: serde_json::Value\",\n http_api_port\n FROM\n nym_nodes\n ORDER BY\n node_id\n ",
"query": "SELECT\n node_id,\n ed25519_identity_pubkey,\n total_stake,\n ip_addresses as \"ip_addresses!: serde_json::Value\",\n mix_port,\n x25519_sphinx_pubkey,\n node_role as \"node_role: serde_json::Value\",\n supported_roles as \"supported_roles: serde_json::Value\",\n entry as \"entry: serde_json::Value\",\n performance,\n self_described as \"self_described: serde_json::Value\",\n bond_info as \"bond_info: serde_json::Value\"\n FROM\n nym_nodes\n WHERE\n self_described IS NOT NULL\n AND\n bond_info IS NOT NULL\n ",
"describe": {
"columns": [
{
@@ -62,11 +62,6 @@
"ordinal": 11,
"name": "bond_info: serde_json::Value",
"type_info": "Jsonb"
},
{
"ordinal": 12,
"name": "http_api_port",
"type_info": "Int4"
}
],
"parameters": {
@@ -84,9 +79,8 @@
true,
false,
true,
true,
true
]
},
"hash": "3ddc12cc4e1796b787a50c40560d2bd71d1cfe5f5265e6f161b3122d1317a421"
"hash": "283f49a65c7d70bf271702ff6a5c7ad6e68c81932d295ff18ed198c54706a57c"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO nym_nodes\n (node_id, ed25519_identity_pubkey,\n total_stake,\n ip_addresses, mix_port,\n x25519_sphinx_pubkey, node_role,\n supported_roles, entry,\n self_described,\n bond_info,\n performance, last_updated_utc\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)\n ON CONFLICT(node_id) DO UPDATE SET\n ed25519_identity_pubkey=excluded.ed25519_identity_pubkey,\n ip_addresses=excluded.ip_addresses,\n mix_port=excluded.mix_port,\n x25519_sphinx_pubkey=excluded.x25519_sphinx_pubkey,\n node_role=excluded.node_role,\n supported_roles=excluded.supported_roles,\n entry=excluded.entry,\n self_described=excluded.self_described,\n bond_info=excluded.bond_info,\n performance=excluded.performance,\n last_updated_utc=excluded.last_updated_utc\n ;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Varchar",
"Int8",
"Jsonb",
"Int4",
"Varchar",
"Jsonb",
"Jsonb",
"Jsonb",
"Jsonb",
"Jsonb",
"Varchar",
"Int4"
]
},
"nullable": []
},
"hash": "b010fb91828f7e4f0b72bdfe3b58b2abb437cccdb6ebd2e1087cc822ed737b0e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n node_id,\n ed25519_identity_pubkey,\n total_stake,\n ip_addresses as \"ip_addresses!: serde_json::Value\",\n mix_port,\n x25519_sphinx_pubkey,\n node_role as \"node_role: serde_json::Value\",\n supported_roles as \"supported_roles: serde_json::Value\",\n entry as \"entry: serde_json::Value\",\n performance,\n self_described as \"self_described: serde_json::Value\",\n bond_info as \"bond_info: serde_json::Value\",\n http_api_port\n FROM\n nym_nodes\n WHERE\n self_described IS NOT NULL\n AND\n bond_info IS NOT NULL\n ",
"query": "SELECT\n node_id,\n ed25519_identity_pubkey,\n total_stake,\n ip_addresses as \"ip_addresses!: serde_json::Value\",\n mix_port,\n x25519_sphinx_pubkey,\n node_role as \"node_role: serde_json::Value\",\n supported_roles as \"supported_roles: serde_json::Value\",\n entry as \"entry: serde_json::Value\",\n performance,\n self_described as \"self_described: serde_json::Value\",\n bond_info as \"bond_info: serde_json::Value\"\n FROM\n nym_nodes\n ORDER BY\n node_id\n ",
"describe": {
"columns": [
{
@@ -62,11 +62,6 @@
"ordinal": 11,
"name": "bond_info: serde_json::Value",
"type_info": "Jsonb"
},
{
"ordinal": 12,
"name": "http_api_port",
"type_info": "Int4"
}
],
"parameters": {
@@ -84,9 +79,8 @@
true,
false,
true,
true,
true
]
},
"hash": "0b51df277ed66c6553f66af9b135342dee177abc1c92e4a89147de3c22d3d1a5"
"hash": "c48d04fc3de59dd484f0a63d40336ced54e08785f77e9ef85f3157d004ec85dc"
}
@@ -1,27 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO nym_nodes\n (node_id, ed25519_identity_pubkey,\n total_stake,\n ip_addresses, mix_port,\n x25519_sphinx_pubkey, node_role,\n supported_roles, entry,\n self_described,\n bond_info,\n performance, last_updated_utc, http_api_port\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ON CONFLICT(node_id) DO UPDATE SET\n ed25519_identity_pubkey=excluded.ed25519_identity_pubkey,\n ip_addresses=excluded.ip_addresses,\n mix_port=excluded.mix_port,\n x25519_sphinx_pubkey=excluded.x25519_sphinx_pubkey,\n node_role=excluded.node_role,\n supported_roles=excluded.supported_roles,\n entry=excluded.entry,\n self_described=excluded.self_described,\n bond_info=excluded.bond_info,\n performance=excluded.performance,\n last_updated_utc=excluded.last_updated_utc,\n http_api_port=excluded.http_api_port\n ;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Varchar",
"Int8",
"Jsonb",
"Int4",
"Varchar",
"Jsonb",
"Jsonb",
"Jsonb",
"Jsonb",
"Jsonb",
"Varchar",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "dde9aff827c34086077927bbe33fa3d5c939e7122ba7c88b78a353f00b271ec2"
}
@@ -3,7 +3,7 @@
[package]
name = "nym-node-status-api"
version = "4.0.11-rc1"
version = "4.0.10"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -1,2 +0,0 @@
ALTER TABLE nym_nodes
ADD COLUMN IF NOT EXISTS http_api_port INTEGER;
@@ -1,5 +1,5 @@
use crate::ticketbook_manager::TicketbookManagerConfig;
use clap::{Parser, Subcommand};
use clap::Parser;
use nym_bin_common::bin_info;
use nym_credential_proxy_lib::shared_state::ecash_state::TicketType;
use reqwest::Url;
@@ -105,19 +105,6 @@ pub(crate) struct Cli {
#[clap(flatten)]
pub(crate) ticketbook: TicketbookArgs,
#[command(subcommand)]
pub(crate) command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
pub(crate) enum Commands {
/// Scrape a single node and output detailed debug logs
ScrapeNode {
/// The id of the node to scrape
#[arg(long)]
node_id: i64,
},
}
#[derive(Debug, Parser)]
@@ -381,7 +381,7 @@ impl ScrapeNodeKind {
pub(crate) struct ScraperNodeInfo {
pub node_kind: ScrapeNodeKind,
pub hosts: Vec<String>,
pub http_api_port: Option<u16>,
pub http_api_port: i64,
}
impl ScraperNodeInfo {
@@ -395,21 +395,8 @@ impl ScraperNodeInfo {
format!("http://{}", host),
]);
if let Some(custom_http_api_port) = self.http_api_port {
urls = Vec::new();
for host in &self.hosts {
urls.append(&mut vec![format!(
"http://{}:{}",
host, custom_http_api_port
)]);
}
// do not fall back to default ports, if the operator sets a custom http api port
// in their bond, use it and error out if it's not available
// this will correctly handle cases where some operators run multiple nodes
// on a single IP address and assign different custom http port apis at bond time
// urls.insert(0, format!("http://{}:{}", host, custom_http_api_port));
if self.http_api_port != DEFAULT_NYM_NODE_HTTP_PORT as i64 {
urls.insert(0, format!("http://{}:{}", host, self.http_api_port));
}
}
@@ -436,7 +423,6 @@ pub(crate) struct NymNodeDto {
pub performance: String,
pub self_described: Option<serde_json::Value>,
pub bond_info: Option<serde_json::Value>,
pub http_api_port: Option<i32>,
}
#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros
@@ -454,7 +440,6 @@ pub(crate) struct NymNodeInsertRecord {
pub entry: Option<serde_json::Value>,
pub self_described: Option<serde_json::Value>,
pub bond_info: Option<serde_json::Value>,
pub http_api_port: Option<i32>,
pub last_updated_utc: i64,
}
@@ -471,12 +456,6 @@ impl NymNodeInsertRecord {
.map(|info| decimal_to_i64(info.total_stake()))
.unwrap_or(0);
let entry = serialize_opt_to_value!(skimmed_node.entry)?;
let http_api_port = bond_info.and_then(|bond| {
bond.bond_information
.node
.custom_http_port
.map(|port| port as i32)
});
let bond_info = serialize_opt_to_value!(bond_info)?;
let self_described = serialize_opt_to_value!(self_described)?;
@@ -493,7 +472,6 @@ impl NymNodeInsertRecord {
entry,
self_described,
bond_info,
http_api_port,
last_updated_utc: now,
};
@@ -35,8 +35,7 @@ pub(crate) async fn get_all_nym_nodes(pool: &DbPool) -> anyhow::Result<Vec<NymNo
entry as "entry: serde_json::Value",
performance,
self_described as "self_described: serde_json::Value",
bond_info as "bond_info: serde_json::Value",
http_api_port
bond_info as "bond_info: serde_json::Value"
FROM
nym_nodes
ORDER BY
@@ -73,8 +72,7 @@ pub(crate) async fn get_described_bonded_nym_nodes(
entry as "entry: serde_json::Value",
performance,
self_described as "self_described: serde_json::Value",
bond_info as "bond_info: serde_json::Value",
http_api_port
bond_info as "bond_info: serde_json::Value"
FROM
nym_nodes
WHERE
@@ -117,9 +115,9 @@ pub(crate) async fn update_nym_nodes(
supported_roles, entry,
self_described,
bond_info,
performance, last_updated_utc, http_api_port
performance, last_updated_utc
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT(node_id) DO UPDATE SET
ed25519_identity_pubkey=excluded.ed25519_identity_pubkey,
ip_addresses=excluded.ip_addresses,
@@ -131,8 +129,7 @@ pub(crate) async fn update_nym_nodes(
self_described=excluded.self_described,
bond_info=excluded.bond_info,
performance=excluded.performance,
last_updated_utc=excluded.last_updated_utc,
http_api_port=excluded.http_api_port
last_updated_utc=excluded.last_updated_utc
;",
record.node_id,
record.ed25519_identity_pubkey,
@@ -147,7 +144,6 @@ pub(crate) async fn update_nym_nodes(
record.bond_info,
record.performance,
record.last_updated_utc as i32,
record.http_api_port,
)
.execute(&mut *tx)
.await
@@ -21,11 +21,10 @@ pub(crate) async fn get_nodes_for_scraping(pool: &DbPool) -> Result<Vec<ScraperN
let skimmed_nodes = queries::get_described_bonded_nym_nodes(pool)
.await
.map(|nodes_dto| {
nodes_dto.into_iter().filter_map(|node_dto| {
let node_id = node_dto.node_id;
let http_api_port = node_dto.http_api_port;
match SkimmedNode::try_from(node_dto) {
Ok(node) => Some((node, http_api_port)),
nodes_dto.into_iter().filter_map(|node| {
let node_id = node.node_id;
match SkimmedNode::try_from(node) {
Ok(node) => Some(node),
Err(e) => {
tracing::error!("Failed to decode node_id={}: {}", node_id, e);
None
@@ -34,7 +33,7 @@ pub(crate) async fn get_nodes_for_scraping(pool: &DbPool) -> Result<Vec<ScraperN
})
})?;
skimmed_nodes.for_each(|(node, http_api_port)| {
skimmed_nodes.for_each(|node| {
// TODO: relies on polyfilling: Nym nodes table might contain legacy mixnodes
// as well. Categorize them here.
let node_kind = if gateway_keys.contains(&node.ed25519_identity_pubkey.to_base58_string()) {
@@ -55,7 +54,7 @@ pub(crate) async fn get_nodes_for_scraping(pool: &DbPool) -> Result<Vec<ScraperN
.into_iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>(),
http_api_port: http_api_port.map(|port| port as u16),
http_api_port: node.mix_port.into(),
})
});
@@ -138,7 +138,6 @@ mod db_tests {
performance: "1.0".to_string(),
self_described: None,
bond_info: None,
http_api_port: None,
};
let skimmed_node: nym_validator_client::nym_api::SkimmedNode =
@@ -363,42 +362,22 @@ fn test_scraper_node_info_contact_addresses() {
let node_info = ScraperNodeInfo {
node_kind: ScrapeNodeKind::MixingNymNode { node_id: 123 },
hosts: vec!["1.1.1.1".to_string(), "example.com".to_string()],
http_api_port: None,
http_api_port: 8080,
};
let addresses = node_info.contact_addresses();
// Should generate multiple URLs for each host
// When no custom port is specified only default ports should be used
// Custom port (8080) should be inserted at the beginning
assert!(addresses.contains(&"http://1.1.1.1:8080".to_string()));
assert!(addresses.contains(&"http://example.com:8080".to_string()));
assert!(addresses.contains(&"http://1.1.1.1:8000".to_string()));
assert!(addresses.contains(&"https://1.1.1.1".to_string()));
assert!(addresses.contains(&"http://1.1.1.1".to_string()));
assert!(addresses.contains(&"http://example.com:8000".to_string()));
// Check that URLs follow the expected pattern
assert!(addresses.len() >= 8); // At least 4 URLs per host
}
#[test]
fn test_scraper_node_info_contact_addresses_with_custom_http_api_port() {
use crate::db::models::{ScrapeNodeKind, ScraperNodeInfo};
let node_info = ScraperNodeInfo {
node_kind: ScrapeNodeKind::MixingNymNode { node_id: 123 },
hosts: vec!["1.1.1.1".to_string(), "example.com".to_string()],
http_api_port: Some(4444),
};
let addresses = node_info.contact_addresses();
// Should generate multiple URLs for each host
// Custom port (4444) should be the only port in the list
assert!(addresses.contains(&"http://1.1.1.1:4444".to_string()));
assert!(addresses.contains(&"http://example.com:4444".to_string()));
// Check that URLs follow the expected pattern
assert!(addresses.len() >= 2); // At least 4 URLs per host
}
#[test]
fn test_scrape_node_kind_node_id() {
use crate::db::models::ScrapeNodeKind;
@@ -435,7 +414,6 @@ fn test_nym_node_dto_with_invalid_keys() {
performance: "1.0".to_string(),
self_described: None,
bond_info: None,
http_api_port: None,
};
let result: Result<nym_validator_client::nym_api::SkimmedNode, _> = nym_node_dto.try_into();
@@ -473,7 +451,6 @@ fn test_nym_node_dto_with_invalid_performance() {
performance: "invalid_percent".to_string(),
self_described: None,
bond_info: None,
http_api_port: None,
};
let result: Result<nym_validator_client::nym_api::SkimmedNode, _> = nym_node_dto.try_into();
@@ -1,6 +1,4 @@
use crate::cli::Commands;
use crate::monitor::DelegationsCache;
use crate::node_scraper::helpers::scrape_and_store_description_by_node_id;
use crate::ticketbook_manager::TicketbookManager;
use crate::ticketbook_manager::state::TicketbookManagerState;
use clap::Parser;
@@ -42,49 +40,11 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Registered {} agent keys", agent_key_list.len());
let connection_url = args.database_url.clone();
if std::env::var("SHOW_CONFIG").ok().is_some() {
tracing::debug!("Using config:\n{:#?}", args);
}
tracing::debug!("Using config:\n{:#?}", args);
let storage = db::Storage::init(connection_url, args.sqlx_busy_timeout_s).await?;
let db_pool = storage.pool_owned();
// node geocache is shared between node monitor and HTTP server
let geocache = moka::future::Cache::builder()
.time_to_live(args.geodata_ttl)
.build();
let delegations_cache = DelegationsCache::new();
let client_config = nym_validator_client::nyxd::Config::try_from_nym_network_details(
&nym_network_defaults::NymNetworkDetails::new_from_env(),
)?;
let nyxd_client = NyxdClient::connect(client_config.clone(), args.nyxd_addr.as_str())
.map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?;
match args.command {
Some(Commands::ScrapeNode { node_id }) => {
if std::env::var("RUN_ONCE_INIT_NODES").ok().is_some() {
let geocache_clone = geocache.clone();
let delegations_cache_clone = Arc::clone(&delegations_cache);
monitor::run_once(
db_pool.clone(),
args.nym_api_client_timeout,
nyxd_client,
args.ipinfo_api_token,
geocache_clone,
delegations_cache_clone,
)
.await?;
}
tracing::info!("Scraping node with id {node_id}...");
scrape_and_store_description_by_node_id(&db_pool, node_id).await?;
return Ok(());
}
None => {
// default behaviour
}
}
// Start the node scraper
let scraper = node_scraper::DescriptionScraper::new(storage.pool_owned());
shutdown_manager.spawn_with_shutdown(async move {
@@ -98,9 +58,20 @@ async fn main() -> anyhow::Result<()> {
scraper.start().await;
});
// node geocache is shared between node monitor and HTTP server
let geocache = moka::future::Cache::builder()
.time_to_live(args.geodata_ttl)
.build();
let delegations_cache = DelegationsCache::new();
// Start the monitor
let geocache_clone = geocache.clone();
let delegations_cache_clone = Arc::clone(&delegations_cache);
let client_config = nym_validator_client::nyxd::Config::try_from_nym_network_details(
&nym_network_defaults::NymNetworkDetails::new_from_env(),
)?;
let nyxd_client = NyxdClient::connect(client_config.clone(), args.nyxd_addr.as_str())
.map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?;
shutdown_manager.spawn_with_shutdown(async move {
monitor::run_in_background(
@@ -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()?;
@@ -68,7 +68,7 @@ pub(crate) async fn run_in_background(
loop {
tracing::info!("Refreshing node info...");
if let Err(e) = monitor.run(false).await {
if let Err(e) = monitor.run().await {
tracing::error!(
"Monitor run failed: {e}, retrying in {}s...",
MONITOR_FAILURE_RETRY_DELAY.as_secs()
@@ -84,33 +84,8 @@ pub(crate) async fn run_in_background(
}
}
#[instrument(level = "debug", name = "data_monitor", skip_all)]
pub(crate) async fn run_once(
db_pool: DbPool,
nym_api_client_timeout: Duration,
nyxd_client: nym_validator_client::QueryHttpRpcNyxdClient,
ipinfo_api_token: String,
geocache: NodeGeoCache,
node_delegations: Arc<RwLock<DelegationsCache>>,
) -> anyhow::Result<()> {
let ipinfo = IpInfoClient::new(ipinfo_api_token.clone());
let mut monitor = Monitor {
db_pool,
network_details: nym_network_defaults::NymNetworkDetails::new_from_env(),
nym_api_client_timeout,
nyxd_client,
ipinfo,
geocache,
node_delegations,
};
tracing::info!("Refreshing node info...");
monitor.run(true).await
}
impl Monitor {
async fn run(&mut self, exit_early: bool) -> anyhow::Result<()> {
async fn run(&mut self) -> anyhow::Result<()> {
self.check_ipinfo_bandwidth().await;
let default_api_url = self
@@ -123,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()?;
@@ -178,11 +153,6 @@ impl Monitor {
tracing::debug!("{} nym nodes written to DB!", inserted);
})?;
// stop here if running once
if exit_early {
return Ok(());
}
// refresh geodata for all nodes
for node_description in described_nodes.values() {
self.location_cached(node_description).await;
@@ -118,17 +118,6 @@ pub fn sanitize_description(
}
}
pub async fn scrape_and_store_description_by_node_id(pool: &DbPool, node_id: i64) -> Result<()> {
let nodes = crate::db::queries::get_nodes_for_scraping(pool).await?;
match nodes.iter().find(|n| *n.node_kind.node_id() == node_id) {
Some(node) => Ok(scrape_and_store_description(pool, node.clone()).await?),
None => {
error!("Could not find node with id {node_id}");
Err(anyhow!("Could not find node with id {node_id}"))
}
}
}
pub async fn scrape_and_store_description(pool: &DbPool, node: ScraperNodeInfo) -> Result<()> {
let client = build_client()?;
let urls = node.contact_addresses();
@@ -163,13 +152,7 @@ pub async fn scrape_and_store_description(pool: &DbPool, node: ScraperNodeInfo)
anyhow::anyhow!("Failed to fetch description from any URL: {}", err_msg)
})?;
let sanitized_description = sanitize_description(description.clone(), *node.node_id());
trace!("tried_url_list = {tried_url_list:?}");
trace!("ndoe_id = {}", node.node_id());
trace!("description = {:?}", description);
trace!("sanitized_description = {:?}", sanitized_description);
let sanitized_description = sanitize_description(description, *node.node_id());
insert_scraped_node_description(pool, &node.node_kind, &sanitized_description).await?;
Ok(())
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-node"
version = "1.21.0"
version = "1.19.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
-1
View File
@@ -17,7 +17,6 @@ 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" }
+235 -5
View File
@@ -15,12 +15,10 @@ 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 {
@@ -28,14 +26,11 @@ 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>,
@@ -61,6 +56,53 @@ pub struct MixnetClientConfig {
}
impl BuilderConfig {
/// Creates a new BuilderConfig with all required parameters.
///
/// However, consider using `BuilderConfig::builder()` instead.
#[allow(clippy::too_many_arguments)]
pub fn new(
entry_node: NymNodeWithKeys,
exit_node: NymNodeWithKeys,
data_path: Option<PathBuf>,
mixnet_client_config: MixnetClientConfig,
two_hops: bool,
user_agent: UserAgent,
custom_topology_provider: Box<dyn TopologyProvider + Send + Sync>,
network_env: NymNetworkDetails,
cancel_token: CancellationToken,
#[cfg(unix)] connection_fd_callback: Arc<dyn Fn(RawFd) + Send + Sync>,
) -> Self {
Self {
entry_node,
exit_node,
data_path,
mixnet_client_config,
two_hops,
user_agent,
custom_topology_provider,
network_env,
cancel_token,
#[cfg(unix)]
connection_fd_callback,
}
}
/// Creates a builder for BuilderConfig
///
/// This is the preferred way to construct a BuilderConfig.
///
/// # Example
/// ```ignore
/// let config = BuilderConfig::builder()
/// .entry_node(entry)
/// .exit_node(exit)
/// .user_agent(agent)
/// .build()?;
/// ```
pub fn builder() -> BuilderConfigBuilder {
BuilderConfigBuilder::default()
}
pub fn mixnet_client_debug_config(&self) -> DebugConfig {
if self.two_hops {
two_hop_debug_config(&self.mixnet_client_config)
@@ -212,6 +254,144 @@ fn true_to_disabled(val: bool) -> &'static str {
if val { "disabled" } else { "enabled" }
}
/// Error type for BuilderConfig validation
#[derive(Debug, Clone, thiserror::Error)]
#[allow(clippy::enum_variant_names)]
pub enum BuilderConfigError {
#[error("entry_node is required")]
MissingEntryNode,
#[error("exit_node is required")]
MissingExitNode,
#[error("mixnet_client_config is required")]
MissingMixnetClientConfig,
#[error("user_agent is required")]
MissingUserAgent,
#[error("custom_topology_provider is required")]
MissingTopologyProvider,
#[error("network_env is required")]
MissingNetworkEnv,
#[error("cancel_token is required")]
MissingCancelToken,
#[cfg(unix)]
#[error("connection_fd_callback is required")]
MissingConnectionFdCallback,
}
/// Builder for `BuilderConfig`
///
/// This provides a more convenient way to construct a `BuilderConfig` compared to the
/// `new()` constructor with many arguments.
#[derive(Default)]
pub struct BuilderConfigBuilder {
entry_node: Option<NymNodeWithKeys>,
exit_node: Option<NymNodeWithKeys>,
data_path: Option<PathBuf>,
mixnet_client_config: Option<MixnetClientConfig>,
two_hops: bool,
user_agent: Option<UserAgent>,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
network_env: Option<NymNetworkDetails>,
cancel_token: Option<CancellationToken>,
#[cfg(unix)]
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
}
impl BuilderConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn entry_node(mut self, entry_node: NymNodeWithKeys) -> Self {
self.entry_node = Some(entry_node);
self
}
pub fn exit_node(mut self, exit_node: NymNodeWithKeys) -> Self {
self.exit_node = Some(exit_node);
self
}
pub fn data_path(mut self, data_path: Option<PathBuf>) -> Self {
self.data_path = data_path;
self
}
pub fn mixnet_client_config(mut self, mixnet_client_config: MixnetClientConfig) -> Self {
self.mixnet_client_config = Some(mixnet_client_config);
self
}
pub fn two_hops(mut self, two_hops: bool) -> Self {
self.two_hops = two_hops;
self
}
pub fn user_agent(mut self, user_agent: UserAgent) -> Self {
self.user_agent = Some(user_agent);
self
}
pub fn custom_topology_provider(
mut self,
custom_topology_provider: Box<dyn TopologyProvider + Send + Sync>,
) -> Self {
self.custom_topology_provider = Some(custom_topology_provider);
self
}
pub fn network_env(mut self, network_env: NymNetworkDetails) -> Self {
self.network_env = Some(network_env);
self
}
pub fn cancel_token(mut self, cancel_token: CancellationToken) -> Self {
self.cancel_token = Some(cancel_token);
self
}
#[cfg(unix)]
pub fn connection_fd_callback(
mut self,
connection_fd_callback: Arc<dyn Fn(RawFd) + Send + Sync>,
) -> Self {
self.connection_fd_callback = Some(connection_fd_callback);
self
}
/// Builds the `BuilderConfig`.
///
/// Returns an error if any required field is missing.
pub fn build(self) -> Result<BuilderConfig, BuilderConfigError> {
Ok(BuilderConfig {
entry_node: self
.entry_node
.ok_or(BuilderConfigError::MissingEntryNode)?,
exit_node: self.exit_node.ok_or(BuilderConfigError::MissingExitNode)?,
data_path: self.data_path,
mixnet_client_config: self
.mixnet_client_config
.ok_or(BuilderConfigError::MissingMixnetClientConfig)?,
two_hops: self.two_hops,
user_agent: self
.user_agent
.ok_or(BuilderConfigError::MissingUserAgent)?,
custom_topology_provider: self
.custom_topology_provider
.ok_or(BuilderConfigError::MissingTopologyProvider)?,
network_env: self
.network_env
.ok_or(BuilderConfigError::MissingNetworkEnv)?,
cancel_token: self
.cancel_token
.ok_or(BuilderConfigError::MissingCancelToken)?,
#[cfg(unix)]
connection_fd_callback: self
.connection_fd_callback
.ok_or(BuilderConfigError::MissingConnectionFdCallback)?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -224,4 +404,54 @@ mod tests {
assert_eq!(config.min_mixnode_performance, None);
assert_eq!(config.min_gateway_performance, None);
}
#[test]
fn test_builder_config_builder_fails_without_required_fields() {
// Building without any fields should fail with specific error
let result = BuilderConfig::builder().build();
assert!(result.is_err());
match result {
Err(BuilderConfigError::MissingEntryNode) => (), // Expected
Err(e) => panic!("Expected MissingEntryNode, got: {}", e),
Ok(_) => panic!("Expected error, got Ok"),
}
}
#[test]
fn test_builder_config_builder_validates_all_required_fields() {
// Test that each required field is validated
let result = BuilderConfig::builder().build();
assert!(result.is_err());
// Short-circuits at first missing field, so we just verify it's one of the expected errors
#[allow(unreachable_patterns)] // All variants are covered, but keeping catch-all for safety
match result {
Err(BuilderConfigError::MissingEntryNode)
| Err(BuilderConfigError::MissingExitNode)
| Err(BuilderConfigError::MissingMixnetClientConfig)
| Err(BuilderConfigError::MissingUserAgent)
| Err(BuilderConfigError::MissingTopologyProvider)
| Err(BuilderConfigError::MissingNetworkEnv)
| Err(BuilderConfigError::MissingCancelToken) => (),
#[cfg(unix)]
Err(BuilderConfigError::MissingConnectionFdCallback) => (),
Err(e) => panic!("Unexpected error: {}", e),
Ok(_) => panic!("Expected validation error, got Ok"),
}
}
#[test]
fn test_builder_config_builder_method_chaining() {
// Test that builder methods chain properly and return Self
let builder = BuilderConfig::builder();
// Verify the builder returns itself for chaining
let builder = builder.two_hops(true);
let builder = builder.two_hops(false);
let builder = builder.data_path(None);
// Builder should still fail because required fields are missing
let result = builder.build();
assert!(result.is_err());
}
}
+5 -2
View File
@@ -12,12 +12,15 @@ 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,
}
@@ -46,7 +49,7 @@ impl RegistrationClientBuilder {
let builder = MixnetClientBuilder::new_with_storage(mixnet_client_storage)
.event_tx(EventSender(event_tx));
let mixnet_client = tokio::time::timeout(
self.config.mixnet_client_startup_timeout,
MIXNET_CLIENT_STARTUP_TIMEOUT,
self.config.build_and_connect_mixnet_client(builder),
)
.await??;
@@ -56,7 +59,7 @@ impl RegistrationClientBuilder {
} else {
let builder = MixnetClientBuilder::new_ephemeral().event_tx(EventSender(event_tx));
let mixnet_client = tokio::time::timeout(
self.config.mixnet_client_startup_timeout,
MIXNET_CLIENT_STARTUP_TIMEOUT,
self.config.build_and_connect_mixnet_client(builder),
)
.await??;
+6 -72
View File
@@ -35,85 +35,19 @@ pub enum RegistrationClientError {
#[error("timeout connecting the mixnet client")]
Timeout(#[from] tokio::time::error::Elapsed),
#[error(
"failed to register wireguard with the gateway for {gateway_id}, no credential was sent"
)]
WireguardEntryRegistration {
#[error("failed to register wireguard with the gateway for {gateway_id}")]
EntryGatewayRegisterWireguard {
gateway_id: String,
authenticator_address: Box<nym_sdk::mixnet::Recipient>,
#[source]
source: Box<nym_authenticator_client::AuthenticationClientError>,
source: Box<nym_authenticator_client::Error>,
},
#[error(
"failed to register wireguard with the gateway for {gateway_id}, no credential was sent"
)]
WireguardExitRegistration {
#[error("failed to register wireguard with the gateway for {gateway_id}")]
ExitGatewayRegisterWireguard {
gateway_id: String,
authenticator_address: Box<nym_sdk::mixnet::Recipient>,
#[source]
source: Box<nym_authenticator_client::AuthenticationClientError>,
},
#[error(
"failed to register wireguard with the gateway for {gateway_id}, a credential was sent"
)]
WireguardEntryRegistrationCredentialSent {
gateway_id: String,
authenticator_address: Box<nym_sdk::mixnet::Recipient>,
#[source]
source: Box<nym_authenticator_client::AuthenticationClientError>,
},
#[error(
"failed to register wireguard with the gateway for {gateway_id}, a credential was sent"
)]
WireguardExitRegistrationCredentialSent {
gateway_id: String,
authenticator_address: Box<nym_sdk::mixnet::Recipient>,
#[source]
source: Box<nym_authenticator_client::AuthenticationClientError>,
source: Box<nym_authenticator_client::Error>,
},
}
impl RegistrationClientError {
pub fn from_authenticator_error(
error: nym_authenticator_client::RegistrationError,
gateway_id: String,
authenticator_address: nym_sdk::mixnet::Recipient,
entry: bool,
) -> Self {
match error {
nym_authenticator_client::RegistrationError::NoCredentialSent(source) => {
if entry {
Self::WireguardEntryRegistration {
gateway_id,
authenticator_address: Box::new(authenticator_address),
source: Box::new(source),
}
} else {
Self::WireguardExitRegistration {
gateway_id,
authenticator_address: Box::new(authenticator_address),
source: Box::new(source),
}
}
}
nym_authenticator_client::RegistrationError::CredentialSent { source } => {
if entry {
Self::WireguardEntryRegistrationCredentialSent {
gateway_id,
authenticator_address: Box::new(authenticator_address),
source: Box::new(source),
}
} else {
Self::WireguardExitRegistrationCredentialSent {
gateway_id,
authenticator_address: Box::new(authenticator_address),
source: Box::new(source),
}
}
}
}
}
}
+49 -102
View File
@@ -9,7 +9,6 @@ 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;
@@ -35,49 +34,23 @@ 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, RegistrationError> {
async fn register_mix_exit(self) -> Result<RegistrationResult, RegistrationClientError> {
let entry_mixnet_gateway_ip = self.config.entry.node.ip_address;
let exit_mixnet_gateway_ip = self.config.exit.node.ip_address;
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 ipr_address = self.config.exit.node.ipr_address.ok_or(
RegistrationClientError::NoIpPacketRouterAddress {
node_id: self.config.exit.node.identity.to_base58_string(),
},
)?;
let mut ipr_client =
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))
IprClientConnect::new(self.mixnet_client, self.cancel_token.clone()).await;
let interface_addresses = ipr_client
.connect(ipr_address)
.await
{
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,
});
}
};
.map_err(RegistrationClientError::ConnectToIpPacketRouter)?;
Ok(RegistrationResult::Mixnet(Box::new(
MixnetRegistrationResult {
@@ -94,24 +67,18 @@ impl RegistrationClient {
)))
}
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(),
},
});
};
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(),
},
)?;
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 exit_auth_address = self.config.exit.node.authenticator_address.ok_or(
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}");
@@ -120,10 +87,8 @@ 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.child_token())
.start();
AuthClientMixnetListener::new(self.mixnet_client, self.cancel_token.clone()).start();
let mut entry_auth_client = AuthenticatorClient::new(
mixnet_listener.subscribe(),
@@ -150,35 +115,24 @@ impl RegistrationClient {
let exit_fut = exit_auth_client
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardExit);
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, exit) = Box::pin(async { tokio::join!(entry_fut, exit_fut) }).await;
let entry = entry.map_err(|source| RegistrationError {
mixnet_client: None,
source: RegistrationClientError::from_authenticator_error(
source,
self.config.entry.node.identity.to_base58_string(),
entry_auth_address,
true,
),
})?;
let exit = exit.map_err(|source| RegistrationError {
mixnet_client: None,
source: RegistrationClientError::from_authenticator_error(
source,
self.config.exit.node.identity.to_base58_string(),
exit_auth_address,
false,
),
})?;
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),
},
)?;
Ok(RegistrationResult::Wireguard(Box::new(
WireguardRegistrationResult {
@@ -193,23 +147,16 @@ impl RegistrationClient {
}
pub async fn register(self) -> Result<RegistrationResult, RegistrationClientError> {
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;
self.cancel_token
.clone()
.run_until_cancelled(async {
if self.config.two_hops {
self.register_wg().await
} else {
self.register_mix_exit().await
}
Err(error.source)
}
}
})
.await
.ok_or(RegistrationClientError::Cancelled)?
}
}
File diff suppressed because it is too large Load Diff
+10 -21
View File
@@ -706,16 +706,6 @@ 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)
@@ -723,11 +713,6 @@ 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);
}
@@ -736,11 +721,15 @@ where
base_builder = base_builder.with_topology_provider(topology_provider);
}
// 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);
}
// 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);
if let Some(event_tx) = self.event_tx {
base_builder = base_builder.with_event_tx(event_tx);
}
@@ -805,7 +794,7 @@ where
client_output,
client_state.clone(),
nym_address,
started_client.shutdown_handle.clone(),
started_client.shutdown_handle.child_tracker(),
packet_type,
);
@@ -4,7 +4,7 @@
[package]
name = "nym-network-requester"
license = "GPL-3.0"
version = "1.1.67"
version = "1.1.65"
authors.workspace = true
edition.workspace = true
rust-version = "1.85"
+2 -4
View File
@@ -226,8 +226,7 @@ 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
let err_str = err.to_string();
if err_str.contains("nym api") || err_str.contains("failed to connect") {
if err.to_string().contains("nym api request failed") {
return Ok(());
}
return Err(err);
@@ -292,8 +291,7 @@ 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
let err_str = err.to_string();
if err_str.contains("nym api") || err_str.contains("failed to connect") {
if err.to_string().contains("nym api request failed") {
return Ok(());
}
return Err(err);
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-cli"
version = "1.1.66"
version = "1.1.64"
authors.workspace = true
edition = "2021"
license.workspace = true
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nymvisor"
version = "0.1.31"
version = "0.1.29"
authors.workspace = true
repository.workspace = true
homepage.workspace = true