diff --git a/Cargo.lock b/Cargo.lock index 7ec3531bb6d..391735e4de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6432,6 +6432,7 @@ dependencies = [ "futures", "hkdf", "human-repr", + "humantime", "humantime-serde", "indicatif", "ipnetwork", diff --git a/gateway/src/node/internal_service_providers/authenticator/config/mod.rs b/gateway/src/node/internal_service_providers/authenticator/config/mod.rs index 1a1e5423687..404e770631a 100644 --- a/gateway/src/node/internal_service_providers/authenticator/config/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/config/mod.rs @@ -57,6 +57,10 @@ pub struct Authenticator { /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. /// The maximum value for IPv6 is 128 pub private_network_prefix_v6: u8, + + /// Timeout (in milliseconds) to wait for responses from the peer controller before failing. + /// Helps the authenticator recover from suspend/resume scenarios where peer RPCs hang. + pub peer_interaction_timeout_ms: u64, } impl Default for Authenticator { @@ -68,6 +72,7 @@ impl Default for Authenticator { tunnel_announced_port: WG_TUNNEL_PORT, private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, + peer_interaction_timeout_ms: default_peer_interaction_timeout_ms(), } } } @@ -85,3 +90,7 @@ impl From for nym_wireguard_types::Config { } } } + +pub const fn default_peer_interaction_timeout_ms() -> u64 { + 5_000 +} diff --git a/gateway/src/node/internal_service_providers/authenticator/error.rs b/gateway/src/node/internal_service_providers/authenticator/error.rs index 5bdde159194..8f5de74508b 100644 --- a/gateway/src/node/internal_service_providers/authenticator/error.rs +++ b/gateway/src/node/internal_service_providers/authenticator/error.rs @@ -85,6 +85,9 @@ pub enum AuthenticatorError { #[error("peers can't be interacted with anymore")] PeerInteractionStopped, + #[error("peers interaction timed out while attempting to {operation}")] + PeerInteractionTimeout { operation: &'static str }, + #[error("unknown version number")] UnknownVersion, diff --git a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs index c05d9d8cd6f..3913452cb87 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs @@ -42,7 +42,6 @@ use std::{ sync::Arc, time::{Duration, SystemTime}, }; -use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; type AuthenticatorHandleResult = Result<(Vec, Option), AuthenticatorError>; @@ -74,7 +73,7 @@ pub(crate) struct MixnetListener { pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient, // Registrations awaiting confirmation - pub(crate) registered_and_free: RwLock, + pub(crate) registered_and_free: RegisteredAndFree, pub(crate) peer_manager: PeerManager, @@ -95,14 +94,15 @@ impl MixnetListener { mixnet_client: nym_sdk::mixnet::MixnetClient, upgrade_mode: UpgradeModeDetails, ecash_verifier: Arc, + peer_interaction_timeout: Duration, ) -> Self { let timeout_check_interval = IntervalStream::new(tokio::time::interval(DEFAULT_REGISTRATION_TIMEOUT_CHECK)); MixnetListener { config, mixnet_client, - registered_and_free: RwLock::new(RegisteredAndFree::new(free_private_network_ips)), - peer_manager: PeerManager::new(wireguard_gateway_data), + registered_and_free: RegisteredAndFree::new(free_private_network_ips), + peer_manager: PeerManager::new(wireguard_gateway_data, peer_interaction_timeout), upgrade_mode, ecash_verifier, timeout_check_interval, @@ -131,8 +131,8 @@ impl MixnetListener { )) } - async fn remove_stale_registrations(&self) -> Result<(), AuthenticatorError> { - let mut registered_and_free = self.registered_and_free.write().await; + async fn remove_stale_registrations(&mut self) -> Result<(), AuthenticatorError> { + let registered_and_free = &mut self.registered_and_free; let registered_values: Vec<_> = registered_and_free .registration_in_progres .values() @@ -185,8 +185,9 @@ impl MixnetListener { ) -> AuthenticatorHandleResult { let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); - let mut registered_and_free = self.registered_and_free.write().await; - if let Some(registration_data) = registered_and_free + + if let Some(registration_data) = self + .registered_and_free .registration_in_progres .get(&remote_public) { @@ -292,7 +293,17 @@ impl MixnetListener { return Ok((bytes, reply_to)); } - let peer = self.peer_manager.query_peer(remote_public).await?; + let peer = match self.peer_manager.query_peer(remote_public).await { + Ok(peer) => peer, + Err(err) => { + tracing::warn!( + "Failed to query peer {}: {err}. Continuing with fresh registration", + remote_public + ); + None + } + }; + if let Some(peer) = peer { let allowed_ipv4 = peer .allowed_ips @@ -383,27 +394,33 @@ impl MixnetListener { return Ok((bytes, reply_to)); } - let private_ip_ref = registered_and_free - .free_private_network_ips - .iter_mut() - .filter(|r| r.1.is_none()) - .choose(&mut thread_rng()) - .ok_or(AuthenticatorError::NoFreeIp)?; - let private_ips = *private_ip_ref.0; - // mark it as used, even though it's not final - *private_ip_ref.1 = Some(SystemTime::now()); - let gateway_data = GatewayClient::new( - self.keypair().private_key(), - remote_public.inner(), - *private_ip_ref.0, - nonce, - ); - let registration_data = latest::registration::RegistrationData { - nonce, - gateway_data: gateway_data.clone(), - wg_port: self.config.authenticator.tunnel_announced_port, + let (registration_data, private_ips) = { + let private_ip = self + .registered_and_free + .free_private_network_ips + .iter_mut() + .filter(|r| r.1.is_none()) + .choose(&mut thread_rng()) + .ok_or(AuthenticatorError::NoFreeIp)?; + let private_ips = *private_ip.0; + // mark it as used, even though it's not final + *private_ip.1 = Some(SystemTime::now()); + + let gateway_data = GatewayClient::new( + self.keypair().private_key(), + remote_public.inner(), + private_ips, + nonce, + ); + let registration_data = latest::registration::RegistrationData { + nonce, + gateway_data: gateway_data.clone(), + wg_port: self.config.authenticator.tunnel_announced_port, + }; + (registration_data, private_ips) }; - registered_and_free + + self.registered_and_free .registration_in_progres .insert(remote_public, registration_data.clone()); let bytes = match AuthenticatorVersion::from(protocol) { @@ -539,12 +556,12 @@ impl MixnetListener { request_id: u64, reply_to: Option, ) -> AuthenticatorHandleResult { - let mut registered_and_free = self.registered_and_free.write().await; - let registration_data = registered_and_free + let registration_data = self + .registered_and_free .registration_in_progres .get(&final_message.gateway_client_pub_key()) - .ok_or(AuthenticatorError::RegistrationNotInProgress)? - .clone(); + .cloned() + .ok_or(AuthenticatorError::RegistrationNotInProgress)?; if final_message .verify(self.keypair().private_key(), registration_data.nonce) @@ -595,7 +612,7 @@ impl MixnetListener { return Err(e); } - registered_and_free + self.registered_and_free .registration_in_progres .remove(&final_message.gateway_client_pub_key()); @@ -818,7 +835,7 @@ impl MixnetListener { .to_bytes() .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { - return Err(AuthenticatorError::UnknownVersion) + return Err(AuthenticatorError::UnknownVersion); } }; diff --git a/gateway/src/node/internal_service_providers/authenticator/mod.rs b/gateway/src/node/internal_service_providers/authenticator/mod.rs index f63a86fcc2d..506dd11de87 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mod.rs @@ -151,6 +151,7 @@ impl Authenticator { } }) .collect(); + let peer_timeout = std::cmp::max(1, self.config.authenticator.peer_interaction_timeout_ms); let mixnet_listener = crate::node::internal_service_providers::authenticator::mixnet_listener::MixnetListener::new( self.config, free_private_network_ips, @@ -158,6 +159,7 @@ impl Authenticator { mixnet_client, self.upgrade_mode_state, self.ecash_verifier, + std::time::Duration::from_millis(peer_timeout), ); tracing::info!("The address of this client is: {self_address}"); diff --git a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs index c057dfa57cc..85130380fe3 100644 --- a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs +++ b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs @@ -8,15 +8,19 @@ use nym_credential_verification::{ClientBandwidth, TicketVerifier}; use nym_credentials_interface::CredentialSpendingData; use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData}; use nym_wireguard_types::PeerPublicKey; +use std::time::Duration; +use tokio::time::timeout; pub struct PeerManager { pub(crate) wireguard_gateway_data: WireguardGatewayData, + response_timeout: Duration, } impl PeerManager { - pub fn new(wireguard_gateway_data: WireguardGatewayData) -> Self { + pub fn new(wireguard_gateway_data: WireguardGatewayData, response_timeout: Duration) -> Self { PeerManager { wireguard_gateway_data, + response_timeout, } } pub async fn add_peer(&self, peer: Peer) -> Result<(), AuthenticatorError> { @@ -28,9 +32,8 @@ impl PeerManager { .await .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - response_rx - .await - .map_err(|_| AuthenticatorError::InternalError("no response for add peer".to_string()))? + recv_with_timeout(response_rx, "add peer", self.response_timeout) + .await? .map_err(|err| { AuthenticatorError::InternalError(format!( "adding peer could not be performed: {err:?}" @@ -48,11 +51,8 @@ impl PeerManager { .await .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - response_rx - .await - .map_err(|_| { - AuthenticatorError::InternalError("no response for remove peer".to_string()) - })? + recv_with_timeout(response_rx, "remove peer", self.response_timeout) + .await? .map_err(|err| { AuthenticatorError::InternalError(format!( "removing peer could not be performed: {err:?}" @@ -73,11 +73,8 @@ impl PeerManager { .await .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - response_rx - .await - .map_err(|_| { - AuthenticatorError::InternalError("no response for query peer".to_string()) - })? + recv_with_timeout(response_rx, "query peer", self.response_timeout) + .await? .map_err(|err| { AuthenticatorError::InternalError(format!( "querying peer could not be performed: {err:?}" @@ -106,13 +103,8 @@ impl PeerManager { .await .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - response_rx - .await - .map_err(|_| { - AuthenticatorError::InternalError( - "no response for query client bandwidth".to_string(), - ) - })? + recv_with_timeout(response_rx, "query client bandwidth", self.response_timeout) + .await? .map_err(|err| { AuthenticatorError::InternalError(format!( "querying client bandwidth could not be performed: {err:?}" @@ -138,11 +130,8 @@ impl PeerManager { .await .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - response_rx - .await - .map_err(|_| { - AuthenticatorError::InternalError("no response for query verifier".to_string()) - })? + recv_with_timeout(response_rx, "query verifier", self.response_timeout) + .await? .map_err(|err| { AuthenticatorError::InternalError(format!( "querying verifier could not be performed: {err:?}" @@ -151,10 +140,31 @@ impl PeerManager { } } +async fn recv_with_timeout( + response_rx: oneshot::Receiver, + operation: &'static str, + timeout_duration: Duration, +) -> Result { + // Suspend/resume can wedge the peer controller, so we bound the wait to avoid deadlocking + // authenticator responses on a stuck oneshot channel. + match timeout(timeout_duration, response_rx).await { + Ok(Ok(value)) => Ok(value), + Ok(Err(_)) => Err(AuthenticatorError::PeerInteractionStopped), + Err(_) => { + tracing::warn!( + "peer controller response timed out while attempting to {operation} after {:?}", + timeout_duration + ); + Err(AuthenticatorError::PeerInteractionTimeout { operation }) + } + } +} + #[cfg(test)] mod tests { use std::{str::FromStr, sync::Arc}; + use futures::channel::oneshot; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ecash::MockEcashManager, }; @@ -163,7 +173,8 @@ mod tests { use nym_gateway_storage::traits::{mock::MockGatewayStorage, BandwidthGatewayStorage}; use nym_wireguard::peer_controller::{start_controller, stop_controller}; use rand::rngs::OsRng; - use time::{Duration, OffsetDateTime}; + use std::time::Duration; + use time::{Duration as TimeDuration, OffsetDateTime}; use tokio::sync::RwLock; use crate::nym_authenticator::{ @@ -243,7 +254,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let peer_manager = PeerManager::new(wireguard_data); + let peer_manager = PeerManager::new(wireguard_data, Duration::from_secs(5)); let (storage, task_manager) = start_controller( peer_manager.wireguard_gateway_data.peer_tx().clone(), request_rx, @@ -291,7 +302,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let mut peer_manager = PeerManager::new(wireguard_data, Duration::from_secs(5)); let key = Key::default(); let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); let (storage, task_manager) = start_controller( @@ -311,7 +322,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let mut peer_manager = PeerManager::new(wireguard_data, Duration::from_secs(5)); let key = Key::default(); let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); let (storage, task_manager) = start_controller( @@ -334,7 +345,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let mut peer_manager = PeerManager::new(wireguard_data, Duration::from_secs(5)); let key = Key::default(); let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); let (storage, task_manager) = start_controller( @@ -357,7 +368,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let mut peer_manager = PeerManager::new(wireguard_data, Duration::from_secs(5)); let key = Key::default(); let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); let (storage, task_manager) = start_controller( @@ -388,7 +399,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let mut peer_manager = PeerManager::new(wireguard_data, Duration::from_secs(5)); let key = Key::default(); let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); let (storage, task_manager) = start_controller( @@ -417,7 +428,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let mut peer_manager = PeerManager::new(wireguard_data, Duration::from_secs(5)); let key = Key::default(); let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); let top_up = 42; @@ -444,7 +455,7 @@ mod tests { .increase_bandwidth( Bandwidth::new_unchecked(top_up as u64), OffsetDateTime::now_utc() - .checked_add(Duration::minutes(1)) + .checked_add(TimeDuration::minutes(1)) .unwrap(), ) .await @@ -466,4 +477,28 @@ mod tests { stop_controller(task_manager).await; } + + #[tokio::test] + async fn recv_with_timeout_errors_after_deadline() { + let (_tx, rx) = oneshot::channel::<()>(); + let err = super::recv_with_timeout(rx, "unit-test", Duration::from_millis(10)) + .await + .unwrap_err(); + assert!(matches!( + err, + AuthenticatorError::PeerInteractionTimeout { + operation: "unit-test" + } + )); + } + + #[tokio::test] + async fn recv_with_timeout_succeeds_before_deadline() { + let (tx, rx) = oneshot::channel::(); + tx.send(42).unwrap(); + let value = super::recv_with_timeout(rx, "unit-test", Duration::from_secs(1)) + .await + .unwrap(); + assert_eq!(value, 42); + } } diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index f431886ebfd..6ce6e796b1f 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -27,6 +27,7 @@ console-subscriber = { workspace = true, optional = true } csv = { workspace = true } clap = { workspace = true, features = ["cargo", "env"] } futures = { workspace = true } +humantime = { workspace = true } humantime-serde = { workspace = true } human-repr = { workspace = true } ipnetwork = { workspace = true } diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 08ccef0857b..15e3fc9ee9f 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -9,6 +9,7 @@ use crate::error::NymNodeError; use celes::Country; use clap::Args; use clap::builder::ArgPredicate; +use humantime::Duration as HumanDuration; use nym_crypto::asymmetric::ed25519; use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; @@ -286,6 +287,13 @@ pub(crate) struct WireguardArgs { )] pub(crate) wireguard_tunnel_announced_port: Option, + /// Timeout to wait for responses from the peer controller before aborting the operation. + #[clap( + long, + env = NYMNODE_WG_PEER_INTERACTION_TIMEOUT_MS_ARG + )] + pub(crate) wireguard_peer_interaction_timeout: Option, + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard. /// The maximum value for IPv4 is 32 and for IPv6 is 128 #[clap( @@ -317,6 +325,10 @@ impl WireguardArgs { section.announced_tunnel_port = announced_tunnel_port } + if let Some(timeout) = self.wireguard_peer_interaction_timeout { + section.peer_interaction_timeout_ms = timeout.into(); + } + if let Some(private_network_prefix) = self.wireguard_private_network_prefix { section.private_network_prefix_v4 = private_network_prefix } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 9605302aa20..fd84a58cb39 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -213,6 +213,7 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { private_network_prefix_v4: config.wireguard.private_network_prefix_v4, private_network_prefix_v6: config.wireguard.private_network_prefix_v6, storage_paths: config.wireguard.storage_paths.clone(), + peer_interaction_timeout_ms: config.wireguard.peer_interaction_timeout_ms, }, custom_mixnet_path: None, }; diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 08b578760ef..4dfe605263c 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -23,6 +23,7 @@ use nym_config::{ }; use nym_gateway::nym_authenticator; use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; use std::env; use std::fmt::{Display, Formatter}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -944,6 +945,7 @@ pub struct Wireguard { /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. + #[serde(alias = "announced_port")] pub announced_tunnel_port: u16, /// Metadata port announced to external clients wishing to connect to the metadata endpoint. @@ -960,6 +962,14 @@ pub struct Wireguard { /// Paths for wireguard keys, client registries, etc. pub storage_paths: persistence::WireguardPaths, + + /// Timeout that limits how long the authenticator waits for the peer controller to reply. + /// Accepts standard humantime values, e.g. `5000ms`, `5s`, `1m`. + #[serde( + default = "Wireguard::default_peer_interaction_timeout", + with = "humantime_serde" + )] + pub peer_interaction_timeout_ms: Duration, } impl Wireguard { @@ -974,8 +984,13 @@ impl Wireguard { private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, storage_paths: persistence::WireguardPaths::new(data_dir), + peer_interaction_timeout_ms: Self::default_peer_interaction_timeout(), } } + + pub fn default_peer_interaction_timeout() -> Duration { + Duration::from_millis(nym_authenticator::config::default_peer_interaction_timeout_ms()) + } } impl From for nym_wireguard_types::Config { @@ -994,6 +1009,9 @@ impl From for nym_wireguard_types::Config { impl From for nym_authenticator::config::Authenticator { fn from(value: Wireguard) -> Self { + let timeout_ms_u128 = value.peer_interaction_timeout_ms.as_millis(); + let timeout_ms = u64::try_from(timeout_ms_u128).unwrap_or(u64::MAX).max(1); + nym_authenticator::config::Authenticator { bind_address: value.bind_address, private_ipv4: value.private_ipv4, @@ -1001,6 +1019,7 @@ impl From for nym_authenticator::config::Authenticator { tunnel_announced_port: value.announced_tunnel_port, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, + peer_interaction_timeout_ms: timeout_ms, } } } diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs index e45cca8dd21..ec7db9bfe8f 100644 --- a/nym-node/src/config/old_configs/old_config_v10.rs +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -20,6 +20,7 @@ use crate::config::{ use crate::error::NymNodeError; use celes::Country; use clap::ValueEnum; +use humantime::parse_duration; use nym_bin_common::logging::LoggingSettings; use nym_client_core_config_types::DebugConfig as ClientDebugConfig; use nym_config::defaults::{DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT}; @@ -29,7 +30,9 @@ use nym_config::{ read_config_from_toml_file, serde_helpers::{de_maybe_port, de_maybe_stringified}, }; +use serde::de::{Deserializer, Error as SerdeDeError}; use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -63,6 +66,7 @@ pub struct WireguardV10 { /// Port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. + #[serde(alias = "announced_tunnel_port")] pub announced_port: u16, /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. @@ -75,6 +79,11 @@ pub struct WireguardV10 { /// Paths for wireguard keys, client registries, etc. pub storage_paths: WireguardPathsV10, + + /// Optional override for the peer interaction timeout expressed in milliseconds. + /// Accepts either a plain integer value or a humantime string such as "5s". + #[serde(default, deserialize_with = "deserialize_optional_duration_ms")] + pub peer_interaction_timeout_ms: Option, } // a temporary solution until all "types" are run at the same time @@ -1334,6 +1343,11 @@ pub async fn try_upgrade_config_v10>( .storage_paths .public_diffie_hellman_key_file, }, + peer_interaction_timeout_ms: old_cfg + .wireguard + .peer_interaction_timeout_ms + .map(Duration::from_millis) + .unwrap_or_else(Wireguard::default_peer_interaction_timeout), }, gateway_tasks: GatewayTasksConfig { storage_paths: GatewayTasksPaths { @@ -1573,3 +1587,31 @@ pub async fn try_upgrade_config_v10>( }; Ok(cfg) } + +fn deserialize_optional_duration_ms<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum MaybeDuration { + Millis(u64), + Human(String), + } + + let maybe_value = Option::::deserialize(deserializer)?; + let Some(value) = maybe_value else { + return Ok(None); + }; + + match value { + MaybeDuration::Millis(ms) => Ok(Some(ms)), + MaybeDuration::Human(text) => { + let duration = + parse_duration(&text).map_err(|err| D::Error::custom(err.to_string()))?; + let millis = u64::try_from(duration.as_millis()) + .map_err(|_| D::Error::custom("duration too large"))?; + Ok(Some(millis)) + } + } +} diff --git a/nym-node/src/config/old_configs/old_config_v9.rs b/nym-node/src/config/old_configs/old_config_v9.rs index d14b89f31ad..c50b0ea4935 100644 --- a/nym-node/src/config/old_configs/old_config_v9.rs +++ b/nym-node/src/config/old_configs/old_config_v9.rs @@ -1348,6 +1348,7 @@ pub async fn try_upgrade_config_v9>( .storage_paths .public_diffie_hellman_key_file, }, + peer_interaction_timeout_ms: None, }, gateway_tasks: GatewayTasksConfigV10 { storage_paths: GatewayTasksPathsV10 { diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index 8177119780d..624be956cf7 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -151,6 +151,10 @@ announced_tunnel_port = {{ wireguard.announced_tunnel_port }} # Useful in the instances where the node is behind a proxy. announced_metadata_port = {{ wireguard.announced_metadata_port }} +# Timeout to wait for responses from the peer controller before aborting the operation. +# Accepts standard humantime duration strings such as `200ms`, `5s`. +peer_interaction_timeout_ms = '{{ wireguard.peer_interaction_timeout_ms }}' + # The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. # The maximum value for IPv4 is 32 private_network_prefix_v4 = {{ wireguard.private_network_prefix_v4 }} diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 1564d087a43..670eb37caef 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -47,6 +47,8 @@ pub mod vars { pub const NYMNODE_WG_BIND_ADDRESS_ARG: &str = "NYMNODE_WG_BIND_ADDRESS"; pub const NYMNODE_WG_ANNOUNCED_PORT_ARG: &str = "NYMNODE_WG_ANNOUNCED_PORT"; pub const NYMNODE_WG_PRIVATE_NETWORK_PREFIX_ARG: &str = "NYMNODE_WG_PRIVATE_NETWORK_PREFIX"; + pub const NYMNODE_WG_PEER_INTERACTION_TIMEOUT_MS_ARG: &str = + "NYMNODE_WG_PEER_INTERACTION_TIMEOUT_MS"; // verloc: pub const NYMNODE_VERLOC_BIND_ADDRESS_ARG: &str = "NYMNODE_VERLOC_BIND_ADDRESS";