Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b90a75266f | |||
| 26b537e256 | |||
| 17b9fa4dd5 | |||
| 22793bc45e | |||
| 37f3ef58a3 | |||
| 1a4d64a0e5 | |||
| 4dcc568ec2 | |||
| 468835e3a2 | |||
| 28a866e26d | |||
| 350d244032 | |||
| 17ca000782 | |||
| babf113fe5 | |||
| 0d7487f530 |
@@ -4,6 +4,60 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.21-mozzarella] (2025-11-25)
|
||||
|
||||
- [bugfix] Tunnel not waiting on MixnetClient to shut down cleanly ([#6225])
|
||||
- bugfix: fix credential proxy upgrade mode attestation url arg ([#6202])
|
||||
- HTTP API resilience enable & domain rotation conditions ([#6200])
|
||||
- Remove debug feature from http-macro spec in gateway probe ([#6195])
|
||||
- DNS relibility and troubleshooting ([#6179])
|
||||
- [bugfix] Distinguish authenticator errors by credential spent ([#6176])
|
||||
- Typescript SDK 1.4.1 ([#6146])
|
||||
- Enable URL rotation and retries for mixnet gateway init ([#6126])
|
||||
- Feature/credential proxy jwt ([#5957])
|
||||
|
||||
[#6225]: https://github.com/nymtech/nym/pull/6225
|
||||
[#6202]: https://github.com/nymtech/nym/pull/6202
|
||||
[#6200]: https://github.com/nymtech/nym/pull/6200
|
||||
[#6195]: https://github.com/nymtech/nym/pull/6195
|
||||
[#6179]: https://github.com/nymtech/nym/pull/6179
|
||||
[#6176]: https://github.com/nymtech/nym/pull/6176
|
||||
[#6146]: https://github.com/nymtech/nym/pull/6146
|
||||
[#6126]: https://github.com/nymtech/nym/pull/6126
|
||||
[#5957]: https://github.com/nymtech/nym/pull/5957
|
||||
|
||||
## [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])
|
||||
|
||||
Generated
+11
-9
@@ -4825,7 +4825,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.68"
|
||||
version = "1.1.70"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5051,7 +5051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.65"
|
||||
version = "1.1.67"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5134,7 +5134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.65"
|
||||
version = "1.1.67"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -5254,7 +5254,9 @@ dependencies = [
|
||||
"cosmrs",
|
||||
"nym-crypto",
|
||||
"nym-gateway-requests",
|
||||
"nym-topology",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -5443,7 +5445,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-credential-proxy"
|
||||
version = "0.3.0-test"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -5866,12 +5868,12 @@ dependencies = [
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-gateway-requests",
|
||||
"nym-http-api-client",
|
||||
"nym-network-defaults",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx",
|
||||
"nym-statistics-common",
|
||||
"nym-task",
|
||||
"nym-topology",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
@@ -6363,7 +6365,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.66"
|
||||
version = "1.1.68"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -6413,7 +6415,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.20.0"
|
||||
version = "1.22.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -6940,7 +6942,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.65"
|
||||
version = "1.1.67"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -7681,7 +7683,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.30"
|
||||
version = "0.1.32"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
|
||||
+1
-1
@@ -264,7 +264,7 @@ generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
handlebars = "3.5.5"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = "0.25"
|
||||
hickory-resolver = "0.25.2"
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = "1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.65"
|
||||
version = "1.1.67"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.65"
|
||||
version = "1.1.67"
|
||||
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"
|
||||
|
||||
@@ -11,6 +11,7 @@ rust-version.workspace = true
|
||||
async-trait.workspace = true
|
||||
cosmrs.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
@@ -20,6 +21,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
nym-crypto = { path = "../../crypto", features = ["asymmetric"] }
|
||||
nym-gateway-requests = { path = "../../gateway-requests" }
|
||||
nym-topology = { path = "../../topology", features = ["persistence"] }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
|
||||
@@ -48,6 +48,6 @@ pub enum BadGateway {
|
||||
raw_listener: String,
|
||||
|
||||
#[source]
|
||||
source: url::ParseError,
|
||||
source: serde_json::Error,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ use crate::BadGateway;
|
||||
use cosmrs::AccountId;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
|
||||
use nym_topology::EntryDetails;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub const REMOTE_GATEWAY_TYPE: &str = "remote";
|
||||
@@ -67,7 +67,7 @@ impl GatewayDetails {
|
||||
gateway_id: ed25519::PublicKey,
|
||||
shared_key: Arc<SharedGatewayKey>,
|
||||
gateway_owner_address: Option<AccountId>,
|
||||
gateway_listener: Url,
|
||||
gateway_listener: EntryDetails,
|
||||
) -> Self {
|
||||
GatewayDetails::Remote(RemoteGatewayDetails {
|
||||
gateway_id,
|
||||
@@ -164,8 +164,7 @@ pub struct RegisteredGateway {
|
||||
pub gateway_type: GatewayType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RawRemoteGatewayDetails {
|
||||
pub gateway_id_bs58: String,
|
||||
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
|
||||
@@ -174,6 +173,16 @@ pub struct RawRemoteGatewayDetails {
|
||||
pub gateway_listener: String,
|
||||
}
|
||||
|
||||
impl Zeroize for RawRemoteGatewayDetails {
|
||||
fn zeroize(&mut self) {
|
||||
self.gateway_id_bs58.zeroize();
|
||||
self.derived_aes128_ctr_blake3_hmac_keys_bs58.zeroize();
|
||||
self.derived_aes256_gcm_siv_key.zeroize();
|
||||
self.gateway_owner_address.zeroize();
|
||||
}
|
||||
}
|
||||
impl ZeroizeOnDrop for RawRemoteGatewayDetails {}
|
||||
|
||||
impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
|
||||
type Error = BadGateway;
|
||||
|
||||
@@ -230,7 +239,7 @@ impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let gateway_listener = Url::parse(&value.gateway_listener).map_err(|source| {
|
||||
let gateway_listener = serde_json::from_str(&value.gateway_listener).map_err(|source| {
|
||||
BadGateway::MalformedListener {
|
||||
gateway_id: value.gateway_id_bs58.clone(),
|
||||
raw_listener: value.gateway_listener.clone(),
|
||||
@@ -255,12 +264,16 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails {
|
||||
SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None),
|
||||
};
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
let gateway_listener = serde_json::to_string(&value.gateway_listener)
|
||||
.expect("serialize for gateway details should never fail");
|
||||
|
||||
RawRemoteGatewayDetails {
|
||||
gateway_id_bs58: value.gateway_id.to_base58_string(),
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58,
|
||||
derived_aes256_gcm_siv_key,
|
||||
gateway_owner_address: value.gateway_owner_address.as_ref().map(|o| o.to_string()),
|
||||
gateway_listener: value.gateway_listener.to_string(),
|
||||
gateway_listener,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +286,7 @@ pub struct RemoteGatewayDetails {
|
||||
|
||||
pub gateway_owner_address: Option<AccountId>,
|
||||
|
||||
pub gateway_listener: Url,
|
||||
pub gateway_listener: EntryDetails,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_topology::EntryDetails;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GatewayInfo {
|
||||
@@ -14,7 +14,7 @@ pub struct GatewayInfo {
|
||||
pub active: bool,
|
||||
|
||||
pub typ: String,
|
||||
pub endpoint: Option<Url>,
|
||||
pub endpoint: Option<EntryDetails>,
|
||||
}
|
||||
|
||||
impl Display for GatewayInfo {
|
||||
|
||||
@@ -561,7 +561,7 @@ where
|
||||
.gateway_owner_address
|
||||
.as_ref()
|
||||
.map(|o| o.to_string()),
|
||||
details.gateway_listener.to_string(),
|
||||
details.gateway_listener,
|
||||
);
|
||||
GatewayClient::new(
|
||||
GatewayClientConfig::new_default()
|
||||
|
||||
@@ -436,7 +436,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref mut next_delay) = &mut self.next_delay {
|
||||
if let Some(next_delay) = &mut self.next_delay {
|
||||
// it is not yet time to return a message
|
||||
if next_delay.as_mut().poll(cx).is_pending() {
|
||||
return Poll::Pending;
|
||||
|
||||
@@ -40,12 +40,8 @@ pub enum ClientCoreError {
|
||||
#[error("no gateway with id: {0}")]
|
||||
NoGatewayWithId(String),
|
||||
|
||||
#[error("Invalid URL: {0}")]
|
||||
InvalidUrl(String),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("resolution failed: {0}")]
|
||||
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
|
||||
#[error("Invalid Endpoint: {0}")]
|
||||
InvalidEndpoint(String),
|
||||
|
||||
#[error("no gateways on network")]
|
||||
NoGatewaysOnNetwork,
|
||||
|
||||
@@ -21,7 +21,7 @@ use url::Url;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::init::websockets::connect_async;
|
||||
|
||||
use nym_topology::NodeId;
|
||||
use nym_topology::{EntryDetails, NodeId};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::net::TcpStream;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -55,7 +55,7 @@ const PING_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
pub trait ConnectableGateway {
|
||||
fn node_id(&self) -> NodeId;
|
||||
fn identity(&self) -> ed25519::PublicKey;
|
||||
fn clients_address(&self, prefer_ipv6: bool) -> Option<String>;
|
||||
fn endpoint_details(&self) -> Option<EntryDetails>;
|
||||
fn is_wss(&self) -> bool;
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ impl ConnectableGateway for RoutingNode {
|
||||
self.identity_key
|
||||
}
|
||||
|
||||
fn clients_address(&self, prefer_ipv6: bool) -> Option<String> {
|
||||
self.ws_entry_address(prefer_ipv6)
|
||||
fn endpoint_details(&self) -> Option<EntryDetails> {
|
||||
self.entry.clone()
|
||||
}
|
||||
|
||||
fn is_wss(&self) -> bool {
|
||||
@@ -191,7 +191,7 @@ pub async fn gateways_for_init(
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
|
||||
async fn connect(endpoint: &EntryDetails) -> Result<WsConn, ClientCoreError> {
|
||||
match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await {
|
||||
Err(_elapsed) => Err(ClientCoreError::GatewayConnectionTimeout),
|
||||
Ok(Err(conn_failure)) => Err(conn_failure),
|
||||
@@ -208,17 +208,17 @@ async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<'_, G>, Cl
|
||||
where
|
||||
G: ConnectableGateway,
|
||||
{
|
||||
let Some(addr) = gateway.clients_address(false) else {
|
||||
let Some(endpoint) = gateway.endpoint_details() else {
|
||||
return Err(ClientCoreError::UnsupportedEntry {
|
||||
id: gateway.node_id(),
|
||||
identity: gateway.identity().to_string(),
|
||||
});
|
||||
};
|
||||
trace!(
|
||||
"establishing connection to {} ({addr})...",
|
||||
"establishing connection to {} ({endpoint})...",
|
||||
gateway.identity(),
|
||||
);
|
||||
let mut stream = connect(&addr).await?;
|
||||
let mut stream = connect(&endpoint).await?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for _ in 0..MEASUREMENTS {
|
||||
@@ -379,7 +379,7 @@ pub(super) fn get_specified_gateway(
|
||||
|
||||
pub(super) async fn register_with_gateway(
|
||||
gateway_id: ed25519::PublicKey,
|
||||
gateway_listener: Url,
|
||||
gateway_listener: EntryDetails,
|
||||
our_identity: Arc<ed25519::KeyPair>,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
) -> Result<RegistrationResult, ClientCoreError> {
|
||||
|
||||
@@ -14,6 +14,7 @@ use nym_gateway_client::client::InitGatewayClient;
|
||||
use nym_gateway_requests::shared_key::SharedGatewayKey;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_topology::node::RoutingNode;
|
||||
use nym_topology::EntryDetails;
|
||||
use nym_validator_client::client::IdentityKey;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use serde::Serialize;
|
||||
@@ -22,7 +23,6 @@ use std::fmt::{Debug, Display};
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
pub enum SelectedGateway {
|
||||
Remote {
|
||||
@@ -30,7 +30,7 @@ pub enum SelectedGateway {
|
||||
|
||||
gateway_owner_address: Option<AccountId>,
|
||||
|
||||
gateway_listener: Url,
|
||||
gateway_listener: EntryDetails,
|
||||
},
|
||||
Custom {
|
||||
gateway_id: ed25519::PublicKey,
|
||||
@@ -46,25 +46,25 @@ impl SelectedGateway {
|
||||
// for now, let's use 'old' behaviour, if you want to change it, you can pass it up the enum stack yourself : )
|
||||
let prefer_ipv6 = false;
|
||||
|
||||
let gateway_listener = if must_use_tls {
|
||||
node.ws_entry_address_tls()
|
||||
.ok_or(ClientCoreError::UnsupportedWssProtocol {
|
||||
gateway: node.identity_key.to_base58_string(),
|
||||
})?
|
||||
} else {
|
||||
node.ws_entry_address(prefer_ipv6)
|
||||
.ok_or(ClientCoreError::UnsupportedEntry {
|
||||
id: node.node_id,
|
||||
identity: node.identity_key.to_base58_string(),
|
||||
})?
|
||||
};
|
||||
let gateway_listener = node.entry.ok_or(ClientCoreError::UnsupportedEntry {
|
||||
id: node.node_id,
|
||||
identity: node.identity_key.to_base58_string(),
|
||||
})?;
|
||||
|
||||
let gateway_listener =
|
||||
Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener {
|
||||
gateway_id: node.identity_key.to_base58_string(),
|
||||
raw_listener: gateway_listener,
|
||||
source,
|
||||
})?;
|
||||
if must_use_tls
|
||||
&& (gateway_listener.hostname.is_none() || gateway_listener.clients_wss_port.is_none())
|
||||
{
|
||||
return Err(ClientCoreError::UnsupportedWssProtocol {
|
||||
gateway: node.identity_key.to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if prefer_ipv6 && gateway_listener.ip_addresses.iter().all(|i| i.is_ipv4()) {
|
||||
return Err(ClientCoreError::UnsupportedEntry {
|
||||
id: node.node_id,
|
||||
identity: node.identity_key.to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(SelectedGateway::Remote {
|
||||
gateway_id: node.identity_key,
|
||||
|
||||
@@ -1,43 +1,60 @@
|
||||
use crate::error::ClientCoreError;
|
||||
|
||||
use nym_http_api_client::HickoryDnsResolver;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use nym_topology::EntryDetails;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
use tungstenite::handshake::client::Response;
|
||||
use url::{Host, Url};
|
||||
use url::Url;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn connect_async(
|
||||
endpoint: &str,
|
||||
endpoint: &EntryDetails,
|
||||
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), ClientCoreError> {
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
let uri = Url::parse(endpoint).map_err(|_| ClientCoreError::InvalidUrl(endpoint.to_owned()))?;
|
||||
let uri = ws_entry_address(endpoint, false)
|
||||
.ok_or(ClientCoreError::InvalidEndpoint(endpoint.to_string()))?;
|
||||
let port: u16 = uri.port_or_known_default().unwrap_or(443);
|
||||
|
||||
let host = uri
|
||||
.host()
|
||||
.ok_or(ClientCoreError::InvalidUrl(endpoint.to_owned()))?;
|
||||
|
||||
// Get address for tcp connection, if a domain is provided use our preferred resolver rather than
|
||||
// the default std resolve
|
||||
let sock_addrs: Vec<SocketAddr> = match host {
|
||||
Host::Ipv4(addr) => vec![SocketAddr::new(addr.into(), port)],
|
||||
Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)],
|
||||
Host::Domain(domain) => {
|
||||
// Do a DNS lookup for the domain using our custom DNS resolver
|
||||
resolver
|
||||
.resolve_str(domain)
|
||||
.await?
|
||||
.map(|a| SocketAddr::new(a, port))
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
let sock_addrs: Vec<SocketAddr> = endpoint
|
||||
.ip_addresses
|
||||
.iter()
|
||||
.map(|addr| SocketAddr::new(addr.clone(), port))
|
||||
.collect();
|
||||
|
||||
let stream = TcpStream::connect(&sock_addrs[..]).await?;
|
||||
|
||||
tokio_tungstenite::client_async_tls(endpoint, stream)
|
||||
tokio_tungstenite::client_async_tls(uri.to_string(), stream)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn ws_entry_address_tls(entry: &EntryDetails) -> Option<Url> {
|
||||
let hostname = entry.hostname.as_ref()?;
|
||||
let wss_port = entry.clients_wss_port?;
|
||||
|
||||
Url::parse(&format!("wss://{hostname}:{wss_port}")).ok()
|
||||
}
|
||||
|
||||
pub fn ws_entry_address_no_tls(entry: &EntryDetails, prefer_ipv6: bool) -> Option<Url> {
|
||||
if let Some(hostname) = entry.hostname.as_ref() {
|
||||
return Url::parse(&format!("ws://{hostname}:{}", entry.clients_ws_port)).ok();
|
||||
}
|
||||
|
||||
if prefer_ipv6 {
|
||||
if let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) {
|
||||
return Url::parse(&format!("ws://{ipv6}:{}", entry.clients_ws_port)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
let any_ip = entry.ip_addresses.first()?;
|
||||
Url::parse(&format!("ws://{any_ip}:{}", entry.clients_ws_port)).ok()
|
||||
}
|
||||
|
||||
pub fn ws_entry_address(entry: &EntryDetails, prefer_ipv6: bool) -> Option<Url> {
|
||||
if let Some(tls) = ws_entry_address_tls(entry) {
|
||||
return Some(tls);
|
||||
}
|
||||
ws_entry_address_no_tls(entry, prefer_ipv6)
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ nym-credential-storage = { path = "../../credential-storage" }
|
||||
nym-credentials-interface = { path = "../../credentials-interface" }
|
||||
nym-crypto = { path = "../../crypto" }
|
||||
nym-gateway-requests = { path = "../../gateway-requests" }
|
||||
nym-http-api-client = { path = "../../http-api-client" }
|
||||
nym-network-defaults = { path = "../../network-defaults" }
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
nym-statistics-common = { path = "../../statistics" }
|
||||
nym-pemstore = { path = "../../pemstore" }
|
||||
nym-topology = { path = "../../topology", features = ["persistence"] }
|
||||
nym-validator-client = { path = "../validator-client", default-features = false }
|
||||
nym-task = { path = "../../task" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -28,13 +28,13 @@ use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
|
||||
use nym_statistics_common::clients::ClientStatsSender;
|
||||
use nym_task::ShutdownToken;
|
||||
use nym_topology::EntryDetails;
|
||||
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use tracing::*;
|
||||
use tungstenite::protocol::Message;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::RawFd;
|
||||
@@ -62,14 +62,14 @@ pub struct GatewayConfig {
|
||||
// currently a dead field
|
||||
pub gateway_owner: Option<String>,
|
||||
|
||||
pub gateway_listener: String,
|
||||
pub gateway_listener: EntryDetails,
|
||||
}
|
||||
|
||||
impl GatewayConfig {
|
||||
pub fn new(
|
||||
gateway_identity: ed25519::PublicKey,
|
||||
gateway_owner: Option<String>,
|
||||
gateway_listener: String,
|
||||
gateway_listener: EntryDetails,
|
||||
) -> Self {
|
||||
GatewayConfig {
|
||||
gateway_identity,
|
||||
@@ -92,7 +92,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
|
||||
|
||||
authenticated: bool,
|
||||
bandwidth: ClientBandwidth,
|
||||
gateway_address: String,
|
||||
gateway_address: EntryDetails,
|
||||
gateway_identity: ed25519::PublicKey,
|
||||
local_identity: Arc<ed25519::KeyPair>,
|
||||
shared_key: Option<Arc<SharedGatewayKey>>,
|
||||
@@ -201,7 +201,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
|
||||
debug!(
|
||||
"Attempting to establish connection to gateway at: {}",
|
||||
"Attempting to establish connection to gateway at: {:?}",
|
||||
self.gateway_address
|
||||
);
|
||||
let (ws_stream, _) = connect_async(
|
||||
@@ -467,7 +467,9 @@ impl<C, St> GatewayClient<C, St> {
|
||||
// right now there are no failure cases here, but this might change in the future
|
||||
match gateway_protocol {
|
||||
None => {
|
||||
warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0");
|
||||
warn!(
|
||||
"the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"
|
||||
);
|
||||
// note: in +1.2.0 we will have to return a hard error here
|
||||
Ok(())
|
||||
}
|
||||
@@ -481,7 +483,9 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
|
||||
Some(_) => {
|
||||
debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!");
|
||||
debug!(
|
||||
"the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -527,7 +531,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
status,
|
||||
} => (status, protocol_version),
|
||||
ServerResponse::Error { message } => {
|
||||
return Err(GatewayClientError::GatewayError(message))
|
||||
return Err(GatewayClientError::GatewayError(message));
|
||||
}
|
||||
other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
|
||||
};
|
||||
@@ -589,7 +593,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
{
|
||||
ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce),
|
||||
ServerResponse::Error { message } => {
|
||||
return Err(GatewayClientError::GatewayError(message))
|
||||
return Err(GatewayClientError::GatewayError(message));
|
||||
}
|
||||
other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
|
||||
};
|
||||
@@ -858,7 +862,9 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
warn!("Not enough bandwidth. Trying to get more bandwidth, this might take a while");
|
||||
if !self.cfg.bandwidth.require_tickets {
|
||||
info!("The client is running in disabled credentials mode - attempting to claim bandwidth without a credential");
|
||||
info!(
|
||||
"The client is running in disabled credentials mode - attempting to claim bandwidth without a credential"
|
||||
);
|
||||
return self.try_claim_testnet_bandwidth().await;
|
||||
}
|
||||
|
||||
@@ -896,7 +902,9 @@ impl<C, St> GatewayClient<C, St> {
|
||||
Err(err) => {
|
||||
error!("failed to claim ecash bandwidth with the gateway...: {err}");
|
||||
if err.is_ticket_replay() {
|
||||
warn!("this was due to our ticket being replayed! have you messed with the database file?")
|
||||
warn!(
|
||||
"this was due to our ticket being replayed! have you messed with the database file?"
|
||||
)
|
||||
} else {
|
||||
// TODO: tracing span
|
||||
info!("attempting to revert ticket withdrawal...");
|
||||
@@ -1112,7 +1120,9 @@ impl<C, St> GatewayClient<C, St> {
|
||||
self.cfg
|
||||
.bandwidth
|
||||
.ensure_above_cutoff(bandwidth_remaining)?;
|
||||
info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen.");
|
||||
info!(
|
||||
"Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen."
|
||||
);
|
||||
self.claim_bandwidth().await?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -1128,7 +1138,7 @@ pub struct InitOnly;
|
||||
impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
|
||||
// for initialisation we do not need credential storage. Though it's still a bit weird we have to set the generic...
|
||||
pub fn new_init(
|
||||
gateway_listener: Url,
|
||||
gateway_listener: EntryDetails,
|
||||
gateway_identity: ed25519::PublicKey,
|
||||
local_identity: Arc<ed25519::KeyPair>,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
@@ -1147,7 +1157,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
|
||||
cfg: GatewayClientConfig::default().with_disabled_credentials_mode(true),
|
||||
authenticated: false,
|
||||
bandwidth: ClientBandwidth::new_empty(),
|
||||
gateway_address: gateway_listener.to_string(),
|
||||
gateway_address: gateway_listener.clone(),
|
||||
gateway_identity,
|
||||
local_identity,
|
||||
shared_key: None,
|
||||
|
||||
@@ -1,51 +1,37 @@
|
||||
use crate::error::GatewayClientError;
|
||||
|
||||
use nym_http_api_client::HickoryDnsResolver;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use nym_topology::EntryDetails;
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
os::fd::{AsRawFd, RawFd},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::net::TcpSocket;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||
use tungstenite::handshake::client::Response;
|
||||
use url::{Host, Url};
|
||||
use url::Url;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn connect_async(
|
||||
endpoint: &str,
|
||||
endpoint: &EntryDetails,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), GatewayClientError> {
|
||||
use tokio::net::TcpSocket;
|
||||
|
||||
let resolver = HickoryDnsResolver::default();
|
||||
let uri =
|
||||
Url::parse(endpoint).map_err(|_| GatewayClientError::InvalidUrl(endpoint.to_owned()))?;
|
||||
let uri = ws_entry_address(endpoint, false)
|
||||
.ok_or(GatewayClientError::InvalidEndpoint(endpoint.to_string()))?;
|
||||
let port: u16 = uri.port_or_known_default().unwrap_or(443);
|
||||
|
||||
let host = uri
|
||||
.host()
|
||||
.ok_or(GatewayClientError::InvalidUrl(endpoint.to_owned()))?;
|
||||
|
||||
// Get address for tcp connection, if a domain is provided use our preferred resolver rather than
|
||||
// the default std resolve
|
||||
let sock_addrs: Vec<SocketAddr> = match host {
|
||||
Host::Ipv4(addr) => vec![SocketAddr::new(addr.into(), port)],
|
||||
Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)],
|
||||
Host::Domain(domain) => {
|
||||
// Do a DNS lookup for the domain using our custom DNS resolver
|
||||
resolver
|
||||
.resolve_str(domain)
|
||||
.await?
|
||||
.map(|a| SocketAddr::new(a, port))
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
let sock_addrs = endpoint
|
||||
.ip_addresses
|
||||
.iter()
|
||||
.map(|addr| SocketAddr::new(*addr, port));
|
||||
let uri_str = uri.to_string();
|
||||
|
||||
let mut stream = Err(GatewayClientError::NoEndpointForConnection {
|
||||
address: endpoint.to_owned(),
|
||||
address: uri_str.clone(),
|
||||
});
|
||||
for sock_addr in sock_addrs {
|
||||
let socket = if sock_addr.is_ipv4() {
|
||||
@@ -54,7 +40,7 @@ pub(crate) async fn connect_async(
|
||||
TcpSocket::new_v6()
|
||||
}
|
||||
.map_err(|err| GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
address: uri_str.clone(),
|
||||
source: Box::new(tungstenite::Error::from(err)),
|
||||
})?;
|
||||
|
||||
@@ -70,7 +56,7 @@ pub(crate) async fn connect_async(
|
||||
}
|
||||
Err(err) => {
|
||||
stream = Err(GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
address: uri_str.clone(),
|
||||
source: Box::new(tungstenite::Error::from(err)),
|
||||
});
|
||||
continue;
|
||||
@@ -78,10 +64,39 @@ pub(crate) async fn connect_async(
|
||||
}
|
||||
}
|
||||
|
||||
tokio_tungstenite::client_async_tls(endpoint, stream?)
|
||||
tokio_tungstenite::client_async_tls(uri.clone(), stream?)
|
||||
.await
|
||||
.map_err(|error| GatewayClientError::NetworkConnectionFailed {
|
||||
address: endpoint.to_owned(),
|
||||
address: uri_str.clone(),
|
||||
source: Box::new(error),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ws_entry_address_tls(entry: &EntryDetails) -> Option<Url> {
|
||||
let hostname = entry.hostname.as_ref()?;
|
||||
let wss_port = entry.clients_wss_port?;
|
||||
|
||||
Url::parse(&format!("wss://{hostname}:{wss_port}")).ok()
|
||||
}
|
||||
|
||||
pub fn ws_entry_address_no_tls(entry: &EntryDetails, prefer_ipv6: bool) -> Option<Url> {
|
||||
if let Some(hostname) = entry.hostname.as_ref() {
|
||||
return Url::parse(&format!("ws://{hostname}:{}", entry.clients_ws_port)).ok();
|
||||
}
|
||||
|
||||
if prefer_ipv6 {
|
||||
if let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) {
|
||||
return Url::parse(&format!("ws://{ipv6}:{}", entry.clients_ws_port)).ok();
|
||||
}
|
||||
}
|
||||
|
||||
let any_ip = entry.ip_addresses.first()?;
|
||||
Url::parse(&format!("ws://{any_ip}:{}", entry.clients_ws_port)).ok()
|
||||
}
|
||||
|
||||
pub fn ws_entry_address(entry: &EntryDetails, prefer_ipv6: bool) -> Option<Url> {
|
||||
if let Some(tls) = ws_entry_address_tls(entry) {
|
||||
return Some(tls);
|
||||
}
|
||||
ws_entry_address_no_tls(entry, prefer_ipv6)
|
||||
}
|
||||
|
||||
@@ -49,12 +49,8 @@ pub enum GatewayClientError {
|
||||
#[error("no socket address for endpoint: {address}")]
|
||||
NoEndpointForConnection { address: String },
|
||||
|
||||
#[error("Invalid URL: {0}")]
|
||||
InvalidUrl(String),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("resolution failed: {0}")]
|
||||
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
|
||||
#[error("Invalid Endpoint: {0}")]
|
||||
InvalidEndpoint(String),
|
||||
|
||||
#[error("No shared key was provided or obtained")]
|
||||
NoSharedKeyAvailable,
|
||||
|
||||
@@ -168,6 +168,14 @@ pub enum CredentialProxyError {
|
||||
device_id: String,
|
||||
credential_id: String,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"the attestation check url has not been provided through either the CLI nor the default .env config"
|
||||
)]
|
||||
AttestationCheckUrlNotSet,
|
||||
|
||||
#[error("the provided attestation check url is malformed: {source}")]
|
||||
MalformedAttestationCheckUrl { source: url::ParseError },
|
||||
}
|
||||
|
||||
impl From<NymAPIError> for CredentialProxyError {
|
||||
|
||||
@@ -27,8 +27,6 @@ pub struct QuorumStateChecker {
|
||||
cancellation_token: CancellationToken,
|
||||
check_interval: Duration,
|
||||
quorum_state: QuorumState,
|
||||
max_retries: u32,
|
||||
retry_initial_delay: Duration,
|
||||
}
|
||||
|
||||
impl QuorumStateChecker {
|
||||
@@ -44,8 +42,6 @@ impl QuorumStateChecker {
|
||||
quorum_state: QuorumState {
|
||||
available: Arc::new(Default::default()),
|
||||
},
|
||||
max_retries: 3,
|
||||
retry_initial_delay: Duration::from_secs(2),
|
||||
};
|
||||
|
||||
// first check MUST succeed, otherwise we shouldn't start
|
||||
@@ -60,102 +56,7 @@ impl QuorumStateChecker {
|
||||
self.quorum_state.clone()
|
||||
}
|
||||
|
||||
fn is_retryable_error(&self, err: &CredentialProxyError) -> bool {
|
||||
let err_str = err.to_string().to_lowercase();
|
||||
|
||||
// Check for DNS-related errors
|
||||
if err_str.contains("dns")
|
||||
|| err_str.contains("lookup")
|
||||
|| err_str.contains("name resolution")
|
||||
|| err_str.contains("temporary failure")
|
||||
|| err_str.contains("failed to lookup address")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a Tendermint RPC error (which could be DNS/timeout related)
|
||||
if let CredentialProxyError::NyxdFailure { source: nyxd_err } = err {
|
||||
let nyxd_err_str = nyxd_err.to_string().to_lowercase();
|
||||
if nyxd_err_str.contains("tendermint rpc request failed") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if nyxd_err.is_tendermint_response_timeout() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
|
||||
self.check_quorum_state_with_retry().await
|
||||
}
|
||||
|
||||
async fn check_quorum_state_with_retry(&self) -> Result<bool, CredentialProxyError> {
|
||||
let mut last_error_msg = None;
|
||||
let delay = self.retry_initial_delay;
|
||||
|
||||
for attempt in 0..=self.max_retries {
|
||||
match self.check_quorum_state_once().await {
|
||||
Ok(result) => {
|
||||
if attempt > 0 {
|
||||
info!("quorum check succeeded after {} retry attempt(s)", attempt);
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
Err(err) => {
|
||||
let err_msg = err.to_string();
|
||||
|
||||
// Check if this error is retryable
|
||||
if !self.is_retryable_error(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
last_error_msg = Some(err_msg.clone());
|
||||
|
||||
if attempt >= self.max_retries {
|
||||
break;
|
||||
}
|
||||
|
||||
// Log the retry attempt
|
||||
warn!(
|
||||
"quorum check failed (attempt {}/{}): {}. Retrying in {:?}...",
|
||||
attempt + 1,
|
||||
self.max_retries + 1,
|
||||
err_msg,
|
||||
delay
|
||||
);
|
||||
|
||||
// Wait before retrying with exponential backoff
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try one final time to get the actual error
|
||||
match self.check_quorum_state_once().await {
|
||||
Ok(result) => {
|
||||
warn!(
|
||||
"quorum check succeeded on final attempt after {} retries",
|
||||
self.max_retries
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(error_msg) = last_error_msg {
|
||||
error!(
|
||||
"quorum check failed after {} retry attempts. Last error: {}",
|
||||
self.max_retries + 1,
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_quorum_state_once(&self) -> Result<bool, CredentialProxyError> {
|
||||
let client_guard = self.client.query_chain().await;
|
||||
|
||||
// split the operation as we only need to hold the reference to chain client for the first part
|
||||
@@ -192,7 +93,7 @@ impl QuorumStateChecker {
|
||||
break
|
||||
}
|
||||
_ = tokio::time::sleep(self.check_interval) => {
|
||||
match self.check_quorum_state_with_retry().await {
|
||||
match self.check_quorum_state().await {
|
||||
Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst),
|
||||
Err(err) => error!("failed to check current quorum state: {err}"),
|
||||
}
|
||||
|
||||
+370
-129
@@ -3,28 +3,41 @@
|
||||
|
||||
//! DNS resolver configuration for internal lookups.
|
||||
//!
|
||||
//! The resolver itself is the set combination of the google, cloudflare, and quad9 endpoints
|
||||
//! supporting DoH and DoT.
|
||||
//! The resolver itself is the set combination of the cloudflare, and quad9 endpoints supporting DoH
|
||||
//! and DoT.
|
||||
//!
|
||||
//! This resolver supports a fallback mechanism where, should the DNS-over-TLS resolution fail, a
|
||||
//! followup resolution will be done using the hosts configured default (e.g. `/etc/resolve.conf` on
|
||||
//! linux). This is disabled by default and can be enabled using [`enable_system_fallback`].
|
||||
//!
|
||||
//! Requires the `dns-over-https-rustls`, `webpki-roots` feature for the
|
||||
//! `hickory-resolver` crate
|
||||
//!
|
||||
//!
|
||||
//! Note: The hickory DoH resolver can cause warning logs about H2 connection failure. This
|
||||
//! indicates that the long lived https connection was closed by the remote peer and the resolver
|
||||
//! will have to reconnect. It should not impact actual functionality.
|
||||
//!
|
||||
//! code ref: https://github.com/hickory-dns/hickory-dns/blob/06a8b1ce9bd9322d8e6accf857d30257e1274427/crates/proto/src/h2/h2_client_stream.rs#L534
|
||||
//!
|
||||
//! example log:
|
||||
//!
|
||||
//! ```txt
|
||||
//! WARN /home/ubuntu/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hickory-proto-0.24.3/src/h2/h2_client_stream.rs:493: h2 connection failed: unexpected end of file
|
||||
//! ```rust
|
||||
//! use nym_http_api_client::HickoryDnsResolver;
|
||||
//! # use nym_http_api_client::ResolveError;
|
||||
//! # type Err = ResolveError;
|
||||
//! # async fn run() -> Result<(), Err> {
|
||||
//! let resolver = HickoryDnsResolver::default();
|
||||
//! resolver.resolve_str("example.com").await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Fallbacks
|
||||
//!
|
||||
//! **System Resolver --** This resolver supports an optional fallback mechanism where, should the
|
||||
//! DNS-over-TLS resolution fail, a followup resolution will be done using the hosts configured
|
||||
//! default (e.g. `/etc/resolve.conf` on linux).
|
||||
//!
|
||||
//! This is disabled by default and can be enabled using `enable_system_fallback`.
|
||||
//!
|
||||
//! **Static Table --** There is also a second optional fallback mechanism that allows a static map
|
||||
//! to be used as a last resort. This can help when DNS encounters errors due to blocked resolvers
|
||||
//! or unknown conditions. This is enabled by default, and can be customized if building a new
|
||||
//! resolver.
|
||||
//!
|
||||
//! ## IPv4 / IPv6
|
||||
//!
|
||||
//! By default the resolver uses only IPv4 nameservers, and is configured to do `A` lookups first,
|
||||
//! and only do `AAAA` if no `A` record is available.
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! Requires the `dns-over-https-rustls`, `webpki-roots` feature for the `hickory-resolver` crate
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use crate::ClientBuilder;
|
||||
@@ -39,7 +52,7 @@ use std::{
|
||||
|
||||
use hickory_resolver::{
|
||||
TokioResolver,
|
||||
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig},
|
||||
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
|
||||
lookup_ip::LookupIpIntoIter,
|
||||
name_server::TokioConnectionProvider,
|
||||
};
|
||||
@@ -49,7 +62,11 @@ use tracing::*;
|
||||
|
||||
mod constants;
|
||||
mod static_resolver;
|
||||
pub use static_resolver::*;
|
||||
pub(crate) use static_resolver::*;
|
||||
|
||||
pub(crate) const DEFAULT_POSITIVE_LOOKUP_CACHE_TTL: Duration = Duration::from_secs(1800);
|
||||
pub(crate) const DEFAULT_OVERALL_LOOKUP_TIMEOUT: Duration = Duration::from_secs(6);
|
||||
pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
impl ClientBuilder {
|
||||
/// Override the DNS resolver implementation used by the underlying http client.
|
||||
@@ -71,7 +88,10 @@ impl ClientBuilder {
|
||||
// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional.
|
||||
static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
|
||||
tracing::debug!("Initializing shared DNS resolver");
|
||||
HickoryDnsResolver::default()
|
||||
HickoryDnsResolver {
|
||||
use_shared: false, // prevent infinite recursion
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -88,6 +108,13 @@ pub enum ResolveError {
|
||||
StaticLookupMiss,
|
||||
}
|
||||
|
||||
impl ResolveError {
|
||||
/// Returns true if the error is a timeout.
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
matches!(self, ResolveError::Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait.
|
||||
///
|
||||
/// Typical use involves instantiating using the `Default` implementation and then resolving using
|
||||
@@ -104,7 +131,7 @@ pub struct HickoryDnsResolver {
|
||||
state: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
static_base: Option<Arc<OnceCell<StaticResolver>>>,
|
||||
dont_use_shared: bool,
|
||||
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,
|
||||
@@ -115,9 +142,9 @@ impl Default for HickoryDnsResolver {
|
||||
Self {
|
||||
state: Default::default(),
|
||||
fallback: Default::default(),
|
||||
static_base: Default::default(),
|
||||
dont_use_shared: Default::default(),
|
||||
overall_dns_timeout: Duration::from_secs(10),
|
||||
static_base: Some(Default::default()),
|
||||
use_shared: true,
|
||||
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,7 +154,7 @@ impl Resolve for HickoryDnsResolver {
|
||||
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 use_shared = self.use_shared;
|
||||
let overall_dns_timeout = self.overall_dns_timeout;
|
||||
Box::pin(async move {
|
||||
resolve(
|
||||
@@ -135,7 +162,7 @@ impl Resolve for HickoryDnsResolver {
|
||||
resolver,
|
||||
maybe_fallback,
|
||||
maybe_static,
|
||||
independent,
|
||||
use_shared,
|
||||
overall_dns_timeout,
|
||||
)
|
||||
.await
|
||||
@@ -229,7 +256,7 @@ impl HickoryDnsResolver {
|
||||
self.state.clone(),
|
||||
self.fallback.clone(),
|
||||
self.static_base.clone(),
|
||||
self.dont_use_shared,
|
||||
self.use_shared,
|
||||
self.overall_dns_timeout,
|
||||
)
|
||||
.await
|
||||
@@ -239,25 +266,25 @@ impl HickoryDnsResolver {
|
||||
/// Create a (lazy-initialized) resolver that is not shared across threads.
|
||||
pub fn thread_resolver() -> Self {
|
||||
Self {
|
||||
dont_use_shared: true,
|
||||
use_shared: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver(dont_use_shared: bool) -> Result<TokioResolver, ResolveError> {
|
||||
fn new_resolver(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 {
|
||||
if !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> {
|
||||
fn new_resolver_system(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() {
|
||||
if !use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
@@ -269,8 +296,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 {
|
||||
fn new_static_fallback(use_shared: bool) -> StaticResolver {
|
||||
if use_shared && let Some(ref shared_resolver) = SHARED_RESOLVER.static_base {
|
||||
shared_resolver
|
||||
.get_or_init(new_default_static_fallback)
|
||||
.clone()
|
||||
@@ -287,6 +314,11 @@ impl HickoryDnsResolver {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_try_init(new_resolver_system)?;
|
||||
|
||||
// IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
|
||||
// if self.use_shared {
|
||||
// SHARED_RESOLVER.enable_system_fallback()?;
|
||||
// }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -294,6 +326,11 @@ impl HickoryDnsResolver {
|
||||
/// returned immediately
|
||||
pub fn disable_system_fallback(&mut self) {
|
||||
self.fallback = None;
|
||||
|
||||
// // IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
|
||||
// if self.use_shared {
|
||||
// SHARED_RESOLVER.fallback = None;
|
||||
// }
|
||||
}
|
||||
|
||||
/// Get the current map of hostname to address in use by the fallback static lookup if one
|
||||
@@ -309,39 +346,122 @@ impl HickoryDnsResolver {
|
||||
.expect("infallible assign");
|
||||
self.static_base = Some(Arc::new(cell));
|
||||
}
|
||||
|
||||
/// Successfully resolved addresses are cached for a minimum of 30 minutes
|
||||
/// Individual lookup Timeouts are set to 3 seconds
|
||||
/// Number of retries after lookup failure before giving up is set to (default) to 2
|
||||
/// Lookup order is set to (default) A then AAAA
|
||||
/// Number or parallel lookup is set to (default) 2
|
||||
/// Nameserver selection uses the (default) EWMA statistics / performance based strategy
|
||||
fn default_options() -> ResolverOpts {
|
||||
let mut opts = ResolverOpts::default();
|
||||
// Always cache successful responses for queries received by this resolver for 30 min minimum.
|
||||
opts.positive_min_ttl = Some(DEFAULT_POSITIVE_LOOKUP_CACHE_TTL);
|
||||
opts.timeout = DEFAULT_QUERY_TIMEOUT;
|
||||
|
||||
opts
|
||||
}
|
||||
|
||||
/// Get the list of currently available nameserver configs.
|
||||
pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
|
||||
default_nameserver_group().to_vec()
|
||||
}
|
||||
|
||||
/// Get the list of currently used nameserver configs.
|
||||
pub fn active_name_servers(&self) -> Vec<NameServerConfig> {
|
||||
if !self.use_shared {
|
||||
return self
|
||||
.state
|
||||
.get()
|
||||
.map(|r| r.config().name_servers().to_vec())
|
||||
.unwrap_or(self.all_configured_name_servers());
|
||||
}
|
||||
|
||||
SHARED_RESOLVER.active_name_servers()
|
||||
}
|
||||
|
||||
/// Do a trial resolution using each nameserver individually to test which are working and which
|
||||
/// fail to complete a lookup. This will always try the full set of default configured resolvers.
|
||||
pub async fn trial_nameservers(&self) {
|
||||
let nameservers = default_nameserver_group();
|
||||
for (ns, result) in trial_nameservers_inner(&nameservers).await {
|
||||
if let Err(e) = result {
|
||||
warn!("trial {ns:?} errored: {e}");
|
||||
} else {
|
||||
info!("trial {ns:?} succeeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// Individual lookup Timeouts are set to 3 seconds
|
||||
/// Number of retries after lookup failure before giving up Defaults to 2
|
||||
/// Lookup order is set to (default) A then AAAA
|
||||
///
|
||||
/// 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");
|
||||
let name_servers = default_nameserver_group_ipv4_only();
|
||||
|
||||
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)
|
||||
Ok(configure_and_build_resolver(name_servers))
|
||||
}
|
||||
|
||||
fn configure_and_build_resolver(
|
||||
name_servers: NameServerConfigGroup,
|
||||
) -> Result<TokioResolver, ResolveError> {
|
||||
fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
|
||||
where
|
||||
G: Into<NameServerConfigGroup>,
|
||||
{
|
||||
let options = HickoryDnsResolver::default_options();
|
||||
let name_servers: NameServerConfigGroup = name_servers.into();
|
||||
info!("building new configured resolver");
|
||||
debug!("configuring resolver with {options:?}, {name_servers:?}");
|
||||
|
||||
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 = LookupIpStrategy::Ipv4AndIpv6;
|
||||
// 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));
|
||||
resolver_builder = resolver_builder.with_options(options);
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
resolver_builder.build()
|
||||
}
|
||||
|
||||
fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
|
||||
nameservers
|
||||
.as_ref()
|
||||
.iter()
|
||||
.filter(|ns| ns.socket_addr.is_ipv4())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
|
||||
nameservers
|
||||
.as_ref()
|
||||
.iter()
|
||||
.filter(|ns| ns.socket_addr.is_ipv6())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn default_nameserver_group() -> NameServerConfigGroup {
|
||||
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());
|
||||
name_servers
|
||||
}
|
||||
|
||||
fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
|
||||
filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
|
||||
filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
|
||||
}
|
||||
|
||||
/// Create a new resolver with the default configuration, which reads from the system DNS config
|
||||
@@ -349,7 +469,12 @@ fn configure_and_build_resolver(
|
||||
/// addresses to work with "happy eyeballs" algorithm.
|
||||
fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
||||
let mut resolver_builder = TokioResolver::builder_tokio()?;
|
||||
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||
|
||||
let options = HickoryDnsResolver::default_options();
|
||||
info!("building new fallback system resolver");
|
||||
debug!("fallback system resolver with {options:?}");
|
||||
|
||||
resolver_builder = resolver_builder.with_options(options);
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
}
|
||||
@@ -358,11 +483,54 @@ fn new_default_static_fallback() -> StaticResolver {
|
||||
StaticResolver::new(constants::default_static_addrs())
|
||||
}
|
||||
|
||||
/// Do a trial resolution using each nameserver individually to test which are working and which
|
||||
/// fail to complete a lookup.
|
||||
async fn trial_nameservers_inner(
|
||||
name_servers: &[NameServerConfig],
|
||||
) -> Vec<(NameServerConfig, Result<(), ResolveError>)> {
|
||||
let mut trial_lookups = tokio::task::JoinSet::new();
|
||||
|
||||
for name_server in name_servers {
|
||||
let ns = name_server.clone();
|
||||
trial_lookups.spawn(async { (ns.clone(), trial_lookup(ns, "example.com").await) });
|
||||
}
|
||||
|
||||
trial_lookups.join_all().await
|
||||
}
|
||||
|
||||
/// Create an independent resolver that has only the provided nameserver and do one lookup for the
|
||||
/// provided query target.
|
||||
async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
|
||||
debug!("running ns trial {name_server:?} query={query}");
|
||||
|
||||
let resolver = configure_and_build_resolver(vec![name_server]);
|
||||
|
||||
match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
|
||||
Ok(Ok(_)) => Ok(()),
|
||||
Ok(Err(e)) => Err(e.into()),
|
||||
Err(_) => Err(ResolveError::Timeout),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
/// 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)),
|
||||
];
|
||||
|
||||
#[tokio::test]
|
||||
async fn reqwest_with_custom_dns() {
|
||||
@@ -421,99 +589,172 @@ mod test {
|
||||
assert!(addrs.contains(&example_ip6));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod failure_test {
|
||||
use super::*;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
// Test the nameserver trial functionality with mostly nameservers guaranteed to be broken and
|
||||
// one that should work.
|
||||
#[tokio::test]
|
||||
async fn trial_nameservers() {
|
||||
let good_cf_ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
|
||||
|
||||
/// 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)),
|
||||
];
|
||||
let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
|
||||
ns_ips.push(good_cf_ip);
|
||||
|
||||
// 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,
|
||||
&ns_ips,
|
||||
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");
|
||||
let inner = configure_and_build_resolver(broken_ns_https);
|
||||
|
||||
// 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),
|
||||
use_shared: false,
|
||||
state: Arc::new(OnceCell::with_value(inner)),
|
||||
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");
|
||||
let name_servers = resolver.state.get().unwrap().config().name_servers();
|
||||
for (ns, result) in trial_nameservers_inner(name_servers).await {
|
||||
if ns.socket_addr.ip() == good_cf_ip {
|
||||
assert!(result.is_ok())
|
||||
} else {
|
||||
assert!(result.is_err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)));
|
||||
mod failure_test {
|
||||
use super::*;
|
||||
|
||||
Ok(())
|
||||
// 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);
|
||||
|
||||
Ok(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 {
|
||||
use_shared: false,
|
||||
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 {
|
||||
use_shared: false,
|
||||
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 static table
|
||||
let domain = "non-existent.nymtech.net";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolver_uses_ipv4_only_nameservers() {
|
||||
let resolver = HickoryDnsResolver::thread_resolver();
|
||||
resolver
|
||||
.active_name_servers()
|
||||
.iter()
|
||||
.all(|cfg| cfg.socket_addr.is_ipv4());
|
||||
|
||||
SHARED_RESOLVER
|
||||
.active_name_servers()
|
||||
.iter()
|
||||
.all(|cfg| cfg.socket_addr.is_ipv4());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
// this test is dependent of external network setup -- i.e. blocking all traffic to the default
|
||||
// resolvers. Otherwise the default resolvers will succeed without using the static fallback,
|
||||
// making the test pointless
|
||||
async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
|
||||
let time_start = 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::default();
|
||||
|
||||
// successful lookup using fallback to static resolver
|
||||
let domain = "rpc.nymtech.net";
|
||||
let _ = resolver
|
||||
.resolve_str(domain)
|
||||
.await
|
||||
.expect("failed to resolve address in static lookup");
|
||||
|
||||
println!(
|
||||
"{}ms resolved {domain}",
|
||||
(Instant::now() - time_start).as_millis()
|
||||
);
|
||||
|
||||
// unsuccessful lookup - primary times out, and not in static table
|
||||
let domain = "non-existent.nymtech.net";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err());
|
||||
// assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
// assert!(result.is_err_and(|e| matches!(e, ResolveError::ResolveError(e) if e.is_nx_domain())));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,4 +121,89 @@ mod tests {
|
||||
// println!("{response:?}");
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fallback_on_failure() {
|
||||
let url1 = Url::new(
|
||||
"https://fake-domain.nymtech.net",
|
||||
Some(vec![
|
||||
"https://fake-front-1.nymtech.net",
|
||||
"https://fake-front-2.nymtech.net",
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
let url2 = Url::new(
|
||||
"https://validator.global.ssl.fastly.net",
|
||||
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||
)
|
||||
.unwrap(); // fastly
|
||||
|
||||
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
||||
.expect("bad url")
|
||||
.with_fronting(FrontPolicy::Always)
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
|
||||
// Check that the initial configuration has the broken domain and front.
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://fake-domain.nymtech.net/",
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("fake-front-1.nymtech.net"),
|
||||
);
|
||||
|
||||
let result = client
|
||||
.send_request::<_, (), &str, &str>(
|
||||
reqwest::Method::GET,
|
||||
&["api", "v1", "network", "details"],
|
||||
NO_PARAMS,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the host configuration updated the front on error.
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://fake-domain.nymtech.net/",
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("fake-front-2.nymtech.net"),
|
||||
);
|
||||
|
||||
let result = client
|
||||
.send_request::<_, (), &str, &str>(
|
||||
reqwest::Method::GET,
|
||||
&["api", "v1", "network", "details"],
|
||||
NO_PARAMS,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the host configuration updated the domain and front on error.
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://validator.global.ssl.fastly.net/",
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("yelp.global.ssl.fastly.net"),
|
||||
);
|
||||
|
||||
let response = client
|
||||
.send_request::<_, (), &str, &str>(
|
||||
reqwest::Method::GET,
|
||||
&["api", "v1", "network", "details"],
|
||||
NO_PARAMS,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("failed get request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ mod user_agent;
|
||||
pub use user_agent::UserAgent;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod dns;
|
||||
pub mod dns;
|
||||
mod path;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -395,6 +395,13 @@ pub enum HttpClientError {
|
||||
#[error("failed to resolve request to {url} due to data inconsistency: {details}")]
|
||||
InternalResponseInconsistency { url: ::url::Url, details: String },
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("encountered dns failure: {inner}")]
|
||||
DnsLookupFailure {
|
||||
#[from]
|
||||
inner: ResolveError,
|
||||
},
|
||||
|
||||
#[error("Failed to encode bincode: {0}")]
|
||||
Bincode(#[from] bincode::Error),
|
||||
|
||||
@@ -421,6 +428,8 @@ impl HttpClientError {
|
||||
HttpClientError::ReqwestClientError { source } => source.is_timeout(),
|
||||
HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(),
|
||||
HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
HttpClientError::DnsLookupFailure { inner } => inner.is_timeout(),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
HttpClientError::RequestTimeout => true,
|
||||
_ => false,
|
||||
@@ -775,6 +784,7 @@ impl ClientBuilder {
|
||||
base_urls: self.urls,
|
||||
current_idx: Arc::new(AtomicUsize::new(0)),
|
||||
reqwest_client,
|
||||
using_secure_dns: self.use_secure_dns,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front,
|
||||
@@ -795,6 +805,7 @@ pub struct Client {
|
||||
base_urls: Vec<Url>,
|
||||
current_idx: Arc<AtomicUsize>,
|
||||
reqwest_client: reqwest::Client,
|
||||
using_secure_dns: bool,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: Option<fronted::Front>,
|
||||
@@ -852,6 +863,7 @@ impl Client {
|
||||
base_urls: vec![new_url],
|
||||
current_idx: Arc::new(Default::default()),
|
||||
reqwest_client: self.reqwest_client.clone(),
|
||||
using_secure_dns: self.using_secure_dns,
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
front: self.front.clone(),
|
||||
@@ -883,8 +895,34 @@ impl Client {
|
||||
self.retry_limit = limit;
|
||||
}
|
||||
|
||||
fn matches_current_host(&self, url: &Url) -> bool {
|
||||
if cfg!(feature = "tunneling") {
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
{
|
||||
url.host_str() == self.current_url().front_str()
|
||||
} else {
|
||||
url.host_str() == self.current_url().host_str()
|
||||
}
|
||||
} else {
|
||||
url.host_str() == self.current_url().host_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error)
|
||||
fn update_host(&self) {
|
||||
///
|
||||
/// Takes an optional URL argument. If this is none, the current host will be updated automatically.
|
||||
/// If a url is provided first check that the CURRENT host matches the hostname in the URL before
|
||||
/// triggering a rotation. This is meant to prevent parallel requests that fail from rotating the host
|
||||
/// multiple times.
|
||||
fn update_host(&self, maybe_url: Option<Url>) {
|
||||
// If a causal url is provided and it doesn't match the hostname currently in use, skip update.
|
||||
if let Some(err_url) = maybe_url
|
||||
&& !self.matches_current_host(&err_url)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front
|
||||
&& front.is_enabled()
|
||||
@@ -1048,8 +1086,28 @@ impl ApiClientCore for Client {
|
||||
.build()
|
||||
.map_err(HttpClientError::reqwest_client_build_error)?;
|
||||
self.apply_hosts_to_req(&mut req);
|
||||
let url: Url = req.url().clone().into();
|
||||
|
||||
// try an explicit DNS resolution - if successful then it will be in cache when reqwest
|
||||
// goes to execute the request. If failure then we get to handle the DNS lookup error.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let url = req.url().clone();
|
||||
if self.using_secure_dns
|
||||
&& let Some(hostname) = req.url().domain()
|
||||
// Default here will use a shared resolver instance
|
||||
&& let Err(err) = HickoryDnsResolver::default().resolve_str(hostname).await
|
||||
{
|
||||
// on failure update host, but don't trigger fronting enable.
|
||||
self.update_host(Some(url.clone()));
|
||||
|
||||
if attempts < self.retry_limit {
|
||||
attempts += 1;
|
||||
warn!(
|
||||
"Retrying request due to dns error on attempt ({attempts}/{}): {err}",
|
||||
self.retry_limit
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let response: Result<Response, HttpClientError> = {
|
||||
@@ -1067,25 +1125,39 @@ impl ApiClientCore for Client {
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(err) => {
|
||||
// if we have multiple urls, update to the next
|
||||
self.update_host();
|
||||
// only if there was a network issue should we consider updating the host info
|
||||
//
|
||||
// note: for now this includes DNS resolution failure, I am not sure how I would go about
|
||||
// segregating that based on the interface provided by request for errors.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let is_network_err = err.is_timeout();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let is_network_err = err.is_timeout() || err.is_connect();
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front {
|
||||
// If fronting is set to be enabled on error, enable domain fronting as we
|
||||
// have encountered an error.
|
||||
let was_enabled = front.is_enabled();
|
||||
front.retry_enable();
|
||||
if !was_enabled && front.is_enabled() {
|
||||
tracing::info!(
|
||||
"Domain fronting activated after connection failure: {err}",
|
||||
);
|
||||
if is_network_err {
|
||||
// if we have multiple urls, update to the next
|
||||
self.update_host(Some(url.clone()));
|
||||
|
||||
#[cfg(feature = "tunneling")]
|
||||
if let Some(ref front) = self.front {
|
||||
// If fronting is set to be enabled on error, enable domain fronting as we
|
||||
// have encountered an error.
|
||||
let was_enabled = front.is_enabled();
|
||||
front.retry_enable();
|
||||
if !was_enabled && front.is_enabled() {
|
||||
tracing::info!(
|
||||
"Domain fronting activated after connection failure: {err}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attempts < self.retry_limit {
|
||||
warn!("Retrying request due to http error: {err}");
|
||||
attempts += 1;
|
||||
warn!(
|
||||
"Retrying request due to http error on attempt ({attempts}/{}): {err}",
|
||||
self.retry_limit
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1094,7 +1166,7 @@ impl ApiClientCore for Client {
|
||||
if #[cfg(target_arch = "wasm32")] {
|
||||
return Err(err);
|
||||
} else {
|
||||
return Err(HttpClientError::request_send_error(url, err));
|
||||
return Err(HttpClientError::request_send_error(url.into(), err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,14 +91,15 @@ 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.test".parse()?, // This should fail because of DNS NXDomain (rotate)
|
||||
"http://127.0.0.1:9".parse()?, // This will fail because of TCP refused (rotate)
|
||||
"https://httpbin.org/status/200".parse()?, // This should succeed
|
||||
])?
|
||||
.with_retries(3)
|
||||
.build()?;
|
||||
|
||||
let req = client.create_get_request(&[], NO_PARAMS).unwrap();
|
||||
let resp = client.send(req).await?;
|
||||
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
|
||||
@@ -107,7 +108,9 @@ async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"https://httpbin.org/status/200"
|
||||
);
|
||||
|
||||
println!("Response status: {}", resp.status());
|
||||
// // This assert can be unreliable due to factors beyond our control and beyond the scope of
|
||||
// // this test
|
||||
// assert_eq!(_resp.status(), StatusCode::OK);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -123,7 +126,7 @@ fn host_updating() {
|
||||
assert_eq!(current_url.front_str(), None);
|
||||
|
||||
// update the url
|
||||
client.update_host();
|
||||
client.update_host(None);
|
||||
|
||||
// check that the url is still the same since there is one URL
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
@@ -138,13 +141,13 @@ fn host_updating() {
|
||||
client.change_base_urls(new_urls);
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
|
||||
client.update_host();
|
||||
client.update_host(None);
|
||||
|
||||
// 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().front_str(), None);
|
||||
|
||||
client.update_host();
|
||||
client.update_host(None);
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
|
||||
// =======================================
|
||||
@@ -162,12 +165,33 @@ fn host_updating() {
|
||||
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
|
||||
client.update_host();
|
||||
client.update_host(None);
|
||||
|
||||
// check that the url got updated now that there are multiple URLs
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_updating_url_conditioned() {
|
||||
let url1 = Url::new("http://nym-api1.test", None).unwrap();
|
||||
let url2 = Url::new("http://nym-api2.test", None).unwrap();
|
||||
let urls = vec![url1.clone(), url2.clone()];
|
||||
let client = ClientBuilder::new_with_urls(urls).unwrap().build().unwrap();
|
||||
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
|
||||
// Try to update with a URL that does NOT match current - should result in no change
|
||||
client.update_host(Some(Url::parse("http://example.com").unwrap()));
|
||||
|
||||
// check that the url did NOT get updated
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
|
||||
assert_eq!(client.current_url().front_str(), None);
|
||||
|
||||
// Try to update with a URL that DOES match current - should result in no change
|
||||
client.update_host(Some(url1));
|
||||
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "tunneling")]
|
||||
fn fronted_host_updating() {
|
||||
@@ -184,7 +208,7 @@ fn fronted_host_updating() {
|
||||
assert_eq!(current_url.front_str(), Some("cdn1.test"));
|
||||
|
||||
// update the url
|
||||
client.update_host();
|
||||
client.update_host(None);
|
||||
|
||||
// check that the url is still the same since there is one URL and one front
|
||||
let current_url = client.current_url();
|
||||
@@ -209,7 +233,7 @@ fn fronted_host_updating() {
|
||||
assert_eq!(current_url.front_str(), Some("cdn1.test"));
|
||||
|
||||
// update the url - this should keep the same host but change the front
|
||||
client.update_host();
|
||||
client.update_host(None);
|
||||
|
||||
let current_url = client.current_url();
|
||||
// check that the url is still the same since there is one URL
|
||||
@@ -217,7 +241,7 @@ fn fronted_host_updating() {
|
||||
assert_eq!(current_url.front_str(), Some("cdn2.test"));
|
||||
|
||||
// update the url - this should wrap around to the first front as the second url is not fronted
|
||||
client.update_host();
|
||||
client.update_host(None);
|
||||
|
||||
let current_url = client.current_url();
|
||||
assert_eq!(current_url.as_str(), "http://nym-api.test/");
|
||||
|
||||
@@ -54,6 +54,11 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
];
|
||||
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
|
||||
pub const UPGRADE_MODE_ATTESTATION_URL: &str = "https://nym.com/upgrade-mode/attestation.json";
|
||||
pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
|
||||
"3bgffBYcfFkTTXc2npNNn9MkddFZ3H2LrPjXDmnJzrqd";
|
||||
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
@@ -159,6 +164,14 @@ pub fn export_to_env() {
|
||||
set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
|
||||
set_var_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTATION_URL,
|
||||
UPGRADE_MODE_ATTESTATION_URL,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "env", feature = "network"))]
|
||||
@@ -199,4 +212,12 @@ pub fn export_to_env_if_not_set() {
|
||||
set_var_conditionally_to_default(var_names::NYM_API, NYM_API);
|
||||
set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
|
||||
set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
|
||||
set_var_conditionally_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTATION_URL,
|
||||
UPGRADE_MODE_ATTESTATION_URL,
|
||||
);
|
||||
set_var_conditionally_to_default(
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
|
||||
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
|
||||
pub const NYM_VPN_API: &str = "NYM_VPN_API";
|
||||
pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER";
|
||||
pub const UPGRADE_MODE_ATTESTATION_URL: &str = "UPGRADE_MODE_ATTESTATION_URL";
|
||||
pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str = "UPGRADE_MODE_ATTESTER_ED25519_PUBKEY";
|
||||
|
||||
pub const DKG_TIME_CONFIGURATION: &str = "DKG_TIME_CONFIGURATION";
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ pub struct EntryDetails {
|
||||
pub clients_wss_port: Option<u16>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EntryDetails {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct SupportedRoles {
|
||||
pub mixnode: bool,
|
||||
@@ -59,36 +65,6 @@ pub struct RoutingNode {
|
||||
}
|
||||
|
||||
impl RoutingNode {
|
||||
pub fn ws_entry_address_tls(&self) -> Option<String> {
|
||||
let entry = self.entry.as_ref()?;
|
||||
let hostname = entry.hostname.as_ref()?;
|
||||
let wss_port = entry.clients_wss_port?;
|
||||
|
||||
Some(format!("wss://{hostname}:{wss_port}"))
|
||||
}
|
||||
|
||||
pub fn ws_entry_address_no_tls(&self, prefer_ipv6: bool) -> Option<String> {
|
||||
let entry = self.entry.as_ref()?;
|
||||
|
||||
if let Some(hostname) = entry.hostname.as_ref() {
|
||||
return Some(format!("ws://{hostname}:{}", entry.clients_ws_port));
|
||||
}
|
||||
|
||||
if prefer_ipv6 && let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) {
|
||||
return Some(format!("ws://{ipv6}:{}", entry.clients_ws_port));
|
||||
}
|
||||
|
||||
let any_ip = entry.ip_addresses.first()?;
|
||||
Some(format!("ws://{any_ip}:{}", entry.clients_ws_port))
|
||||
}
|
||||
|
||||
pub fn ws_entry_address(&self, prefer_ipv6: bool) -> Option<String> {
|
||||
if let Some(tls) = self.ws_entry_address_tls() {
|
||||
return Some(tls);
|
||||
}
|
||||
self.ws_entry_address_no_tls(prefer_ipv6)
|
||||
}
|
||||
|
||||
pub fn identity(&self) -> ed25519::PublicKey {
|
||||
self.identity_key
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.68"
|
||||
version = "1.1.70"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
@@ -453,7 +453,7 @@ impl PacketPreparer {
|
||||
for gateway in &gateways_to_test_details {
|
||||
let recipient = self.create_packet_sender(gateway);
|
||||
let gateway_identity = gateway.identity_key;
|
||||
let gateway_address = gateway.ws_entry_address(false);
|
||||
let gateway_address = gateway.entry;
|
||||
|
||||
// the unwrap here is fine as:
|
||||
// 1. the topology is definitely valid (otherwise we wouldn't be here)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-credential-proxy"
|
||||
version = "0.3.0-test"
|
||||
version = "0.3.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -156,12 +156,8 @@ pub struct Cli {
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct UpgradeModeConfig {
|
||||
/// URL for polling for upgrade mode changes.
|
||||
#[clap(
|
||||
long,
|
||||
env = "NYM_CREDENTIAL_PROXY_ATTESTATION_CHECK_URL",
|
||||
default_value = "5m"
|
||||
)]
|
||||
pub(crate) attestation_check_url: Url,
|
||||
#[clap(long, env = "NYM_CREDENTIAL_PROXY_ATTESTATION_CHECK_URL")]
|
||||
pub(crate) attestation_check_url: Option<Url>,
|
||||
|
||||
/// Default polling interval of the upgrade mode endpoint.
|
||||
#[clap(
|
||||
|
||||
@@ -8,6 +8,8 @@ use nym_bin_common::bin_info;
|
||||
use nym_credential_proxy_lib::error::CredentialProxyError;
|
||||
use nym_credential_proxy_lib::storage::CredentialProxyStorage;
|
||||
use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager;
|
||||
use nym_network_defaults::var_names;
|
||||
use nym_network_defaults::var_names::CONFIGURED;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub async fn wait_for_signal() {
|
||||
@@ -55,6 +57,28 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> {
|
||||
let webhook_cfg = cli.webhook;
|
||||
let jwt_signing_keys = cli.jwt_signing_keys.signing_keys()?;
|
||||
|
||||
let upgrade_mode_attestation_check_url = match cli.upgrade_mode.attestation_check_url {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
// argument hasn't been provided and env is not configured
|
||||
if std::env::var(CONFIGURED).is_err() {
|
||||
return Err(CredentialProxyError::AttestationCheckUrlNotSet);
|
||||
}
|
||||
// argument hasn't been provided and the relevant env value hasn't been set
|
||||
// (technically this shouldn't be possible)
|
||||
let Ok(env_url) = std::env::var(var_names::UPGRADE_MODE_ATTESTATION_URL) else {
|
||||
return Err(CredentialProxyError::AttestationCheckUrlNotSet);
|
||||
};
|
||||
|
||||
match env_url.parse() {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
return Err(CredentialProxyError::MalformedAttestationCheckUrl { source: err });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ticketbook_manager = TicketbookManager::new(
|
||||
build_sha_short(),
|
||||
cli.quorum_check_interval,
|
||||
@@ -70,7 +94,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> {
|
||||
cli.upgrade_mode.attestation_check_regular_polling_interval,
|
||||
cli.upgrade_mode
|
||||
.attestation_check_expedited_polling_interval,
|
||||
cli.upgrade_mode.attestation_check_url,
|
||||
upgrade_mode_attestation_check_url,
|
||||
jwt_signing_keys,
|
||||
cli.upgrade_mode.upgrade_mode_jwt_validity,
|
||||
);
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node"
|
||||
version = "1.20.0"
|
||||
version = "1.22.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient};
|
||||
use nym_authenticator_client::{
|
||||
AuthClientMixnetListener, AuthClientMixnetListenerHandle, AuthenticatorClient,
|
||||
};
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_ip_packet_client::IprClientConnect;
|
||||
@@ -35,9 +37,22 @@ pub struct RegistrationClient {
|
||||
event_rx: EventReceiver,
|
||||
}
|
||||
|
||||
enum MixnetClientHandle {
|
||||
Authenticator(AuthClientMixnetListenerHandle),
|
||||
Sdk(Box<MixnetClient>),
|
||||
}
|
||||
|
||||
impl MixnetClientHandle {
|
||||
async fn stop(self) {
|
||||
match self {
|
||||
Self::Authenticator(handle) => handle.stop().await,
|
||||
Self::Sdk(handle) => handle.disconnect().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bundle of an actual error and the underlying mixnet client so it can be shutdown correctly if needed
|
||||
struct RegistrationError {
|
||||
mixnet_client: Option<MixnetClient>,
|
||||
mixnet_client_handle: MixnetClientHandle,
|
||||
source: crate::RegistrationClientError,
|
||||
}
|
||||
|
||||
@@ -49,7 +64,7 @@ impl RegistrationClient {
|
||||
|
||||
let Some(ipr_address) = self.config.exit.node.ipr_address else {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client: Some(self.mixnet_client),
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(self.mixnet_client)),
|
||||
source: RegistrationClientError::NoIpPacketRouterAddress {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
@@ -67,13 +82,17 @@ impl RegistrationClient {
|
||||
Some(Ok(addr)) => addr,
|
||||
Some(Err(e)) => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client: Some(ipr_client.into_mixnet_client()),
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(
|
||||
ipr_client.into_mixnet_client(),
|
||||
)),
|
||||
source: RegistrationClientError::ConnectToIpPacketRouter(e),
|
||||
});
|
||||
}
|
||||
None => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client: Some(ipr_client.into_mixnet_client()),
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(
|
||||
ipr_client.into_mixnet_client(),
|
||||
)),
|
||||
source: RegistrationClientError::Cancelled,
|
||||
});
|
||||
}
|
||||
@@ -97,7 +116,7 @@ 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),
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(self.mixnet_client)),
|
||||
source: RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.entry.node.identity.to_base58_string(),
|
||||
},
|
||||
@@ -106,7 +125,7 @@ impl RegistrationClient {
|
||||
|
||||
let Some(exit_auth_address) = self.config.exit.node.authenticator_address else {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client: Some(self.mixnet_client),
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(self.mixnet_client)),
|
||||
source: RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
@@ -150,35 +169,50 @@ impl RegistrationClient {
|
||||
let exit_fut = exit_auth_client
|
||||
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardExit);
|
||||
|
||||
let (entry, exit) = Box::pin(
|
||||
let (entry, exit) = match 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,
|
||||
})?;
|
||||
{
|
||||
Some((entry, exit)) => (entry, exit),
|
||||
None => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Authenticator(mixnet_listener),
|
||||
source: RegistrationClientError::Cancelled,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(source) => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Authenticator(mixnet_listener),
|
||||
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 exit = match exit {
|
||||
Ok(exit) => exit,
|
||||
Err(source) => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Authenticator(mixnet_listener),
|
||||
source: RegistrationClientError::from_authenticator_error(
|
||||
source,
|
||||
self.config.exit.node.identity.to_base58_string(),
|
||||
exit_auth_address,
|
||||
false,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(RegistrationResult::Wireguard(Box::new(
|
||||
WireguardRegistrationResult {
|
||||
@@ -199,15 +233,14 @@ impl RegistrationClient {
|
||||
self.register_mix_exit().await
|
||||
};
|
||||
|
||||
// If we failed to register, and we were the owner of the mixnet client, shut it down
|
||||
// If we failed to register, shut down the mixnet client and wait for it to exit
|
||||
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;
|
||||
}
|
||||
debug!("Shutting down mixnet client");
|
||||
error.mixnet_client_handle.stop().await;
|
||||
debug!("Mixnet client stopped");
|
||||
Err(error.source)
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
-2
@@ -2864,9 +2864,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a128410b38d6f931fcc6ca5c107a3b02cabd6c05967841269a4ad65d23c44331"
|
||||
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -4219,6 +4219,7 @@ dependencies = [
|
||||
"serde_plain",
|
||||
"serde_yaml",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"wasmtimer",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-network-requester"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.66"
|
||||
version = "1.1.68"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version = "1.85"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-cli"
|
||||
version = "1.1.65"
|
||||
version = "1.1.67"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nymvisor"
|
||||
version = "0.1.30"
|
||||
version = "0.1.32"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
Reference in New Issue
Block a user