Compare commits

..

10 Commits

Author SHA1 Message Date
benedettadavico 22793bc45e update changelog 2025-11-25 15:16:42 +01:00
Simon Wicky 37f3ef58a3 [bugfix] Tunnel not waiting on MixnetClient to shut down cleanly (#6225)
* return the handlefor a clean shutdown

* cargo lock
2025-11-21 16:39:12 +01:00
benedettadavico 1a4d64a0e5 bump versions 2025-11-14 11:23:47 +01:00
benedetta davico 4dcc568ec2 Merge pull request #6204 from nymtech/master
merging master to develop to maintain sync
2025-11-14 11:21:13 +01:00
benedetta davico 468835e3a2 Merge pull request #6199 from nymtech/release/2025.20-leerdammer
Release/2025.20 leerdammer
2025-11-14 10:36:23 +01:00
benedetta davico 28a866e26d Merge pull request #6198 from nymtech/release/2025.20-leerdammer
Release/2025.20 leerdammer
2025-11-14 10:36:11 +01:00
Jędrzej Stuczyński 350d244032 bugfix: fix credential proxy upgrade mode attestation url arg (#6202)
this includes bringing over changes introduced in #6174
2025-11-14 08:19:21 +00:00
Jack Wampler 17ca000782 HTTP API resilience enable & domain rotation conditions (#6200)
* http url fallback conditions

* include changes and tests for fronted

* Allow for explicit DNS error Handling in HTTP client  (#6201)

when sending http reqs add manual DNS so we can handle errors directly

* Address PR nits

---------

Co-authored-by: durch <durch@users.noreply.github.com>
2025-11-14 08:59:36 +01:00
benedettadavico babf113fe5 update changelog 2025-11-12 08:39:48 +01:00
benedettadavico 0d7487f530 bump versions 2025-10-31 13:24:27 +01:00
22 changed files with 413 additions and 185 deletions
+54
View File
@@ -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
+8 -8
View File
@@ -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",
@@ -5443,7 +5443,7 @@ dependencies = [
[[package]]
name = "nym-credential-proxy"
version = "0.3.0-test"
version = "0.3.0"
dependencies = [
"anyhow",
"axum",
@@ -6363,7 +6363,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.66"
version = "1.1.68"
dependencies = [
"addr",
"anyhow",
@@ -6413,7 +6413,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.20.0"
version = "1.22.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6940,7 +6940,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.65"
version = "1.1.67"
dependencies = [
"bs58",
"clap",
@@ -7681,7 +7681,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.30"
version = "0.1.32"
dependencies = [
"anyhow",
"bytes",
+1 -1
View File
@@ -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 -1
View File
@@ -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"
+8
View File
@@ -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 {
+1 -100
View File
@@ -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}"),
}
+7
View File
@@ -88,6 +88,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
+85
View File
@@ -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);
}
}
+88 -16
View File
@@ -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));
}
}
}
+35 -11
View File
@@ -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 will fail because of DNS (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/");
+21
View File
@@ -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,
);
}
+2
View File
@@ -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";
+1 -1
View File
@@ -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
@@ -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
View File
@@ -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
+68 -35
View File
@@ -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)
}
}
+1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nymvisor"
version = "0.1.30"
version = "0.1.32"
authors.workspace = true
repository.workspace = true
homepage.workspace = true