feat: upgrade mode: VPN adjustments (#6189)
* placeholder handling of wg registration with upgrade mode token * include upgrade mode credentials as part of credential storage * introduce helper for decoding JWT payload * expose methods for removing emergency credentials from the storage * don't allow duplicate emergency credentials with the same content * added authenticator ClientMessage for upgrade mode check * retrieve credentials with longest expiration first * post rebasing fixes * fixed gateway config * feat: allow specifying minimum node performance for client init * nym-node UM improvements * fixed upgrade mode bandwidth on initial authentication * fix: logs and thresholds * expose attestation information from nym-node http api * additional logs * post rebasing fixes * make @simonwicky happy by removing empty lines in emergency_credential table definition * chore: remove '_' prefix for internal counters within in-mem ecash storage * improved import of 'UpgradeModeState' within the nym-node * use explicit time dependency within credential-storage * re-order imports within the gateway-client * moved 'AvailableBandwidth' definition to the monorepo
This commit is contained in:
committed by
GitHub
parent
6b2bb3029b
commit
d126d8e5a0
Generated
+4
-6
@@ -5005,21 +5005,16 @@ name = "nym-bandwidth-controller"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bip39",
|
||||
"log",
|
||||
"nym-credential-storage",
|
||||
"nym-credentials",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-ecash-contract-common",
|
||||
"nym-ecash-time",
|
||||
"nym-network-defaults",
|
||||
"nym-task",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
"thiserror 2.0.12",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5572,6 +5567,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"sqlx-pool-guard",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -6443,6 +6439,7 @@ dependencies = [
|
||||
"nym-bin-common",
|
||||
"nym-client-core-config-types",
|
||||
"nym-config",
|
||||
"nym-credential-verification",
|
||||
"nym-crypto",
|
||||
"nym-gateway",
|
||||
"nym-gateway-stats-storage",
|
||||
@@ -6515,13 +6512,13 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"celes",
|
||||
"humantime",
|
||||
"humantime-serde",
|
||||
"nym-bin-common",
|
||||
"nym-crypto",
|
||||
"nym-exit-policy",
|
||||
"nym-http-api-client",
|
||||
"nym-noise-keys",
|
||||
"nym-upgrade-mode-check",
|
||||
"nym-wireguard-types",
|
||||
"rand_chacha 0.3.1",
|
||||
"schemars 0.8.22",
|
||||
@@ -6532,6 +6529,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ use nym_wireguard_types::PeerPublicKey;
|
||||
|
||||
use crate::{
|
||||
AuthenticatorVersion, Error,
|
||||
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable},
|
||||
traits::{
|
||||
FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage,
|
||||
Versionable,
|
||||
},
|
||||
v2, v3, v4, v5, v6,
|
||||
};
|
||||
|
||||
@@ -18,6 +21,7 @@ pub enum ClientMessage {
|
||||
Final(Box<dyn FinalMessage + Send + Sync + 'static>),
|
||||
Query(Box<dyn QueryBandwidthMessage + Send + Sync + 'static>),
|
||||
TopUp(Box<dyn TopUpMessage + Send + Sync + 'static>),
|
||||
UpgradeModeCheck(Box<dyn UpgradeModeMessage + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
pub struct SerialisedRequest {
|
||||
@@ -131,6 +135,7 @@ impl ClientMessage {
|
||||
);
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
_ => Err(Error::UnsupportedMessage),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +194,7 @@ impl ClientMessage {
|
||||
);
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
_ => Err(Error::UnsupportedMessage),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +243,7 @@ impl ClientMessage {
|
||||
});
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
_ => Err(Error::UnsupportedMessage),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +252,7 @@ impl ClientMessage {
|
||||
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair},
|
||||
request::AuthenticatorRequest,
|
||||
topup::TopUpMessage,
|
||||
upgrade_mode_check::UpgradeModeCheckRequest,
|
||||
};
|
||||
match self {
|
||||
ClientMessage::Initial(init_message) => {
|
||||
@@ -282,6 +290,22 @@ impl ClientMessage {
|
||||
});
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
ClientMessage::UpgradeModeCheck(upgrade_mode_check) => {
|
||||
// currently JWT is the only emergency credential option
|
||||
let Some(upgrade_mode_jwt) =
|
||||
upgrade_mode_check.upgrade_mode_global_attestation_jwt()
|
||||
else {
|
||||
return Err(Error::conversion(
|
||||
"no valid known upgrade mode check variants",
|
||||
));
|
||||
};
|
||||
let msg = UpgradeModeCheckRequest::UpgradeModeJwt {
|
||||
token: upgrade_mode_jwt,
|
||||
};
|
||||
|
||||
let (req, id) = AuthenticatorRequest::new_upgrade_mode_check_request(msg);
|
||||
Ok(SerialisedRequest::new(req.to_bytes()?, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +316,7 @@ impl ClientMessage {
|
||||
match self {
|
||||
Self::Final(msg) => msg.credential().is_some(),
|
||||
Self::TopUp(_) => true,
|
||||
Self::Initial(_) | Self::Query(_) => false,
|
||||
Self::Initial(_) | Self::Query(_) | Self::UpgradeModeCheck(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +326,7 @@ impl ClientMessage {
|
||||
ClientMessage::Final(msg) => msg.version(),
|
||||
ClientMessage::Query(msg) => msg.version(),
|
||||
ClientMessage::TopUp(msg) => msg.version(),
|
||||
ClientMessage::UpgradeModeCheck(msg) => msg.version(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ pub enum CurrentUpgradeModeStatus {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl CurrentUpgradeModeStatus {
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(self, CurrentUpgradeModeStatus::Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for CurrentUpgradeModeStatus {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
|
||||
@@ -7,21 +7,16 @@ license.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
bip39 = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
nym-credential-storage = { path = "../credential-storage" }
|
||||
nym-credentials = { path = "../credentials" }
|
||||
nym-credentials-interface = { path = "../credentials-interface" }
|
||||
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "stream_cipher", "aes", "hashing"] }
|
||||
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-time = { path = "../ecash-time" }
|
||||
nym-network-defaults = { path = "../network-defaults" }
|
||||
nym-task = { path = "../task" }
|
||||
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ pub enum BandwidthControllerError {
|
||||
#[error("There was a credential storage error - {0}")]
|
||||
CredentialStorageError(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
#[error("retrieved upgrade mode token is not a valid String")]
|
||||
MalformedUpgradeModeToken,
|
||||
|
||||
#[error("the credential storage does not contain any usable credentials")]
|
||||
NoCredentialsAvailable,
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::utils::{
|
||||
ApiClientsWrapper,
|
||||
};
|
||||
use log::error;
|
||||
use nym_credential_storage::models::RetrievedTicketbook;
|
||||
use nym_credential_storage::models::{EmergencyCredential, RetrievedTicketbook};
|
||||
use nym_credential_storage::storage::Storage;
|
||||
use nym_credentials::ecash::bandwidth::CredentialSpendingData;
|
||||
use nym_credentials_interface::{
|
||||
@@ -220,6 +220,19 @@ impl<C, St: Storage> BandwidthController<C, St> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_emergency_credential(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<Option<EmergencyCredential>, BandwidthControllerError>
|
||||
where
|
||||
<St as Storage>::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
self.storage
|
||||
.get_emergency_credential(typ)
|
||||
.await
|
||||
.map_err(BandwidthControllerError::credential_storage_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, St> Clone for BandwidthController<C, St>
|
||||
|
||||
@@ -11,6 +11,9 @@ use crate::{error::BandwidthControllerError, BandwidthController, PreparedCreden
|
||||
|
||||
pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1;
|
||||
|
||||
// TODO: this does not really belong here
|
||||
pub const UPGRADE_MODE_JWT_TYPE: &str = "UPGRADE_MODE_JWT";
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait BandwidthTicketProvider: Send + Sync {
|
||||
@@ -20,6 +23,8 @@ pub trait BandwidthTicketProvider: Send + Sync {
|
||||
gateway_id: ed25519::PublicKey,
|
||||
tickets_to_spend: u32,
|
||||
) -> Result<PreparedCredential, BandwidthControllerError>;
|
||||
|
||||
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -39,4 +44,16 @@ where
|
||||
self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
|
||||
let Some(emergency_credential) =
|
||||
self.get_emergency_credential(UPGRADE_MODE_JWT_TYPE).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
// upgrade mode credential is just a simple stringified JWT
|
||||
let token = String::from_utf8(emergency_credential.data.content)
|
||||
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
|
||||
Ok(Some(token))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,10 @@ pub struct CommonClientInitArgs {
|
||||
#[cfg_attr(feature = "cli", clap(long, hide = true))]
|
||||
pub enabled_credentials_mode: Option<bool>,
|
||||
|
||||
/// Change the default minimum node performance used during initial node selection filtering.
|
||||
#[cfg_attr(feature = "cli", clap(long, hide = true))]
|
||||
pub minimum_gateway_performance: Option<u8>,
|
||||
|
||||
/// Mostly debug-related option to increase default traffic rate so that you would not need to
|
||||
/// modify config post init
|
||||
#[cfg_attr(feature = "cli", clap(long, hide = true))]
|
||||
@@ -173,10 +177,14 @@ where
|
||||
})?;
|
||||
hardcoded_topology.entry_capable_nodes().cloned().collect()
|
||||
} else {
|
||||
let minimum_performance = common_args
|
||||
.minimum_gateway_performance
|
||||
.unwrap_or(core.debug.topology.minimum_gateway_performance);
|
||||
|
||||
crate::init::helpers::gateways_for_init(
|
||||
&core.client.nym_api_urls,
|
||||
user_agent,
|
||||
core.debug.topology.minimum_gateway_performance,
|
||||
minimum_performance,
|
||||
core.debug.topology.ignore_ingress_epoch_role,
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::bandwidth::ClientBandwidth;
|
||||
use crate::client::config::BandwidthTickets;
|
||||
use crate::error::GatewayClientError;
|
||||
use crate::packet_router::PacketRouter;
|
||||
use crate::traits::GatewayPacketRouter;
|
||||
@@ -177,7 +178,20 @@ impl PartiallyDelegatedRouter {
|
||||
let available_bi2 = bibytes2(available as f64);
|
||||
let required_bi2 = bibytes2(required as f64);
|
||||
warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message");
|
||||
self.client_bandwidth.update_and_log(available, None);
|
||||
// if we run out of bandwidth (and tried to send reasonable amount of data),
|
||||
// the upgrade mode is implicitly disabled, as otherwise we would have been
|
||||
// to proceed
|
||||
let upgrade_mode = if available
|
||||
< BandwidthTickets::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD
|
||||
{
|
||||
Some(false)
|
||||
} else {
|
||||
// we were attempting to send a lot of data at once
|
||||
// - we have no certainty about upgrade mode at this point
|
||||
None
|
||||
};
|
||||
self.client_bandwidth
|
||||
.update_and_log(available, upgrade_mode);
|
||||
// UNIMPLEMENTED: we should stop sending messages until we recover bandwidth
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ bincode = { workspace = true, optional = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
CREATE TABLE emergency_credential
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL,
|
||||
-- don't define any strict schema on the content as it might be implementation-dependant
|
||||
content BLOB NOT NULL,
|
||||
expiration TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
-- no point in allowing duplicate data
|
||||
CREATE UNIQUE INDEX emergency_credential_unique_type_content
|
||||
ON emergency_credential (type, content);
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
|
||||
use crate::models::{
|
||||
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
|
||||
RetrievedPendingTicketbook, RetrievedTicketbook,
|
||||
};
|
||||
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
|
||||
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
|
||||
use nym_compact_ecash::VerificationKeyAuth;
|
||||
@@ -22,6 +25,12 @@ pub struct MemoryEcachTicketbookManager {
|
||||
inner: Arc<RwLock<EcashCredentialManagerInner>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InternalIdCounters {
|
||||
next_ticketbook_id: i64,
|
||||
next_emergency_credential_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct EcashCredentialManagerInner {
|
||||
ticketbooks: HashMap<i64, RetrievedTicketbook>,
|
||||
@@ -29,13 +38,22 @@ struct EcashCredentialManagerInner {
|
||||
master_vk: HashMap<u64, VerificationKeyAuth>,
|
||||
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
|
||||
expiration_date_sigs: HashMap<(u64, Date), Vec<AnnotatedExpirationDateSignature>>,
|
||||
_next_id: i64,
|
||||
emergency_credentials: HashMap<String, Vec<EmergencyCredential>>,
|
||||
|
||||
// internal counters emulating assignment of an increasing id to new inserted database entries
|
||||
internal_counters: InternalIdCounters,
|
||||
}
|
||||
|
||||
impl EcashCredentialManagerInner {
|
||||
fn next_id(&mut self) -> i64 {
|
||||
let next = self._next_id;
|
||||
self._next_id += 1;
|
||||
fn next_ticketbook_id(&mut self) -> i64 {
|
||||
let next = self.internal_counters.next_ticketbook_id;
|
||||
self.internal_counters.next_ticketbook_id += 1;
|
||||
next
|
||||
}
|
||||
|
||||
fn next_emergency_credential_id(&mut self) -> i64 {
|
||||
let next = self.internal_counters.next_emergency_credential_id;
|
||||
self.internal_counters.next_emergency_credential_id += 1;
|
||||
next
|
||||
}
|
||||
}
|
||||
@@ -170,7 +188,7 @@ impl MemoryEcachTicketbookManager {
|
||||
used_tickets: u32,
|
||||
) {
|
||||
let mut guard = self.inner.write().await;
|
||||
let id = guard.next_id();
|
||||
let id = guard.next_ticketbook_id();
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let mut nasty_clone = hack_clone_ticketbook(ticketbook);
|
||||
@@ -277,4 +295,41 @@ impl MemoryEcachTicketbookManager {
|
||||
sigs.signatures.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn get_emergency_credential(&self, typ: &str) -> Option<EmergencyCredential> {
|
||||
let guard = self.inner.read().await;
|
||||
|
||||
guard.emergency_credentials.get(typ)?.first().cloned()
|
||||
}
|
||||
|
||||
pub(crate) async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
) {
|
||||
let mut guard = self.inner.write().await;
|
||||
let id = guard.next_emergency_credential_id();
|
||||
|
||||
guard
|
||||
.emergency_credentials
|
||||
.entry(credential.typ.clone())
|
||||
.or_default()
|
||||
.push(EmergencyCredential {
|
||||
id,
|
||||
data: credential.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_emergency_credential(&self, id: i64) {
|
||||
let mut guard = self.inner.write().await;
|
||||
|
||||
guard.emergency_credentials.retain(|_, credentials| {
|
||||
credentials.retain(|c| c.id != id);
|
||||
!credentials.is_empty()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_emergency_credentials_of_type(&self, typ: &str) {
|
||||
let mut guard = self.inner.write().await;
|
||||
guard.emergency_credentials.remove(typ);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::models::{
|
||||
BasicTicketbookInformation, RawCoinIndexSignatures, RawExpirationDateSignatures,
|
||||
RawVerificationKey, StoredIssuedTicketbook, StoredPendingTicketbook,
|
||||
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
|
||||
RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey,
|
||||
StoredIssuedTicketbook, StoredPendingTicketbook,
|
||||
};
|
||||
use nym_ecash_time::Date;
|
||||
use sqlx::{Executor, Sqlite, Transaction};
|
||||
@@ -305,6 +306,74 @@ impl SqliteEcashTicketbookManager {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_emergency_credential(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<Option<EmergencyCredential>, sqlx::Error> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT *
|
||||
FROM emergency_credential
|
||||
WHERE type = ?
|
||||
AND (expiration IS NULL OR expiration > CURRENT_TIMESTAMP)
|
||||
ORDER BY expiration DESC NULLS LAST
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(typ)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO emergency_credential
|
||||
(type, content, expiration)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(type, content) DO NOTHING;
|
||||
"#,
|
||||
credential.typ,
|
||||
credential.content,
|
||||
credential.expiration,
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_emergency_credential(&self, id: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM emergency_credential
|
||||
WHERE id = ?
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_emergency_credentials_of_type(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM emergency_credential
|
||||
WHERE type = ?
|
||||
"#,
|
||||
typ
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_next_unspent_ticketbook<'a, E>(
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
use crate::backends::memory::MemoryEcachTicketbookManager;
|
||||
use crate::error::StorageError;
|
||||
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
|
||||
use crate::models::{
|
||||
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
|
||||
RetrievedPendingTicketbook, RetrievedTicketbook,
|
||||
};
|
||||
use crate::storage::Storage;
|
||||
use async_trait::async_trait;
|
||||
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
|
||||
@@ -218,6 +221,38 @@ impl Storage for EphemeralStorage {
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_emergency_credential(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<Option<EmergencyCredential>, Self::StorageError> {
|
||||
Ok(self.storage_manager.get_emergency_credential(typ).await)
|
||||
}
|
||||
|
||||
async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager
|
||||
.insert_emergency_credential(credential)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager.remove_emergency_credential(id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_emergency_credentials_of_type(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager
|
||||
.remove_emergency_credentials_of_type(typ)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
|
||||
use nym_ecash_time::Date;
|
||||
use time::OffsetDateTime;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub struct RetrievedTicketbook {
|
||||
@@ -78,3 +79,20 @@ pub struct RawVerificationKey {
|
||||
pub serialised_key: Vec<u8>,
|
||||
pub serialization_revision: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
|
||||
pub struct EmergencyCredential {
|
||||
pub id: i64,
|
||||
#[cfg_attr(not(target_arch = "wasm32"), sqlx(flatten))]
|
||||
pub data: EmergencyCredentialContent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
|
||||
pub struct EmergencyCredentialContent {
|
||||
#[cfg_attr(not(target_arch = "wasm32"), sqlx(rename = "type"))]
|
||||
pub typ: String,
|
||||
pub content: Vec<u8>,
|
||||
pub expiration: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
mod legacy_helpers;
|
||||
|
||||
use crate::models::{EmergencyCredential, EmergencyCredentialContent};
|
||||
use crate::{
|
||||
backends::sqlite::{
|
||||
get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager,
|
||||
@@ -401,4 +402,36 @@ impl Storage for PersistentStorage {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_emergency_credential(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<Option<EmergencyCredential>, Self::StorageError> {
|
||||
Ok(self.storage_manager.get_emergency_credential(typ).await?)
|
||||
}
|
||||
|
||||
async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager
|
||||
.insert_emergency_credential(credential)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager.remove_emergency_credential(id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_emergency_credentials_of_type(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.storage_manager
|
||||
.remove_emergency_credentials_of_type(typ)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
|
||||
use crate::models::{
|
||||
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
|
||||
RetrievedPendingTicketbook, RetrievedTicketbook,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use nym_compact_ecash::VerificationKeyAuth;
|
||||
use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey;
|
||||
@@ -108,4 +111,21 @@ pub trait Storage: Clone + Send + Sync {
|
||||
&self,
|
||||
signatures: &AggregatedExpirationDateSignatures,
|
||||
) -> Result<(), Self::StorageError>;
|
||||
|
||||
async fn get_emergency_credential(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<Option<EmergencyCredential>, Self::StorageError>;
|
||||
|
||||
async fn insert_emergency_credential(
|
||||
&self,
|
||||
credential: &EmergencyCredentialContent,
|
||||
) -> Result<(), Self::StorageError>;
|
||||
|
||||
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError>;
|
||||
|
||||
async fn remove_emergency_credentials_of_type(
|
||||
&self,
|
||||
typ: &str,
|
||||
) -> Result<(), Self::StorageError>;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use tracing::*;
|
||||
|
||||
pub use client_bandwidth::*;
|
||||
pub use error::*;
|
||||
pub use upgrade_mode::UpgradeModeState;
|
||||
|
||||
pub mod bandwidth_storage_manager;
|
||||
mod client_bandwidth;
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::{Notify, RwLock};
|
||||
use tracing::{debug, error};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UpgradeModeEnableError {
|
||||
@@ -101,6 +101,10 @@ impl UpgradeModeDetails {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &UpgradeModeState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.state.upgrade_mode_enabled()
|
||||
}
|
||||
@@ -156,6 +160,7 @@ impl UpgradeModeDetails {
|
||||
|
||||
// note: if attestation has been returned, it means we're definitely in upgrade mode
|
||||
// (otherwise it wouldn't have existed in the state)
|
||||
info!("managed to initialise upgrade mode through received JWT");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -198,6 +203,10 @@ impl UpgradeModeState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attester_pubkey(&self) -> ed25519::PublicKey {
|
||||
self.inner.expected_attester_public_key
|
||||
}
|
||||
|
||||
pub async fn attestation(&self) -> Option<UpgradeModeAttestation> {
|
||||
self.inner.expected_attestation.read().await.clone()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ pub enum UpgradeModeCheckError {
|
||||
#[error("failed to decode jwt metadata: {source}")]
|
||||
TokenMetadataDecodeFailure { source: jwt_simple::Error },
|
||||
|
||||
#[error("the upgrade mode JWT is malformed")]
|
||||
MalformedToken,
|
||||
|
||||
#[error("the jwt metadata didn't contain explicit public key")]
|
||||
MissingTokenPublicKey,
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
use crate::{UpgradeModeAttestation, UpgradeModeCheckError};
|
||||
use jwt_simple::claims::Claims;
|
||||
use jwt_simple::common::{KeyMetadata, VerificationOptions};
|
||||
use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike};
|
||||
use jwt_simple::prelude::{
|
||||
Base64UrlSafeNoPadding, EdDSAKeyPairLike, EdDSAPublicKeyLike, JWTClaims,
|
||||
};
|
||||
use jwt_simple::reexports::ct_codecs::Decoder;
|
||||
use jwt_simple::token::Token;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::collections::HashSet;
|
||||
@@ -76,12 +79,26 @@ pub fn validate_upgrade_mode_jwt(
|
||||
Ok(attestation)
|
||||
}
|
||||
|
||||
/// Attempt to extract the upgrade mode JWT payload from the provided token
|
||||
pub fn try_decode_upgrade_mode_jwt_claims(
|
||||
token: &str,
|
||||
) -> Result<JWTClaims<UpgradeModeAttestation>, UpgradeModeCheckError> {
|
||||
let mut parts = token.split('.');
|
||||
let _header = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?;
|
||||
let claims_b64 = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?;
|
||||
let claims_bytes = Base64UrlSafeNoPadding::decode_to_vec(claims_b64, None)
|
||||
.map_err(|_| UpgradeModeCheckError::MalformedToken)?;
|
||||
|
||||
serde_json::from_slice(&claims_bytes).map_err(|_| UpgradeModeCheckError::MalformedToken)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate_new_attestation;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[test]
|
||||
fn generate_and_validate_jwt() {
|
||||
@@ -142,4 +159,58 @@ mod tests {
|
||||
// we don't care about issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_upgrade_mode_claims() {
|
||||
let invalid_jwts = [
|
||||
"",
|
||||
"invalidSections",
|
||||
"also.invalid.sections",
|
||||
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0",
|
||||
];
|
||||
|
||||
let attestation_key = ed25519::PrivateKey::from_bytes(&[
|
||||
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
|
||||
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
|
||||
])
|
||||
.unwrap();
|
||||
let jwt_key = ed25519::PrivateKey::from_bytes(&[
|
||||
152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167,
|
||||
2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32,
|
||||
])
|
||||
.unwrap();
|
||||
let jwt_keys = ed25519::KeyPair::from(jwt_key);
|
||||
|
||||
let validity = Duration::from_secs(60 * 60);
|
||||
let attestation = generate_new_attestation(&attestation_key, vec![*jwt_keys.public_key()]);
|
||||
let valid_jwt = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation.clone(),
|
||||
validity,
|
||||
&jwt_keys,
|
||||
Some("nym-credential-proxy"),
|
||||
);
|
||||
|
||||
for invalid in invalid_jwts {
|
||||
assert!(try_decode_upgrade_mode_jwt_claims(invalid).is_err())
|
||||
}
|
||||
|
||||
let decoded = try_decode_upgrade_mode_jwt_claims(&valid_jwt).unwrap();
|
||||
assert_eq!(decoded.issuer.unwrap(), "nym-credential-proxy");
|
||||
assert_eq!(decoded.custom, attestation);
|
||||
|
||||
// unfortunately we can't inject current time when constructing the JWT so the best we can do is ensure its within error margin
|
||||
let margin = Duration::from_secs(10);
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let min = now - margin;
|
||||
let max = now + margin;
|
||||
let issued = decoded.issued_at.unwrap();
|
||||
let issued_time = OffsetDateTime::from_unix_timestamp(issued.as_secs() as i64).unwrap();
|
||||
assert!(issued_time >= min && issued_time <= max);
|
||||
|
||||
let min = now - margin + validity;
|
||||
let max = now + margin + validity;
|
||||
let expires = decoded.expires_at.unwrap();
|
||||
let expires_time = OffsetDateTime::from_unix_timestamp(expires.as_secs() as i64).unwrap();
|
||||
assert!(expires_time >= min && expires_time <= max);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ pub use attestation::{
|
||||
pub use error::UpgradeModeCheckError;
|
||||
pub use jwt::{
|
||||
CREDENTIAL_PROXY_JWT_ISSUER, generate_jwt_for_upgrade_mode_attestation,
|
||||
validate_upgrade_mode_jwt,
|
||||
try_decode_upgrade_mode_jwt_claims, validate_upgrade_mode_jwt,
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use attestation::attempt_retrieve_attestation;
|
||||
|
||||
pub const UPGRADE_MODE_CREDENTIAL_TYPE: &str = "upgrade_mode_jwt";
|
||||
|
||||
@@ -10,7 +10,9 @@ pub mod routes;
|
||||
pub use models::v0;
|
||||
pub use models::{
|
||||
AxumErrorResponse, AxumResult, Construct, ErrorResponse, Extract, Request, Response, Version,
|
||||
error::Error as ModelError, interface, latest, v1, v2,
|
||||
error::Error as ModelError,
|
||||
interface::{self, AvailableBandwidth},
|
||||
latest, v1, v2,
|
||||
};
|
||||
|
||||
fn make_bincode_serializer() -> impl bincode::Options {
|
||||
|
||||
@@ -229,3 +229,45 @@ impl Extract<ResponseData> for Response {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AvailableBandwidth {
|
||||
pub bandwidth_bytes: i64,
|
||||
pub upgrade_mode: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<v1::AvailableBandwidthResponse> for AvailableBandwidth {
|
||||
fn from(value: v1::AvailableBandwidthResponse) -> Self {
|
||||
AvailableBandwidth {
|
||||
bandwidth_bytes: value.available_bandwidth,
|
||||
upgrade_mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::AvailableBandwidthResponse> for AvailableBandwidth {
|
||||
fn from(value: v2::AvailableBandwidthResponse) -> Self {
|
||||
AvailableBandwidth {
|
||||
bandwidth_bytes: value.available_bandwidth,
|
||||
upgrade_mode: Some(value.upgrade_mode),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v1::TopUpResponse> for AvailableBandwidth {
|
||||
fn from(value: v1::TopUpResponse) -> Self {
|
||||
AvailableBandwidth {
|
||||
bandwidth_bytes: value.available_bandwidth,
|
||||
upgrade_mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::TopUpResponse> for AvailableBandwidth {
|
||||
fn from(value: v2::TopUpResponse) -> Self {
|
||||
AvailableBandwidth {
|
||||
bandwidth_bytes: value.available_bandwidth,
|
||||
upgrade_mode: Some(value.upgrade_mode),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
SinkExt, StreamExt,
|
||||
};
|
||||
use nym_credentials_interface::AvailableBandwidth;
|
||||
use nym_credentials_interface::{AvailableBandwidth, DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD};
|
||||
use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::authenticate::AuthenticateRequest;
|
||||
@@ -35,6 +35,7 @@ use nym_node_metrics::events::MetricsEvent;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use nym_task::ShutdownToken;
|
||||
use rand::CryptoRng;
|
||||
use std::cmp::max;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
@@ -528,6 +529,35 @@ impl<R, S> FreshHandler<R, S> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the amount of remaining bandwidth the authenticated client should see.
|
||||
/// This depends on whether the bandwidth stored persistently had already expired (in which case it's set back to 0)
|
||||
/// and whether the upgrade mode is enabled. In that case the minimum constant amount is returned.
|
||||
async fn authenticated_bandwidth_bytes(
|
||||
&self,
|
||||
client_id: i64,
|
||||
) -> Result<i64, InitialAuthenticationError> {
|
||||
// 1. get the actual registered bandwidth
|
||||
let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?;
|
||||
|
||||
// 2. check if it had already expired
|
||||
let true_remaining_bandwidth = if available_bandwidth.expired() {
|
||||
self.shared_state.storage.reset_bandwidth(client_id).await?;
|
||||
0
|
||||
} else {
|
||||
available_bandwidth.bytes
|
||||
};
|
||||
|
||||
// 3. perform upgrade mode adjustments
|
||||
if self.upgrade_mode_enabled() {
|
||||
Ok(max(
|
||||
true_remaining_bandwidth,
|
||||
DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD + 1,
|
||||
))
|
||||
} else {
|
||||
Ok(true_remaining_bandwidth)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to handle the received authentication request by checking correctness of the received data.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -594,14 +624,7 @@ impl<R, S> FreshHandler<R, S> {
|
||||
.await?;
|
||||
|
||||
// check the bandwidth
|
||||
let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?;
|
||||
|
||||
let bandwidth_remaining = if available_bandwidth.expired() {
|
||||
self.shared_state.storage.reset_bandwidth(client_id).await?;
|
||||
0
|
||||
} else {
|
||||
available_bandwidth.bytes
|
||||
};
|
||||
let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?;
|
||||
|
||||
Ok(InitialAuthResult::new(
|
||||
Some(ClientDetails::new(
|
||||
@@ -681,14 +704,7 @@ impl<R, S> FreshHandler<R, S> {
|
||||
.await?;
|
||||
|
||||
// finally check and retrieve client's bandwidth
|
||||
let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?;
|
||||
|
||||
let bandwidth_remaining = if available_bandwidth.expired() {
|
||||
self.shared_state.storage.reset_bandwidth(client_id).await?;
|
||||
0
|
||||
} else {
|
||||
available_bandwidth.bytes
|
||||
};
|
||||
let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?;
|
||||
|
||||
Ok(InitialAuthResult::new(
|
||||
Some(ClientDetails::new(
|
||||
|
||||
@@ -49,7 +49,7 @@ pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgen
|
||||
pub(crate) mod client_handling;
|
||||
pub(crate) mod internal_service_providers;
|
||||
mod stale_data_cleaner;
|
||||
pub(crate) mod upgrade_mode;
|
||||
pub mod upgrade_mode;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalNetworkRequesterOpts {
|
||||
@@ -122,7 +122,7 @@ impl GatewayTasksBuilder {
|
||||
metrics: NymNodeMetrics,
|
||||
mnemonic: Arc<Zeroizing<bip39::Mnemonic>>,
|
||||
user_agent: UserAgent,
|
||||
upgrade_mode_attester_public_key: ed25519::PublicKey,
|
||||
upgrade_mode_state: UpgradeModeState,
|
||||
shutdown_tracker: ShutdownTracker,
|
||||
) -> GatewayTasksBuilder {
|
||||
GatewayTasksBuilder {
|
||||
@@ -137,7 +137,7 @@ impl GatewayTasksBuilder {
|
||||
mix_packet_sender,
|
||||
metrics_sender,
|
||||
metrics,
|
||||
upgrade_mode_state: UpgradeModeState::new(upgrade_mode_attester_public_key),
|
||||
upgrade_mode_state,
|
||||
mnemonic,
|
||||
shutdown_tracker,
|
||||
ecash_manager: None,
|
||||
|
||||
@@ -10,11 +10,17 @@ use nym_credential_verification::upgrade_mode::{
|
||||
use nym_task::ShutdownToken;
|
||||
use nym_upgrade_mode_check::attempt_retrieve_attestation;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, info, trace};
|
||||
use url::Url;
|
||||
|
||||
/// Specifies the threshold for retrieval failures that will trigger disabling upgrade mode.
|
||||
/// This assumes the file has been removed incorrectly and has been replaced by some placeholder 404
|
||||
/// page that does not deserialise correctly
|
||||
const FAILURE_THRESHOLD: usize = 5;
|
||||
|
||||
pub struct UpgradeModeWatcher {
|
||||
// default polling interval
|
||||
regular_polling_interval: Duration,
|
||||
@@ -35,6 +41,8 @@ pub struct UpgradeModeWatcher {
|
||||
user_agent: UserAgent,
|
||||
|
||||
shutdown_token: ShutdownToken,
|
||||
|
||||
consecutive_retrieval_failures: usize,
|
||||
}
|
||||
|
||||
impl UpgradeModeWatcher {
|
||||
@@ -58,6 +66,7 @@ impl UpgradeModeWatcher {
|
||||
upgrade_mode_state,
|
||||
user_agent,
|
||||
shutdown_token,
|
||||
consecutive_retrieval_failures: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +74,7 @@ impl UpgradeModeWatcher {
|
||||
self.check_request_sender.clone()
|
||||
}
|
||||
|
||||
async fn try_update_state(&self) {
|
||||
async fn try_update_state(&mut self) {
|
||||
match attempt_retrieve_attestation(
|
||||
self.attestation_url.as_str(),
|
||||
Some(self.user_agent.clone()),
|
||||
@@ -73,10 +82,28 @@ impl UpgradeModeWatcher {
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
self.consecutive_retrieval_failures += 1;
|
||||
info!("upgrade mode attestation is not available at this time");
|
||||
debug!("retrieval error: {err}")
|
||||
debug!("retrieval error: {err}");
|
||||
|
||||
if self.upgrade_mode_state.upgrade_mode_enabled()
|
||||
&& self.consecutive_retrieval_failures > FAILURE_THRESHOLD
|
||||
{
|
||||
self.upgrade_mode_state
|
||||
.try_set_expected_attestation(None)
|
||||
.await
|
||||
} else {
|
||||
self.upgrade_mode_state
|
||||
.update_last_queried(OffsetDateTime::now_utc());
|
||||
}
|
||||
}
|
||||
Ok(attestation) => {
|
||||
self.consecutive_retrieval_failures = 0;
|
||||
if attestation.is_some() {
|
||||
info!("retrieved valid attestation: attempting to begin upgrade mode")
|
||||
} else {
|
||||
info!("attempting to disable upgrade mode")
|
||||
}
|
||||
self.upgrade_mode_state
|
||||
.try_set_expected_attestation(attestation)
|
||||
.await
|
||||
@@ -110,7 +137,8 @@ impl UpgradeModeWatcher {
|
||||
async fn run(&mut self) {
|
||||
info!("starting the update mode watcher");
|
||||
|
||||
let check_wait = tokio::time::sleep(self.regular_polling_interval);
|
||||
// make sure the first check happens immediately
|
||||
let check_wait = tokio::time::sleep(Duration::new(0, 0));
|
||||
tokio::pin!(check_wait);
|
||||
|
||||
loop {
|
||||
|
||||
@@ -40,6 +40,12 @@ pub enum AuthenticationClientError {
|
||||
source: nym_bandwidth_controller::error::BandwidthControllerError,
|
||||
},
|
||||
|
||||
#[error("failed to retrieve upgrade mode token")]
|
||||
UpgradeModeToken {
|
||||
#[source]
|
||||
source: nym_bandwidth_controller::error::BandwidthControllerError,
|
||||
},
|
||||
|
||||
#[error("unknown authenticator version number")]
|
||||
UnsupportedAuthenticatorVersion,
|
||||
|
||||
|
||||
@@ -8,17 +8,18 @@ use nym_registration_common::GatewayData;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, error, trace};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender};
|
||||
use crate::types::{AvailableBandwidthClientResponse, TopUpClientResponse};
|
||||
use nym_authenticator_requests::models::BandwidthClaim;
|
||||
use nym_authenticator_requests::traits::UpgradeModeStatus;
|
||||
use nym_authenticator_requests::{
|
||||
AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse,
|
||||
traits::Id, v2, v3, v4, v5, v6,
|
||||
};
|
||||
use nym_credentials_interface::{CredentialSpendingData, TicketType};
|
||||
use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType};
|
||||
use nym_sdk::mixnet::{IncludedSurbs, Recipient, ReconstructedMessage};
|
||||
use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
@@ -205,6 +206,57 @@ impl AuthenticatorClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn produce_bandwidth_claim(
|
||||
&self,
|
||||
controller: &dyn BandwidthTicketProvider,
|
||||
upgrade_mode_enabled: bool,
|
||||
ticketbook_type: TicketType,
|
||||
) -> Result<BandwidthClaim> {
|
||||
if upgrade_mode_enabled {
|
||||
match controller
|
||||
.get_upgrade_mode_token()
|
||||
.await
|
||||
.map_err(|source| AuthenticationClientError::UpgradeModeToken { source })?
|
||||
{
|
||||
None => warn!(
|
||||
"the wireguard node is in the upgrade mode, whilst we do not have an upgrade mode token - we will have to use normal ZK nym instead"
|
||||
),
|
||||
|
||||
Some(upgrade_mode_token) => {
|
||||
return Ok(BandwidthClaim {
|
||||
credential: BandwidthCredential::UpgradeModeJWT {
|
||||
token: upgrade_mode_token,
|
||||
},
|
||||
kind: ticketbook_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let credential = controller
|
||||
.get_ecash_ticket(
|
||||
ticketbook_type,
|
||||
self.auth_recipient.gateway(),
|
||||
DEFAULT_TICKETS_TO_SPEND,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| AuthenticationClientError::GetTicket {
|
||||
ticketbook_type,
|
||||
source,
|
||||
})?
|
||||
.data;
|
||||
|
||||
let credential = credential
|
||||
.try_into()
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
"failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}"
|
||||
)
|
||||
})
|
||||
.map_err(|_| AuthenticationClientError::InternalError)?;
|
||||
Ok(credential)
|
||||
}
|
||||
|
||||
pub async fn register_wireguard(
|
||||
&mut self,
|
||||
controller: &dyn BandwidthTicketProvider,
|
||||
@@ -255,33 +307,19 @@ impl AuthenticatorClient {
|
||||
&self.ip_addr, &pending_registration_response
|
||||
);
|
||||
|
||||
// This call takes care of updating the credential count in storage, so failure of this must be counted as credential waste
|
||||
let credential = Some(
|
||||
controller
|
||||
.get_ecash_ticket(
|
||||
ticketbook_type,
|
||||
self.auth_recipient.gateway(),
|
||||
DEFAULT_TICKETS_TO_SPEND,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| RegistrationError::CredentialSent {
|
||||
source: AuthenticationClientError::GetTicket {
|
||||
ticketbook_type,
|
||||
source,
|
||||
},
|
||||
})?
|
||||
.data,
|
||||
);
|
||||
let credential = credential
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.inspect_err(|err| error!("failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}"))
|
||||
.map_err(|_| RegistrationError::CredentialSent {
|
||||
source: AuthenticationClientError::InternalError,
|
||||
})?;
|
||||
// if the node reports upgrade mode, we can use the corresponding token for registration
|
||||
// instead of spending zk-nym ticket
|
||||
let upgrade_mode_enabled = pending_registration_response
|
||||
.upgrade_mode_status()
|
||||
.is_enabled();
|
||||
|
||||
let bandwidth_claim = self
|
||||
.produce_bandwidth_claim(controller, upgrade_mode_enabled, ticketbook_type)
|
||||
.await
|
||||
.map_err(|source| RegistrationError::CredentialSent { source })?;
|
||||
|
||||
let finalized_message = pending_registration_response
|
||||
.finalise_registration(self.keypair.private_key(), credential);
|
||||
.finalise_registration(self.keypair.private_key(), Some(bandwidth_claim));
|
||||
let client_message = ClientMessage::Final(finalized_message);
|
||||
|
||||
trace!("sending final msg to {}: {client_message:?}", &self.ip_addr);
|
||||
@@ -421,4 +459,32 @@ impl AuthenticatorClient {
|
||||
current_upgrade_mode_status,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn check_upgrade_mode(&mut self, upgrade_mode_jwt: String) -> Result<bool> {
|
||||
let check_um_message = match self.auth_version {
|
||||
AuthenticatorVersion::V1
|
||||
| AuthenticatorVersion::V2
|
||||
| AuthenticatorVersion::V3
|
||||
| AuthenticatorVersion::V4
|
||||
| AuthenticatorVersion::V5
|
||||
| AuthenticatorVersion::UNKNOWN => {
|
||||
return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion);
|
||||
}
|
||||
|
||||
AuthenticatorVersion::V6 => ClientMessage::UpgradeModeCheck(Box::new(
|
||||
v6::upgrade_mode_check::UpgradeModeCheckRequest::UpgradeModeJwt {
|
||||
token: upgrade_mode_jwt,
|
||||
},
|
||||
)),
|
||||
};
|
||||
|
||||
let response = self.send_and_wait_for_response(&check_um_message).await?;
|
||||
let AuthenticatorResponse::UpgradeMode(upgrade_mode_check_response) = response else {
|
||||
return Err(AuthenticationClientError::InvalidGatewayAuthResponse);
|
||||
};
|
||||
|
||||
Ok(upgrade_mode_check_response
|
||||
.upgrade_mode_status()
|
||||
.is_enabled())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, info};
|
||||
use url::Url;
|
||||
|
||||
/// Specifies the threshold for retrieval failures that will trigger disabling upgrade mode.
|
||||
/// This assumes the file has been removed incorrectly and has been replaced by some placeholder 404
|
||||
/// page that does not deserialise correctly
|
||||
const FAILURE_THRESHOLD: usize = 5;
|
||||
|
||||
pub struct AttestationWatcher {
|
||||
// default polling interval
|
||||
regular_polling_interval: Duration,
|
||||
@@ -28,6 +33,8 @@ pub struct AttestationWatcher {
|
||||
jwt_validity: Duration,
|
||||
|
||||
upgrade_mode_state: UpgradeModeState,
|
||||
|
||||
consecutive_retrieval_failures: usize,
|
||||
}
|
||||
|
||||
impl AttestationWatcher {
|
||||
@@ -49,6 +56,7 @@ impl AttestationWatcher {
|
||||
upgrade_mode_state: UpgradeModeState {
|
||||
inner: Arc::new(Default::default()),
|
||||
},
|
||||
consecutive_retrieval_failures: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +64,7 @@ impl AttestationWatcher {
|
||||
self.upgrade_mode_state.clone()
|
||||
}
|
||||
|
||||
async fn try_update_state(&self) {
|
||||
async fn try_update_state(&mut self) {
|
||||
match attempt_retrieve_attestation(
|
||||
self.attestation_url.as_str(),
|
||||
Some(generate_user_agent!()),
|
||||
@@ -64,10 +72,26 @@ impl AttestationWatcher {
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
self.consecutive_retrieval_failures += 1;
|
||||
info!("upgrade mode attestation is not available at this time");
|
||||
debug!("retrieval error: {err}")
|
||||
debug!("retrieval error: {err}");
|
||||
|
||||
if self.upgrade_mode_state.has_attestation().await
|
||||
&& self.consecutive_retrieval_failures > FAILURE_THRESHOLD
|
||||
{
|
||||
self.upgrade_mode_state
|
||||
.update(
|
||||
None,
|
||||
self.expected_attester_public_key,
|
||||
&self.jwt_signing_keys,
|
||||
self.jwt_validity,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
Ok(attestation) => {
|
||||
self.consecutive_retrieval_failures = 0;
|
||||
|
||||
self.upgrade_mode_state
|
||||
.update(
|
||||
attestation,
|
||||
@@ -80,9 +104,10 @@ impl AttestationWatcher {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_forever(self, cancellation_token: CancellationToken) {
|
||||
pub async fn run_forever(mut self, cancellation_token: CancellationToken) {
|
||||
info!("starting the attestation watcher task");
|
||||
|
||||
// make sure the first check happens immediately
|
||||
let check_wait = tokio::time::sleep(Duration::new(0, 0));
|
||||
tokio::pin!(check_wait);
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ nym-client-core-config-types = { path = "../common/client-core/config-types", fe
|
||||
"disk-persistence",
|
||||
] }
|
||||
nym-config = { path = "../common/config" }
|
||||
nym-credential-verification = { path = "../common/credential-verification" }
|
||||
nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] }
|
||||
nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" }
|
||||
nym-mixnet-client = { path = "../common/client-libs/mixnet-client" }
|
||||
|
||||
@@ -12,7 +12,6 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
celes = { workspace = true } # country codes
|
||||
humantime = { workspace = true }
|
||||
humantime-serde = { workspace = true }
|
||||
schemars = { workspace = true, features = ["preserve_order"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -21,6 +20,7 @@ strum = { workspace = true, features = ["derive"] }
|
||||
strum_macros = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
|
||||
nym-crypto = { path = "../../common/crypto", features = [
|
||||
"asymmetric",
|
||||
@@ -29,6 +29,7 @@ nym-crypto = { path = "../../common/crypto", features = [
|
||||
nym-exit-policy = { path = "../../common/exit-policy" }
|
||||
nym-noise-keys = { path = "../../common/nymnoise/keys" }
|
||||
nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false }
|
||||
nym-upgrade-mode-check = { path = "../../common/upgrade-mode-check", features = ["openapi"] }
|
||||
|
||||
# feature-specific dependencies:
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod health;
|
||||
pub mod ip_packet_router;
|
||||
pub mod metrics;
|
||||
pub mod mixnode;
|
||||
pub mod network;
|
||||
pub mod network_requester;
|
||||
pub mod node;
|
||||
pub mod node_load;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod models;
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
|
||||
use nym_upgrade_mode_check::UpgradeModeAttestation;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct UpgradeModeStatus {
|
||||
pub enabled: bool,
|
||||
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub last_queried: OffsetDateTime,
|
||||
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub attestation_provider: Url,
|
||||
|
||||
#[serde(with = "bs58_ed25519_pubkey")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = String))]
|
||||
pub attester_pubkey: ed25519::PublicKey,
|
||||
|
||||
pub published_attestation: Option<UpgradeModeAttestation>,
|
||||
}
|
||||
@@ -36,6 +36,7 @@ pub mod routes {
|
||||
pub const AUXILIARY: &str = "/auxiliary-details";
|
||||
pub const HEALTH: &str = "/health";
|
||||
pub const LOAD: &str = "/load";
|
||||
pub const NETWORK: &str = "/network";
|
||||
pub const SWAGGER: &str = "/swagger";
|
||||
|
||||
pub const GATEWAY: &str = "/gateway";
|
||||
@@ -49,6 +50,7 @@ pub mod routes {
|
||||
// define helper functions to get absolute routes
|
||||
absolute_route!(health_absolute, v1_absolute(), HEALTH);
|
||||
absolute_route!(load_absolute, v1_absolute(), LOAD);
|
||||
absolute_route!(network_absolute, v1_absolute(), NETWORK);
|
||||
absolute_route!(roles_absolute, v1_absolute(), ROLES);
|
||||
absolute_route!(build_info_absolute, v1_absolute(), BUILD_INFO);
|
||||
absolute_route!(host_info_absolute, v1_absolute(), HOST_INFO);
|
||||
@@ -131,6 +133,17 @@ pub mod routes {
|
||||
pub mod ip_packet_router {
|
||||
// use super::*;
|
||||
}
|
||||
|
||||
pub mod network {
|
||||
use super::*;
|
||||
pub const UPGRADE_MODE_STATUS: &str = "/upgrade-mode-status";
|
||||
|
||||
absolute_route!(
|
||||
upgrade_mode_status_absolute,
|
||||
network_absolute(),
|
||||
UPGRADE_MODE_STATUS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ fn check_public_ips(ips: &[IpAddr], local: bool) -> Result<(), NymNodeError> {
|
||||
warn!("\n##### WARNING #####");
|
||||
for ip in suspicious_ip {
|
||||
warn!(
|
||||
"The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients.\
|
||||
"The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients. \
|
||||
Please make sure this is what you intended to announce.\
|
||||
You can ignore this warning if you're running setup on a local network "
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::error::NymNodeError;
|
||||
use celes::Country;
|
||||
use clap::Args;
|
||||
use clap::builder::ArgPredicate;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::{Path, PathBuf};
|
||||
use url::Url;
|
||||
@@ -429,12 +430,22 @@ pub(crate) struct EntryGatewayArgs {
|
||||
pub(crate) mnemonic: Option<bip39::Mnemonic>,
|
||||
|
||||
/// Endpoint to query to retrieve current upgrade mode attestation.
|
||||
/// This argument should never be set outside testnets and local networks.
|
||||
#[clap(
|
||||
long,
|
||||
env = NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG
|
||||
)]
|
||||
#[zeroize(skip)]
|
||||
pub(crate) upgrade_mode_attestation_url: Option<Url>,
|
||||
|
||||
/// Expected public key of the entity signing the published attestation.
|
||||
/// This argument should never be set outside testnets and local networks.
|
||||
#[clap(
|
||||
long,
|
||||
env = NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG
|
||||
)]
|
||||
#[zeroize(skip)]
|
||||
pub(crate) upgrade_mode_attester_public_key: Option<ed25519::PublicKey>,
|
||||
}
|
||||
|
||||
impl EntryGatewayArgs {
|
||||
@@ -465,6 +476,9 @@ impl EntryGatewayArgs {
|
||||
if let Some(upgrade_mode_attestation_url) = self.upgrade_mode_attestation_url.take() {
|
||||
section.upgrade_mode.attestation_url = upgrade_mode_attestation_url
|
||||
}
|
||||
if let Some(upgrade_mode_attester_public_key) = self.upgrade_mode_attester_public_key {
|
||||
section.upgrade_mode.attester_public_key = upgrade_mode_attester_public_key
|
||||
}
|
||||
|
||||
section
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ pub struct UpgradeModeWatcher {
|
||||
#[serde(with = "bs58_ed25519_pubkey")]
|
||||
pub attester_public_key: ed25519::PublicKey,
|
||||
|
||||
#[serde(default)]
|
||||
pub debug: UpgradeModeWatcherDebug,
|
||||
}
|
||||
|
||||
|
||||
@@ -695,6 +695,10 @@ impl ReplayProtectionDebug {
|
||||
pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200;
|
||||
|
||||
pub fn validate(&self) -> Result<(), NymNodeError> {
|
||||
if self.unsafe_disabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.false_positive_rate >= 1.0 || self.false_positive_rate <= 0.0 {
|
||||
return Err(NymNodeError::config_validation_failure(
|
||||
"false positive rate for replay detection can't be larger than (or equal to) 1 or smaller than (or equal to) 0",
|
||||
|
||||
@@ -63,6 +63,8 @@ pub mod vars {
|
||||
pub const NYMNODE_MNEMONIC_ARG: &str = "NYMNODE_MNEMONIC";
|
||||
pub const NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG: &str =
|
||||
"NYMNODE_UPGRADE_MODE_ATTESTATION_URL";
|
||||
pub const NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG: &str =
|
||||
"NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY";
|
||||
|
||||
// exit gateway:
|
||||
pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY";
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod ip_packet_router;
|
||||
pub mod load;
|
||||
pub mod metrics;
|
||||
pub mod mixnode;
|
||||
pub mod network;
|
||||
pub mod network_requester;
|
||||
pub mod node;
|
||||
pub mod openapi;
|
||||
@@ -34,6 +35,7 @@ pub(super) fn routes(config: Config) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(v1::HEALTH, get(health::root_health))
|
||||
.route(v1::LOAD, get(load::root_load))
|
||||
.nest(v1::NETWORK, network::routes())
|
||||
.nest(v1::METRICS, metrics::routes(config.metrics))
|
||||
.nest(v1::BRIDGES, bridges::routes(config.bridges))
|
||||
.nest(v1::GATEWAY, gateway::routes(config.gateway))
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::api::v1::network::upgrade_mode::upgrade_mode_status;
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use nym_node_requests::routes::api::v1::network;
|
||||
|
||||
pub mod upgrade_mode;
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new().route(network::UPGRADE_MODE_STATUS, get(upgrade_mode_status))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_node_requests::api::v1::network::models::UpgradeModeStatus;
|
||||
|
||||
/// Returns current upgrade mode information as perceived by this node.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/upgrade-mode-status",
|
||||
context_path = "/api/v1/network",
|
||||
tag = "Network",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(UpgradeModeStatus = "application/json"),
|
||||
(UpgradeModeStatus = "application/yaml"),
|
||||
(UpgradeModeStatus = "application/bincode")
|
||||
))
|
||||
),
|
||||
params(OutputParams)
|
||||
)]
|
||||
pub(crate) async fn upgrade_mode_status(
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> UpgradeModeStatusResponse {
|
||||
let output = output.get_output();
|
||||
let um_state = &state.upgrade_mode_state.node_state;
|
||||
output.to_response(UpgradeModeStatus {
|
||||
enabled: um_state.upgrade_mode_enabled(),
|
||||
last_queried: um_state.last_queried(),
|
||||
attestation_provider: state.upgrade_mode_state.attestation_url.clone(),
|
||||
attester_pubkey: um_state.attester_pubkey(),
|
||||
published_attestation: um_state.attestation().await,
|
||||
})
|
||||
}
|
||||
|
||||
pub type UpgradeModeStatusResponse = FormattedResponse<UpgradeModeStatus>;
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::router::api;
|
||||
use axum::Router;
|
||||
use nym_node_requests::api as api_requests;
|
||||
use nym_node_requests::routes::api::{v1, v1_absolute};
|
||||
@@ -13,25 +12,31 @@ use utoipa_swagger_ui::SwaggerUi;
|
||||
#[openapi(
|
||||
info(title = "NymNode API"),
|
||||
paths(
|
||||
api::v1::node::build_information::build_information,
|
||||
api::v1::node::host_information::host_information,
|
||||
api::v1::node::roles::roles,
|
||||
api::v1::node::hardware::host_system,
|
||||
api::v1::node::description::description,
|
||||
api::v1::node::auxiliary::auxiliary,
|
||||
api::v1::metrics::legacy_mixing::legacy_mixing_stats,
|
||||
api::v1::metrics::packets_stats::packets_stats,
|
||||
api::v1::metrics::verloc::verloc_stats,
|
||||
api::v1::metrics::prometheus::prometheus_metrics,
|
||||
api::v1::health::root_health,
|
||||
api::v1::load::root_load,
|
||||
api::v1::gateway::root::root_gateway,
|
||||
api::v1::gateway::client_interfaces::client_interfaces,
|
||||
api::v1::gateway::client_interfaces::mixnet_websockets,
|
||||
api::v1::mixnode::root::root_mixnode,
|
||||
api::v1::network_requester::root::root_network_requester,
|
||||
api::v1::network_requester::exit_policy::node_exit_policy,
|
||||
api::v1::ip_packet_router::root::root_ip_packet_router,
|
||||
crate::node::http::router::api::v1::metrics::verloc::verloc_stats,
|
||||
crate::node::http::router::api::v1::metrics::legacy_mixing::legacy_mixing_stats,
|
||||
crate::node::http::router::api::v1::metrics::wireguard::wireguard_stats,
|
||||
crate::node::http::router::api::v1::metrics::sessions::sessions_stats,
|
||||
crate::node::http::router::api::v1::metrics::packets_stats::packets_stats,
|
||||
crate::node::http::router::api::v1::metrics::prometheus::prometheus_metrics,
|
||||
crate::node::http::router::api::v1::ip_packet_router::root::root_ip_packet_router,
|
||||
crate::node::http::router::api::v1::health::root_health,
|
||||
crate::node::http::router::api::v1::network::upgrade_mode::upgrade_mode_status,
|
||||
crate::node::http::router::api::v1::mixnode::root::root_mixnode,
|
||||
crate::node::http::router::api::v1::load::root_load,
|
||||
crate::node::http::router::api::v1::node::host_information::host_information,
|
||||
crate::node::http::router::api::v1::node::description::description,
|
||||
crate::node::http::router::api::v1::node::roles::roles,
|
||||
crate::node::http::router::api::v1::node::auxiliary::auxiliary,
|
||||
crate::node::http::router::api::v1::node::build_information::build_information,
|
||||
crate::node::http::router::api::v1::node::hardware::host_system,
|
||||
crate::node::http::router::api::v1::authenticator::root::root_authenticator,
|
||||
crate::node::http::router::api::v1::network_requester::exit_policy::node_exit_policy,
|
||||
crate::node::http::router::api::v1::network_requester::root::root_network_requester,
|
||||
crate::node::http::router::api::v1::gateway::client_interfaces::client_interfaces,
|
||||
crate::node::http::router::api::v1::gateway::client_interfaces::mixnet_websockets,
|
||||
crate::node::http::router::api::v1::gateway::client_interfaces::wireguard_details,
|
||||
crate::node::http::router::api::v1::gateway::root::root_gateway,
|
||||
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::node::http::state::load::CachedNodeLoad;
|
||||
use crate::node::http::state::metrics::MetricsAppState;
|
||||
use crate::node::key_rotation::active_keys::ActiveSphinxKeys;
|
||||
use nym_credential_verification::UpgradeModeState;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
@@ -12,6 +13,7 @@ use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
use url::Url;
|
||||
|
||||
pub mod load;
|
||||
pub mod metrics;
|
||||
@@ -23,6 +25,12 @@ pub(crate) struct StaticNodeInformation {
|
||||
pub(crate) hostname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct UpgradeModeApiState {
|
||||
pub(crate) node_state: UpgradeModeState,
|
||||
pub(crate) attestation_url: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AppState {
|
||||
pub(crate) startup_time: Instant,
|
||||
@@ -34,15 +42,18 @@ pub(crate) struct AppState {
|
||||
pub(crate) cached_load: CachedNodeLoad,
|
||||
|
||||
pub(crate) metrics: MetricsAppState,
|
||||
|
||||
pub(crate) upgrade_mode_state: UpgradeModeApiState,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub(crate) fn new(
|
||||
static_information: StaticNodeInformation,
|
||||
x25519_sphinx_keys: ActiveSphinxKeys,
|
||||
metrics: NymNodeMetrics,
|
||||
verloc: SharedVerlocStats,
|
||||
upgrade_mode_attestation_url: Url,
|
||||
upgrade_mode_state: UpgradeModeState,
|
||||
load_cache_ttl: Duration,
|
||||
) -> Self {
|
||||
AppState {
|
||||
@@ -56,6 +67,10 @@ impl AppState {
|
||||
startup_time: Instant::now(),
|
||||
cached_load: CachedNodeLoad::new(load_cache_ttl),
|
||||
metrics: MetricsAppState { metrics, verloc },
|
||||
upgrade_mode_state: UpgradeModeApiState {
|
||||
node_state: upgrade_mode_state,
|
||||
attestation_url: upgrade_mode_attestation_url,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ use crate::node::shared_network::{
|
||||
CachedNetwork, CachedTopologyProvider, LocalGatewayNode, NetworkRefresher,
|
||||
};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_credential_verification::UpgradeModeState;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender};
|
||||
use nym_mixnet_client::client::ActiveConnections;
|
||||
@@ -373,6 +374,8 @@ pub(crate) struct NymNode {
|
||||
|
||||
entry_gateway: GatewayTasksData,
|
||||
|
||||
upgrade_mode_state: UpgradeModeState,
|
||||
|
||||
#[allow(dead_code)]
|
||||
service_providers: ServiceProvidersData,
|
||||
|
||||
@@ -461,6 +464,9 @@ impl NymNode {
|
||||
metrics: NymNodeMetrics::new(),
|
||||
verloc_stats: Default::default(),
|
||||
entry_gateway: GatewayTasksData::new(&config.gateway_tasks).await?,
|
||||
upgrade_mode_state: UpgradeModeState::new(
|
||||
config.gateway_tasks.upgrade_mode.attester_public_key,
|
||||
),
|
||||
service_providers: ServiceProvidersData::new(&config.service_providers)?,
|
||||
wireguard: Some(wireguard_data),
|
||||
config,
|
||||
@@ -625,7 +631,7 @@ impl NymNode {
|
||||
self.metrics.clone(),
|
||||
self.entry_gateway.mnemonic.clone(),
|
||||
Self::user_agent(),
|
||||
self.config.gateway_tasks.upgrade_mode.attester_public_key,
|
||||
self.upgrade_mode_state.clone(),
|
||||
self.shutdown_tracker().clone(),
|
||||
);
|
||||
|
||||
@@ -861,6 +867,12 @@ impl NymNode {
|
||||
self.active_sphinx_keys()?.clone(),
|
||||
self.metrics.clone(),
|
||||
self.verloc_stats.clone(),
|
||||
self.config
|
||||
.gateway_tasks
|
||||
.upgrade_mode
|
||||
.attestation_url
|
||||
.clone(),
|
||||
self.upgrade_mode_state.clone(),
|
||||
self.config.http.node_load_cache_ttl,
|
||||
);
|
||||
|
||||
@@ -1152,7 +1164,7 @@ impl NymNode {
|
||||
}
|
||||
|
||||
pub(crate) async fn run_minimal_mixnet_processing(mut self) -> Result<(), NymNodeError> {
|
||||
let noise_config = nym_noise::config::NoiseConfig::new(
|
||||
let noise_config = NoiseConfig::new(
|
||||
self.x25519_noise_keys.clone(),
|
||||
NoiseNetworkView::new_empty(),
|
||||
self.config.mixnet.debug.initial_connection_timeout,
|
||||
|
||||
Generated
+3
-1
@@ -4466,13 +4466,13 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"celes",
|
||||
"humantime 2.2.0",
|
||||
"humantime-serde",
|
||||
"nym-bin-common",
|
||||
"nym-crypto",
|
||||
"nym-exit-policy",
|
||||
"nym-http-api-client",
|
||||
"nym-noise-keys",
|
||||
"nym-upgrade-mode-check",
|
||||
"nym-wireguard-types",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4481,6 +4481,7 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"url",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
@@ -4606,6 +4607,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -26,7 +26,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { workspace = true }
|
||||
bytecodec = { workspace = true }
|
||||
nym-sdk = { path = "../../sdk/rust/nym-sdk/" }
|
||||
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
|
||||
bytes.workspace = true
|
||||
dirs.workspace = true
|
||||
clap.workspace = true
|
||||
|
||||
@@ -213,12 +213,14 @@ impl NetworkManager {
|
||||
id,
|
||||
"--enabled-credentials-mode",
|
||||
"true",
|
||||
"--minimum-gateway-performance",
|
||||
"0",
|
||||
"--port",
|
||||
&port.to_string(),
|
||||
])
|
||||
.stdout(Stdio::null())
|
||||
// .stdout(Stdio::null())
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
// .stderr(Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
|
||||
if let Some(gateway) = &ctx.gateway {
|
||||
@@ -243,10 +245,10 @@ impl NetworkManager {
|
||||
config_file,
|
||||
r#"
|
||||
|
||||
[debug.topology]
|
||||
minimum_mixnode_performance = 0
|
||||
minimum_gateway_performance = 0
|
||||
"#
|
||||
[debug.topology]
|
||||
minimum_mixnode_performance = 0
|
||||
minimum_gateway_performance = 0
|
||||
"#
|
||||
)?;
|
||||
|
||||
ctx.println(format!("\t✅client {id} is ready to use!"));
|
||||
|
||||
@@ -577,7 +577,7 @@ impl NetworkManager {
|
||||
));
|
||||
let id = ctx.nym_node_id(mixnode);
|
||||
cmds.push(format!(
|
||||
"{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise"
|
||||
"{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -588,7 +588,7 @@ impl NetworkManager {
|
||||
));
|
||||
let id = ctx.nym_node_id(gateway);
|
||||
cmds.push(format!(
|
||||
"{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise"
|
||||
"{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection"
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user