Merge pull request #4667 from nymtech/feature/authenticator

Add authenticator
This commit is contained in:
Tommy Verrall
2024-07-10 10:14:58 +02:00
committed by GitHub
68 changed files with 4060 additions and 877 deletions
Generated
+50
View File
@@ -3950,6 +3950,54 @@ dependencies = [
"tokio",
]
[[package]]
name = "nym-authenticator"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"bs58 0.5.1",
"bytes",
"clap 4.5.4",
"fastrand 2.1.0",
"futures",
"ipnetwork 0.16.0",
"log",
"nym-authenticator-requests",
"nym-bin-common",
"nym-client-core",
"nym-config",
"nym-crypto",
"nym-id",
"nym-network-defaults",
"nym-sdk",
"nym-service-providers-common",
"nym-sphinx",
"nym-task",
"nym-types",
"nym-wireguard",
"nym-wireguard-types",
"rand 0.8.5",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tokio-util",
"url",
]
[[package]]
name = "nym-authenticator-requests"
version = "0.1.0"
dependencies = [
"bincode",
"nym-sphinx",
"nym-wireguard-types",
"rand 0.8.5",
"serde",
]
[[package]]
name = "nym-bandwidth-controller"
version = "0.1.0"
@@ -4514,6 +4562,7 @@ dependencies = [
"ipnetwork 0.16.0",
"log",
"nym-api-requests",
"nym-authenticator",
"nym-bin-common",
"nym-config",
"nym-credentials",
@@ -4964,6 +5013,7 @@ dependencies = [
"cupid",
"humantime-serde",
"ipnetwork 0.16.0",
"nym-authenticator",
"nym-bin-common",
"nym-client-core-config-types",
"nym-config",
+2
View File
@@ -20,6 +20,7 @@ members = [
"clients/native",
"clients/native/websocket-requests",
"clients/socks5",
"common/authenticator-requests",
"common/async-file-watcher",
"common/bandwidth-controller",
"common/bin-common",
@@ -95,6 +96,7 @@ members = [
"mixnode",
"sdk/lib/socks5-listener",
"sdk/rust/nym-sdk",
"service-providers/authenticator",
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "nym-authenticator-requests"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
bincode = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
nym-sphinx = { path = "../nymsphinx" }
nym-wireguard-types = { path = "../wireguard-types" }
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod v1;
pub const CURRENT_VERSION: u8 = 1;
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
@@ -0,0 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod request;
pub mod response;
const VERSION: u8 = 1;
@@ -0,0 +1,70 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sphinx::addressing::Recipient;
use nym_wireguard_types::{GatewayClient, InitMessage};
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
use super::VERSION;
fn generate_random() -> u64 {
use rand::RngCore;
let mut rng = rand::rngs::OsRng;
rng.next_u64()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthenticatorRequest {
pub version: u8,
pub data: AuthenticatorRequestData,
pub reply_to: Recipient,
pub request_id: u64,
}
impl AuthenticatorRequest {
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn new_initial_request(init_message: InitMessage, reply_to: Recipient) -> (Self, u64) {
let request_id = generate_random();
(
Self {
version: VERSION,
data: AuthenticatorRequestData::Initial(init_message),
reply_to,
request_id,
},
request_id,
)
}
pub fn new_final_request(gateway_client: GatewayClient, reply_to: Recipient) -> (Self, u64) {
let request_id = generate_random();
(
Self {
version: VERSION,
data: AuthenticatorRequestData::Final(gateway_client),
reply_to,
request_id,
},
request_id,
)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AuthenticatorRequestData {
Initial(InitMessage),
Final(GatewayClient),
}
@@ -0,0 +1,88 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sphinx::addressing::Recipient;
use nym_wireguard_types::registration::RegistrationData;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
use super::VERSION;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthenticatorResponse {
pub version: u8,
pub data: AuthenticatorResponseData,
pub reply_to: Recipient,
}
impl AuthenticatorResponse {
pub fn new_pending_registration_success(
registration_data: RegistrationData,
request_id: u64,
reply_to: Recipient,
) -> Self {
Self {
version: VERSION,
data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse {
reply: registration_data,
reply_to,
request_id,
}),
reply_to,
}
}
pub fn new_registered(reply_to: Recipient, request_id: u64) -> Self {
Self {
version: VERSION,
data: AuthenticatorResponseData::Registered(RegisteredResponse {
reply_to,
request_id,
}),
reply_to,
}
}
pub fn recipient(&self) -> Recipient {
self.reply_to
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn id(&self) -> Option<u64> {
match &self.data {
AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id),
AuthenticatorResponseData::Registered(response) => Some(response.request_id),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AuthenticatorResponseData {
PendingRegistration(PendingRegistrationResponse),
Registered(RegisteredResponse),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PendingRegistrationResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistrationData,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
}
+1 -1
View File
@@ -14,7 +14,7 @@ bytes = { workspace = true }
nym-bin-common = { path = "../bin-common" }
nym-crypto = { path = "../crypto" }
nym-sphinx = { path = "../nymsphinx" }
rand = "0.8.5"
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
time = { workspace = true }
+1 -2
View File
@@ -10,8 +10,7 @@ pub use config::Config;
pub use error::Error;
pub use public_key::PeerPublicKey;
pub use registration::{
ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, GatewayClientRegistry,
InitMessage, Nonce,
ClientMac, ClientMessage, GatewayClient, GatewayClientRegistry, InitMessage, Nonce,
};
#[cfg(feature = "verify")]
+3 -11
View File
@@ -7,6 +7,7 @@ use base64::{engine::general_purpose, Engine};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::time::SystemTime;
use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")]
@@ -18,13 +19,13 @@ use sha2::Sha256;
pub type GatewayClientRegistry = DashMap<PeerPublicKey, GatewayClient>;
pub type PendingRegistrations = DashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = DashMap<IpAddr, Free>;
pub type PrivateIPs = DashMap<IpAddr, Taken>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Free = bool;
pub type Taken = Option<SystemTime>;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "camelCase")]
@@ -53,15 +54,6 @@ impl InitMessage {
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub enum ClientRegistrationResponse {
PendingRegistration(RegistrationData),
Registered,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RegistrationData {
pub nonce: u64,
pub gateway_data: GatewayClient,
+1
View File
@@ -58,6 +58,7 @@ zeroize = { workspace = true }
# internal
nym-authenticator = { path = "../service-providers/authenticator" }
nym-api-requests = { path = "../nym-api/nym-api-requests" }
nym-bin-common = { path = "../common/bin-common", features = ["output_format"] }
nym-config = { path = "../common/config" }
+22 -3
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::storage::error::StorageError;
use nym_authenticator::error::AuthenticatorError;
use nym_ip_packet_router::error::IpPacketRouterError;
use nym_network_requester::error::{ClientCoreError, NetworkRequesterError};
use nym_validator_client::nyxd::error::NyxdError;
@@ -60,6 +61,16 @@ pub enum GatewayError {
source: io::Error,
},
#[error(
"failed to load config file for authenticator (gateway-id: '{id}') using path '{}'. detailed message: {source}",
path.display()
)]
AuthenticatorConfigLoadFailure {
id: String,
path: PathBuf,
source: io::Error,
},
#[error(
"failed to load config file for wireguard (gateway-id: '{id}') using path '{}'. detailed message: {source}",
path.display()
@@ -110,6 +121,9 @@ pub enum GatewayError {
#[error("Path to ip packet router configuration file hasn't been specified. Perhaps try to run `setup-ip-packet-router`?")]
UnspecifiedIpPacketRouterConfig,
#[error("Path to authenticator configuration file hasn't been specified. Perhaps try to run `setup-authenticator`?")]
UnspecifiedAuthenticatorConfig,
#[error("there was an issue with the local network requester: {source}")]
NetworkRequesterFailure {
#[from]
@@ -122,6 +136,12 @@ pub enum GatewayError {
source: IpPacketRouterError,
},
#[error("there was an issue with the local authenticator: {source}")]
AuthenticatorFailure {
#[from]
source: AuthenticatorError,
},
#[error("failed to startup local network requester")]
NetworkRequesterStartupFailure,
@@ -174,9 +194,8 @@ pub enum GatewayError {
#[error("wireguard not set")]
WireguardNotSet,
#[cfg(all(feature = "wireguard", target_os = "linux"))]
#[error("failed to catch an interrupt: {source}")]
StdError {
#[error("failed to start authenticator: {source}")]
AuthenticatorStartError {
source: Box<dyn std::error::Error + Send + Sync>,
},
}
+1 -1
View File
@@ -257,7 +257,7 @@ impl<'a> HttpApiBuilder<'a> {
}
let bind_address = self.gateway_config.http.bind_address;
let router = nym_node_http_api::NymNodeRouter::new(config, None, None);
let router = nym_node_http_api::NymNodeRouter::new(config, None);
tokio::spawn(async move {
let server = match router.build_server(&bind_address).await {
+40 -15
View File
@@ -89,7 +89,7 @@ pub async fn create_gateway(
let ip_opts = ip_packet_router_config.map(|config| LocalIpPacketRouterOpts {
config,
custom_mixnet_path: custom_mixnet,
custom_mixnet_path: custom_mixnet.clone(),
});
Gateway::new(config, nr_opts, ip_opts, storage)
@@ -109,6 +109,13 @@ pub struct LocalIpPacketRouterOpts {
pub custom_mixnet_path: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct LocalAuthenticatorOpts {
pub config: nym_authenticator::Config,
pub custom_mixnet_path: Option<PathBuf>,
}
pub struct Gateway<St = PersistentStorage> {
config: Config,
@@ -116,6 +123,10 @@ pub struct Gateway<St = PersistentStorage> {
ip_packet_router_opts: Option<LocalIpPacketRouterOpts>,
// Use None when wireguard feature is not enabled too
#[allow(dead_code)]
authenticator_opts: Option<LocalAuthenticatorOpts>,
/// ed25519 keypair used to assert one's identity.
identity_keypair: Arc<identity::KeyPair>,
@@ -146,6 +157,7 @@ impl<St> Gateway<St> {
config,
network_requester_opts,
ip_packet_router_opts,
authenticator_opts: None,
#[cfg(all(feature = "wireguard", target_os = "linux"))]
wireguard_data: None,
run_http_server: true,
@@ -157,6 +169,7 @@ impl<St> Gateway<St> {
config: Config,
network_requester_opts: Option<LocalNetworkRequesterOpts>,
ip_packet_router_opts: Option<LocalIpPacketRouterOpts>,
authenticator_opts: Option<LocalAuthenticatorOpts>,
identity_keypair: Arc<identity::KeyPair>,
sphinx_keypair: Arc<encryption::KeyPair>,
storage: St,
@@ -165,6 +178,7 @@ impl<St> Gateway<St> {
config,
network_requester_opts,
ip_packet_router_opts,
authenticator_opts,
identity_keypair,
sphinx_keypair,
storage,
@@ -222,11 +236,18 @@ impl<St> Gateway<St> {
}
#[cfg(all(feature = "wireguard", target_os = "linux"))]
async fn start_wireguard(
async fn start_authenticator(
&mut self,
opts: &LocalAuthenticatorOpts,
shutdown: TaskClient,
) -> Result<Arc<nym_wireguard::WgApiWrapper>, Box<dyn std::error::Error + Send + Sync>> {
if let Some(wireguard_data) = self.wireguard_data.take() {
let authenticator_server = nym_authenticator::Authenticator::new(
opts.config.clone(),
wireguard_data.inner.clone(),
)
.with_shutdown(shutdown.fork("authenticator"));
tokio::spawn(async move { authenticator_server.run_service_provider().await });
nym_wireguard::start_wireguard(shutdown, wireguard_data).await
} else {
Err(Box::new(GatewayError::WireguardNotSet))
@@ -234,8 +255,12 @@ impl<St> Gateway<St> {
}
#[cfg(all(feature = "wireguard", not(target_os = "linux")))]
async fn start_wireguard(&self, _shutdown: TaskClient) {
nym_wireguard::start_wireguard().await
async fn start_authenticator(
&self,
_opts: &LocalAuthenticatorOpts,
_shutdown: TaskClient,
) -> Result<Arc<nym_wireguard::WgApiWrapper>, Box<dyn std::error::Error + Send + Sync>> {
todo!("Authenticator is currently only supported on Linux");
}
fn start_client_websocket_listener(
@@ -538,6 +563,17 @@ impl<St> Gateway<St> {
info!("embedded ip packet router is disabled");
};
#[cfg(feature = "wireguard")]
let _wg_api = if let Some(opts) = self.authenticator_opts.clone() {
Some(
self.start_authenticator(&opts, shutdown.fork("wireguard"))
.await
.map_err(|source| GatewayError::AuthenticatorStartError { source })?,
)
} else {
None
};
if self.run_http_server {
HttpApiBuilder::new(
&self.config,
@@ -550,17 +586,6 @@ impl<St> Gateway<St> {
.start(shutdown.fork("http-api"))?;
}
// Once this is a bit more mature, make this a commandline flag instead of a compile time
// flag
#[cfg(all(feature = "wireguard", target_os = "linux"))]
let _wg_api = self
.start_wireguard(shutdown.fork("wireguard"))
.await
.map_err(|source| GatewayError::StdError { source })?;
#[cfg(all(feature = "wireguard", not(target_os = "linux")))]
self.start_wireguard(shutdown.fork("wireguard")).await;
info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!");
info!(
+1 -1
View File
@@ -103,7 +103,7 @@ impl<'a> HttpApiBuilder<'a> {
.with_mixnode(load_mixnode_details(self.mixnode_config)?)
.with_landing_page_assets(self.mixnode_config.http.landing_page_assets_path.as_ref());
let router = nym_node_http_api::NymNodeRouter::new(config, None, None);
let router = nym_node_http_api::NymNodeRouter::new(config, None);
tokio::spawn(async move {
let server = match router.build_server(&bind_address).await {
+9
View File
@@ -606,6 +606,9 @@ pub struct NymNodeDescription {
#[serde(default)]
pub ip_packet_router: Option<IpPacketRouterDetails>,
#[serde(default)]
pub authenticator: Option<AuthenticatorDetails>,
// for now we only care about their ws/wss situation, nothing more
pub mixnet_websockets: WebSockets,
@@ -657,6 +660,12 @@ pub struct IpPacketRouterDetails {
pub address: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct AuthenticatorDetails {
/// address of the embedded authenticator
pub address: String,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ApiHealthResponse {
pub status: ApiStatus,
+10 -1
View File
@@ -8,7 +8,7 @@ use crate::support::config;
use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE;
use futures::{stream, StreamExt};
use nym_api_requests::models::{
IpPacketRouterDetails, NetworkRequesterDetails, NymNodeDescription,
AuthenticatorDetails, IpPacketRouterDetails, NetworkRequesterDetails, NymNodeDescription,
};
use nym_api_requests::nym_nodes::NodeRole;
use nym_config::defaults::{mainnet, DEFAULT_NYM_NODE_HTTP_PORT};
@@ -189,12 +189,21 @@ async fn try_get_description(
None
};
let authenticator = if let Ok(auth) = client.get_authenticator().await {
Some(AuthenticatorDetails {
address: auth.address,
})
} else {
None
};
let description = NymNodeDescription {
host_information: host_info.data.into(),
last_polled: OffsetDateTime::now_utc().into(),
build_information: build_info,
network_requester,
ip_packet_router,
authenticator,
mixnet_websockets: websockets.into(),
auxiliary_details,
role: data.role(),
+1
View File
@@ -55,6 +55,7 @@ nym-wireguard-types = { path = "../common/wireguard-types", default-features = f
# nodes:
nym-mixnode = { path = "../mixnode" }
nym-gateway = { path = "../gateway" }
nym-authenticator = { path = "../service-providers/authenticator" }
nym-network-requester = { path = "../service-providers/network-requester" }
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
@@ -1,7 +1,6 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::api::v1::gateway::client_interfaces::wireguard::WireguardAppState;
use crate::state::AppState;
use axum::Router;
use nym_node_requests::routes;
@@ -17,9 +16,6 @@ pub struct Config {
pub v1_config: v1::Config,
}
pub(super) fn routes(config: Config, initial_wg_state: WireguardAppState) -> Router<AppState> {
Router::new().nest(
routes::api::V1,
v1::routes(config.v1_config, initial_wg_state),
)
pub(super) fn routes(config: Config) -> Router<AppState> {
Router::new().nest(routes::api::V1, v1::routes(config.v1_config))
}
@@ -0,0 +1,23 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use axum::routing::get;
use axum::Router;
use nym_node_requests::api::v1::authenticator::models;
pub mod root;
#[derive(Debug, Clone, Default)]
pub struct Config {
pub details: Option<models::Authenticator>,
}
pub(crate) fn routes<S: Send + Sync + 'static + Clone>(config: Config) -> Router<S> {
Router::new().route(
"/",
get({
let authenticator_details = config.details;
move |query| root::root_authenticator(authenticator_details, query)
}),
)
}
@@ -0,0 +1,33 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::router::api::{FormattedResponse, OutputParams};
use axum::extract::Query;
use axum::http::StatusCode;
use nym_node_requests::api::v1::authenticator::models::Authenticator;
/// Returns root authenticator information
#[utoipa::path(
get,
path = "",
context_path = "/api/v1/authenticator",
tag = "Authenticator",
responses(
(status = 501, description = "the endpoint hasn't been implemented yet"),
(status = 200, content(
("application/json" = Authenticator),
("application/yaml" = Authenticator)
))
),
params(OutputParams)
)]
pub(crate) async fn root_authenticator(
details: Option<Authenticator>,
Query(output): Query<OutputParams>,
) -> Result<AuthenticatorResponse, StatusCode> {
let details = details.ok_or(StatusCode::NOT_IMPLEMENTED)?;
let output = output.output.unwrap_or_default();
Ok(output.to_response(details))
}
pub type AuthenticatorResponse = FormattedResponse<Authenticator>;
@@ -1,20 +1,16 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::api::v1::gateway::client_interfaces::wireguard::WireguardAppState;
use crate::api::{FormattedResponse, OutputParams};
use axum::extract::Query;
use axum::http::StatusCode;
use axum::routing::get;
use axum::Router;
use nym_node_requests::api::v1::gateway::models::{ClientInterfaces, WebSockets, Wireguard};
use nym_node_requests::api::v1::gateway::models::{ClientInterfaces, WebSockets};
use nym_node_requests::routes::api::v1::gateway::client_interfaces;
pub(crate) mod wireguard;
pub(crate) fn routes<S: Send + Sync + 'static + Clone>(
interfaces: Option<ClientInterfaces>,
initial_wg_state: WireguardAppState,
) -> Router<S> {
Router::new()
.route(
@@ -31,17 +27,6 @@ pub(crate) fn routes<S: Send + Sync + 'static + Clone>(
move |query| mixnet_websockets(websockets, query)
}),
)
.nest(
client_interfaces::WIREGUARD,
wireguard::routes(initial_wg_state),
)
.route(
client_interfaces::WIREGUARD,
get({
let wireguard_cfg_info = interfaces.and_then(|i| i.wireguard);
move |query| wireguard_info(wireguard_cfg_info, query)
}),
)
}
/// Returns client interfaces supported by this gateway.
@@ -70,32 +55,6 @@ pub(crate) async fn client_interfaces(
pub type ClientInterfacesResponse = FormattedResponse<ClientInterfaces>;
/// Returns client interfaces supported by this gateway.
#[utoipa::path(
get,
path = "/wireguard",
context_path = "/api/v1/gateway/client-interfaces",
tag = "Gateway",
responses(
(status = 501, description = "the endpoint hasn't been implemented yet"),
(status = 200, content(
("application/json" = Wireguard),
("application/yaml" = Wireguard)
))
),
params(OutputParams)
)]
pub(crate) async fn wireguard_info(
wireguard: Option<Wireguard>,
Query(output): Query<OutputParams>,
) -> Result<WireguardResponse, StatusCode> {
let wireguard = wireguard.ok_or(StatusCode::NOT_IMPLEMENTED)?;
let output = output.output.unwrap_or_default();
Ok(output.to_response(wireguard))
}
pub type WireguardResponse = FormattedResponse<Wireguard>;
/// Returns client interfaces supported by this gateway.
#[utoipa::path(
get,
@@ -1,265 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use super::error::WireguardError;
use crate::api::v1::gateway::client_interfaces::wireguard::{
WireguardAppState, WireguardAppStateInner,
};
use crate::api::{FormattedResponse, OutputParams};
use crate::router::types::RequestError;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::Json;
use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::{
ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, PeerPublicKey,
};
use nym_wireguard_types::registration::RegistrationData;
use rand::{prelude::IteratorRandom, thread_rng};
fn remove_from_registry(
state: &WireguardAppStateInner,
remote_public: &PeerPublicKey,
gateway_client: &GatewayClient,
) -> Result<(), RequestError> {
state
.wireguard_gateway_data
.remove_peer(gateway_client)
.map_err(|err| RequestError::from_err(err, StatusCode::INTERNAL_SERVER_ERROR))?;
state
.wireguard_gateway_data
.client_registry()
.remove(remote_public);
Ok(())
}
async fn process_final_message(
client: GatewayClient,
state: &WireguardAppStateInner,
) -> Result<ClientRegistrationResponse, RequestError> {
let registration_data = state
.registration_in_progress
.get(&client.pub_key())
.ok_or(RequestError::from_err(
WireguardError::RegistrationNotInProgress,
StatusCode::BAD_REQUEST,
))?
.value()
.clone();
if client
.verify(
state.wireguard_gateway_data.keypair().private_key(),
registration_data.nonce,
)
.is_ok()
{
state
.wireguard_gateway_data
.add_peer(&client)
.map_err(|err| RequestError::from_err(err, StatusCode::INTERNAL_SERVER_ERROR))?;
state.registration_in_progress.remove(&client.pub_key());
state
.wireguard_gateway_data
.client_registry()
.insert(client.pub_key(), client);
Ok(ClientRegistrationResponse::Registered)
} else {
Err(RequestError::from_err(
WireguardError::MacVerificationFailure,
StatusCode::BAD_REQUEST,
))
}
}
async fn process_init_message(
init_message: InitMessage,
state: &WireguardAppStateInner,
) -> Result<ClientRegistrationResponse, RequestError> {
let remote_public = init_message.pub_key();
let nonce: u64 = fastrand::u64(..);
if let Some(registration_data) = state.registration_in_progress.get(&remote_public) {
return Ok(ClientRegistrationResponse::PendingRegistration(
registration_data.value().clone(),
));
}
let gateway_client_opt = if let Some(gateway_client) = state
.wireguard_gateway_data
.client_registry()
.get(&remote_public)
{
let mut private_ip_ref = state
.free_private_network_ips
.get_mut(&gateway_client.private_ip)
.ok_or(RequestError::new(
"Internal data corruption",
StatusCode::INTERNAL_SERVER_ERROR,
))?;
*private_ip_ref = true;
Some(gateway_client.clone())
} else {
None
};
if let Some(gateway_client) = gateway_client_opt {
remove_from_registry(state, &remote_public, &gateway_client)?;
}
let mut private_ip_ref = state
.free_private_network_ips
.iter_mut()
.filter(|r| **r)
.choose(&mut thread_rng())
.ok_or(RequestError::new(
"No more space in the network",
StatusCode::SERVICE_UNAVAILABLE,
))?;
// mark it as used, even though it's not final
*private_ip_ref = false;
let gateway_data = GatewayClient::new(
state.wireguard_gateway_data.keypair().private_key(),
remote_public.inner(),
*private_ip_ref.key(),
nonce,
);
let registration_data = RegistrationData {
nonce,
gateway_data,
wg_port: state.binding_port,
};
state
.registration_in_progress
.insert(remote_public, registration_data.clone());
Ok(ClientRegistrationResponse::PendingRegistration(
registration_data,
))
}
/// Perform wireguard client registration.
#[utoipa::path(
post,
path = "/client",
context_path = "/api/v1/gateway/client-interfaces/wireguard",
tag = "Wireguard (EXPERIMENTAL AND UNSTABLE)",
request_body(
content = ClientMessage,
description = "Data used for proceeding with client wireguard registration",
content_type = "application/json"
),
responses(
(status = 501, body = ErrorResponse, description = "the endpoint hasn't been implemented yet"),
(status = 400, body = ErrorResponse),
(status = 200, content(
("application/json" = ClientRegistrationResponse),
("application/yaml" = ClientRegistrationResponse)
))
),
params(OutputParams)
)]
pub(crate) async fn register_client(
State(state): State<WireguardAppState>,
Query(output): Query<OutputParams>,
Json(payload): Json<ClientMessage>,
) -> Result<RegisterClientResponse, RequestError> {
let output = output.output.unwrap_or_default();
let Some(state) = state.inner() else {
return Err(RequestError::new_status(StatusCode::NOT_IMPLEMENTED));
};
let response = match payload {
ClientMessage::Initial(init) => process_init_message(init, state).await?,
ClientMessage::Final(finalize) => process_final_message(finalize, state).await?,
};
Ok(output.to_response(response))
}
pub type RegisterClientResponse = FormattedResponse<ClientRegistrationResponse>;
/// Get public keys of all registered wireguard clients.
#[utoipa::path(
get,
path = "/clients",
context_path = "/api/v1/gateway/client-interfaces/wireguard",
tag = "Wireguard (EXPERIMENTAL AND UNSTABLE)",
responses(
(status = 501, body = ErrorResponse, description = "the endpoint hasn't been implemented yet"),
(status = 200, content(
("application/json" = Vec<String>),
("application/yaml" = Vec<String>)
))
),
params(OutputParams)
)]
pub(crate) async fn get_all_clients(
Query(output): Query<OutputParams>,
State(state): State<WireguardAppState>,
) -> Result<AllClientsResponse, RequestError> {
let output = output.output.unwrap_or_default();
let Some(state) = state.inner() else {
return Err(RequestError::new_status(StatusCode::NOT_IMPLEMENTED));
};
let clients = state
.wireguard_gateway_data
.client_registry()
.iter()
.map(|c| c.pub_key())
.collect::<Vec<PeerPublicKey>>();
Ok(output.to_response(clients))
}
pub type AllClientsResponse = FormattedResponse<Vec<PeerPublicKey>>;
/// Get client details of the registered wireguard client by its public key.
#[utoipa::path(
get,
path = "/client/{pub_key}",
context_path = "/api/v1/gateway/client-interfaces/wireguard",
tag = "Wireguard (EXPERIMENTAL AND UNSTABLE)",
params(
("pub_key", Path, description = "The public key of the client"),
OutputParams
),
responses(
(status = 501, body = ErrorResponse, description = "the endpoint hasn't been implemented yet"),
(status = 404, body = ErrorResponse, description = "there are no clients with the provided public key"),
(status = 400, body = ErrorResponse),
(status = 200, content(
("application/json" = Vec<GatewayClient>),
("application/yaml" = Vec<GatewayClient>)
))
),
)]
pub(crate) async fn get_client(
Path(pub_key): Path<PeerPublicKey>,
Query(output): Query<OutputParams>,
State(state): State<WireguardAppState>,
) -> Result<ClientResponse, RequestError> {
let output = output.output.unwrap_or_default();
let Some(state) = state.inner() else {
return Err(RequestError::new_status(StatusCode::NOT_IMPLEMENTED));
};
let clients = state
.wireguard_gateway_data
.client_registry()
.iter()
.filter_map(|c| {
if c.pub_key() == pub_key {
Some(c.clone())
} else {
None
}
})
.collect::<Vec<GatewayClient>>();
if clients.is_empty() {
return Err(RequestError::new_status(StatusCode::NOT_FOUND));
}
Ok(output.to_response(clients))
}
pub type ClientResponse = FormattedResponse<Vec<GatewayClient>>;
@@ -1,13 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Debug, Error)]
pub enum WireguardError {
#[error("the client is currently not in the process of being registered")]
RegistrationNotInProgress,
#[error("the client mac failed to get verified correctly")]
MacVerificationFailure,
}
@@ -1,296 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::api::v1::gateway::client_interfaces::wireguard::client_registry::{
get_all_clients, get_client, register_client,
};
use crate::error::NymNodeHttpError;
use axum::routing::{get, post};
use axum::Router;
use ipnetwork::IpNetwork;
use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard;
use nym_wireguard::WireguardGatewayData;
use nym_wireguard_types::registration::PendingRegistrations;
use nym_wireguard_types::registration::PrivateIPs;
use std::sync::Arc;
pub(crate) mod client_registry;
mod error;
// I don't see any reason why this state should be accessible to any routes outside /wireguard
// if anyone finds compelling reason, it could be moved to the `AppState` struct instead
#[derive(Clone, Default)]
pub struct WireguardAppState {
inner: Option<WireguardAppStateInner>,
}
impl WireguardAppState {
pub fn new(
wireguard_gateway_data: WireguardGatewayData,
registration_in_progress: Arc<PendingRegistrations>,
binding_port: u16,
private_ip_network: IpNetwork,
) -> Result<Self, NymNodeHttpError> {
Ok(WireguardAppState {
inner: Some(WireguardAppStateInner {
wireguard_gateway_data,
registration_in_progress,
binding_port,
free_private_network_ips: Arc::new(
private_ip_network.iter().map(|ip| (ip, true)).collect(),
),
}),
})
}
// #[allow(dead_code)]
// pub(crate) fn dh_keypair(&self) -> Option<&encryption::KeyPair> {
// self.inner.as_ref().map(|s| s.dh_keypair.as_ref())
// }
//
// #[allow(dead_code)]
// pub(crate) fn client_registry(&self) -> Option<&RwLock<ClientRegistry>> {
// self.inner.as_ref().map(|s| s.client_registry.as_ref())
// }
//
// #[allow(dead_code)]
// pub(crate) fn registration_in_progress(&self) -> Option<&RwLock<PendingRegistrations>> {
// self.inner
// .as_ref()
// .map(|s| s.registration_in_progress.as_ref())
// }
// not sure what to feel about exposing this method
pub(crate) fn inner(&self) -> Option<&WireguardAppStateInner> {
self.inner.as_ref()
}
}
// helper macro to deal with missing wg state (if not being exposed by the node)
#[macro_export]
macro_rules! get_state {
( $state: ident, $field: ident ) => {{
let Some(ref inner) = $state.inner else {
return ::axum::http::StatusCode::NOT_IMPLEMENTED;
};
inner.$field.as_ref()
}};
}
#[derive(Clone)]
pub(crate) struct WireguardAppStateInner {
wireguard_gateway_data: WireguardGatewayData,
registration_in_progress: Arc<PendingRegistrations>,
binding_port: u16,
free_private_network_ips: Arc<PrivateIPs>,
}
pub(crate) fn routes<S>(initial_state: WireguardAppState) -> Router<S> {
Router::new()
// .route("/", get())
.route(wireguard::CLIENTS, get(get_all_clients))
.route(wireguard::CLIENT, post(register_client))
.route(&format!("{}/:pub_key", wireguard::CLIENT), get(get_client))
.with_state(initial_state)
}
#[cfg(test)]
mod test {
use crate::api::v1::gateway::client_interfaces::wireguard::{
routes, WireguardAppState, WireguardAppStateInner,
};
use axum::body::to_bytes;
use axum::body::Body;
use axum::http::Request;
use axum::http::StatusCode;
use base64::{engine::general_purpose, Engine as _};
use dashmap::DashMap;
use hmac::Mac;
use ipnetwork::IpNetwork;
use nym_crypto::asymmetric::encryption;
use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::{
ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage,
PeerPublicKey,
};
use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard;
use nym_wireguard::{peer_controller::PeerControlMessage, WireguardGatewayData};
use nym_wireguard_types::registration::{HmacSha256, RegistrationData};
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc;
use tower::Service;
use tower::ServiceExt;
use x25519_dalek::{PublicKey, StaticSecret};
const PRIVATE_KEY: &str = "AEqXrLFT4qjYq3wmX0456iv94uM6nDj5ugp6Jedcflg=";
fn decode_base64_key(base64_key: &str) -> [u8; 32] {
general_purpose::STANDARD
.decode(base64_key)
.unwrap()
.try_into()
.unwrap()
}
fn server_static_private_key() -> x25519_dalek::StaticSecret {
// TODO: this is a temporary solution for development
let static_private_bytes: [u8; 32] = decode_base64_key(PRIVATE_KEY);
x25519_dalek::StaticSecret::from(static_private_bytes)
}
#[tokio::test]
async fn registration() {
// 1. Provision random keys for gateway and client
// 2. Generate DH shared secret
// 3. Client submits its public key to the gateway to start the handshake process, gateway responds with nonce
// 4. Client generates mac digest using DH shared secret, its own public key, socket address and port, and nonce
// 5. Client sends its public key, socket address and port, nonce and mac digest to the gateway
// 6. Gateway verifies mac digest and nonce, and stores client's public key and socket address and port
let mut rng = rand::thread_rng();
let gateway_private_key =
encryption::PrivateKey::from_bytes(server_static_private_key().as_bytes()).unwrap();
let gateway_public_key = encryption::PublicKey::from(&gateway_private_key);
let gateway_key_pair = encryption::KeyPair::from_bytes(
&gateway_private_key.to_bytes(),
&gateway_public_key.to_bytes(),
)
.unwrap();
let client_key_pair = encryption::KeyPair::new(&mut rng);
let gateway_static_public = PublicKey::from(gateway_key_pair.public_key().to_bytes());
let client_static_private = StaticSecret::from(client_key_pair.private_key().to_bytes());
let client_static_public = PublicKey::from(client_key_pair.public_key().to_bytes());
let client_dh = client_static_private.diffie_hellman(&gateway_static_public);
let registration_in_progress = Arc::new(DashMap::new());
let free_private_network_ips = Arc::new(
IpNetwork::from_str("10.1.0.0/24")
.unwrap()
.iter()
.map(|ip| (ip, true))
.collect(),
);
let client_private_ip = IpAddr::from_str("10.1.0.42").unwrap();
let (wireguard_gateway_data, mut peer_rx) = WireguardGatewayData::new(
nym_wireguard_types::Config {
bind_address: "0.0.0.0:51822".parse().unwrap(),
private_ip: "10.1.0.1".parse().unwrap(),
announced_port: 51822,
private_network_prefix: 16,
},
Arc::new(gateway_key_pair),
);
let state = WireguardAppState {
inner: Some(WireguardAppStateInner {
wireguard_gateway_data: wireguard_gateway_data.clone(),
registration_in_progress: Arc::clone(&registration_in_progress),
binding_port: 8080,
free_private_network_ips,
}),
};
// `Router` implements `tower::Service<Request<Body>>` so we can
// call it like any tower service, no need to run an HTTP server.
let mut app = routes(state);
let init_message = ClientMessage::Initial(InitMessage {
pub_key: PeerPublicKey::new(client_static_public),
});
let init_request = Request::builder()
.method("POST")
.uri(wireguard::CLIENT)
.header("Content-type", "application/json")
.body(Body::from(serde_json::to_vec(&init_message).unwrap()))
.unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(init_request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert!(!registration_in_progress.is_empty());
let ClientRegistrationResponse::PendingRegistration(RegistrationData {
nonce,
gateway_data,
wg_port: 8080,
}) = serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap())
.unwrap()
else {
panic!("invalid response")
};
assert!(gateway_data
.verify(client_key_pair.private_key(), nonce)
.is_ok());
let mut mac = HmacSha256::new_from_slice(client_dh.as_bytes()).unwrap();
mac.update(client_static_public.as_bytes());
mac.update(client_private_ip.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
let mac = mac.finalize().into_bytes();
let finalized_message = ClientMessage::Final(GatewayClient {
pub_key: PeerPublicKey::new(client_static_public),
private_ip: client_private_ip,
mac: ClientMac::new(mac.as_slice().to_vec()),
});
let final_request = Request::builder()
.method("POST")
.uri(wireguard::CLIENT)
.header("Content-type", "application/json")
.body(Body::from(serde_json::to_vec(&finalized_message).unwrap()))
.unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(final_request)
.await
.unwrap();
let msg = peer_rx.recv().await.unwrap();
assert!(matches!(msg, PeerControlMessage::AddPeer(_)));
assert_eq!(response.status(), StatusCode::OK);
assert!(!wireguard_gateway_data.client_registry().is_empty());
let clients_request = Request::builder()
.method("GET")
.uri(wireguard::CLIENTS)
.body(Body::empty())
.unwrap();
let response = ServiceExt::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(clients_request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let clients: Vec<PeerPublicKey> =
serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap())
.unwrap();
assert!(!clients.is_empty());
assert_eq!(
wireguard_gateway_data
.client_registry()
.iter()
.map(|c| c.value().pub_key())
.collect::<Vec<PeerPublicKey>>(),
clients
)
}
}
@@ -1,7 +1,6 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::api::v1::gateway::client_interfaces::wireguard::WireguardAppState;
use axum::routing::get;
use axum::Router;
use nym_node_requests::api::v1::gateway::models;
@@ -15,10 +14,7 @@ pub struct Config {
pub details: Option<models::Gateway>,
}
pub(crate) fn routes<S: Send + Sync + 'static + Clone>(
config: Config,
initial_wg_state: WireguardAppState,
) -> Router<S> {
pub(crate) fn routes<S: Send + Sync + 'static + Clone>(config: Config) -> Router<S> {
Router::new()
.route(
"/",
@@ -29,9 +25,6 @@ pub(crate) fn routes<S: Send + Sync + 'static + Clone>(
)
.nest(
gateway::CLIENT_INTERFACES,
client_interfaces::routes(
config.details.map(|g| g.client_interfaces),
initial_wg_state,
),
client_interfaces::routes(config.details.map(|g| g.client_interfaces)),
)
}
@@ -1,12 +1,12 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::api::v1::gateway::client_interfaces::wireguard::WireguardAppState;
use crate::state::AppState;
use axum::routing::get;
use axum::Router;
use nym_node_requests::routes::api::v1;
pub mod authenticator;
pub mod gateway;
pub mod health;
pub mod ip_packet_router;
@@ -24,16 +24,14 @@ pub struct Config {
pub mixnode: mixnode::Config,
pub network_requester: network_requester::Config,
pub ip_packet_router: ip_packet_router::Config,
pub authenticator: authenticator::Config,
}
pub(super) fn routes(config: Config, initial_wg_state: WireguardAppState) -> Router<AppState> {
pub(super) fn routes(config: Config) -> Router<AppState> {
Router::new()
.route(v1::HEALTH, get(health::root_health))
.nest(v1::METRICS, metrics::routes(config.metrics))
.nest(
v1::GATEWAY,
gateway::routes(config.gateway, initial_wg_state),
)
.nest(v1::GATEWAY, gateway::routes(config.gateway))
.nest(v1::MIXNODE, mixnode::routes(config.mixnode))
.nest(
v1::NETWORK_REQUESTER,
@@ -43,6 +41,10 @@ pub(super) fn routes(config: Config, initial_wg_state: WireguardAppState) -> Rou
v1::IP_PACKET_ROUTER,
ip_packet_router::routes(config.ip_packet_router),
)
.nest(
v1::AUTHENTICATOR,
authenticator::routes(config.authenticator),
)
.merge(node::routes(config.node))
.merge(openapi::route())
}
@@ -26,11 +26,7 @@ use utoipa_swagger_ui::SwaggerUi;
api::v1::health::root_health,
api::v1::gateway::root::root_gateway,
api::v1::gateway::client_interfaces::client_interfaces,
api::v1::gateway::client_interfaces::wireguard_info,
api::v1::gateway::client_interfaces::mixnet_websockets,
api::v1::gateway::client_interfaces::wireguard::client_registry::register_client,
api::v1::gateway::client_interfaces::wireguard::client_registry::get_all_clients,
api::v1::gateway::client_interfaces::wireguard::client_registry::get_client,
api::v1::mixnode::root::root_mixnode,
api::v1::network_requester::root::root_network_requester,
api::v1::network_requester::exit_policy::node_exit_policy,
@@ -67,7 +63,6 @@ use utoipa_swagger_ui::SwaggerUi;
api_requests::v1::gateway::client_interfaces::wireguard::models::ClientMessage,
api_requests::v1::gateway::client_interfaces::wireguard::models::InitMessage,
api_requests::v1::gateway::client_interfaces::wireguard::models::GatewayClient,
api_requests::v1::gateway::client_interfaces::wireguard::models::ClientRegistrationResponse,
api_requests::v1::mixnode::models::Mixnode,
api_requests::v1::network_requester::models::NetworkRequester,
api_requests::v1::network_requester::exit_policy::models::AddressPolicy,
+10 -10
View File
@@ -1,7 +1,6 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub use crate::api::v1::gateway::client_interfaces::wireguard::WireguardAppState;
use crate::error::NymNodeHttpError;
use crate::middleware::logging;
use crate::state::AppState;
@@ -9,6 +8,7 @@ use crate::NymNodeHTTPServer;
use axum::response::Redirect;
use axum::routing::get;
use axum::Router;
use nym_node_requests::api::v1::authenticator::models::Authenticator;
use nym_node_requests::api::v1::gateway::models::{Gateway, Wireguard};
use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter;
use nym_node_requests::api::v1::mixnode::models::Mixnode;
@@ -54,6 +54,7 @@ impl Config {
mixnode: Default::default(),
network_requester: Default::default(),
ip_packet_router: Default::default(),
authenticator: Default::default(),
},
},
}
@@ -148,6 +149,12 @@ impl Config {
self.api.v1_config.ip_packet_router.details = Some(ip_packet_router);
self
}
#[must_use]
pub fn with_authenticator_details(mut self, authenticator: Authenticator) -> Self {
self.api.v1_config.authenticator.details = Some(authenticator);
self
}
}
pub struct NymNodeRouter {
@@ -156,11 +163,7 @@ pub struct NymNodeRouter {
impl NymNodeRouter {
// TODO: move the wg state to a builder
pub fn new(
config: Config,
app_state: Option<AppState>,
initial_wg_state: Option<WireguardAppState>,
) -> NymNodeRouter {
pub fn new(config: Config, app_state: Option<AppState>) -> NymNodeRouter {
let state = app_state.unwrap_or(AppState::new());
NymNodeRouter {
@@ -189,10 +192,7 @@ impl NymNodeRouter {
}),
)
.nest(routes::LANDING_PAGE, landing_page::routes(config.landing))
.nest(
routes::API,
api::routes(config.api, initial_wg_state.unwrap_or_default()),
)
.nest(routes::API, api::routes(config.api))
.layer(axum::middleware::from_fn(logging::logger))
.with_state(state),
}
@@ -24,19 +24,6 @@ impl RequestError {
status,
}
}
pub(crate) fn new_status(status: StatusCode) -> Self {
RequestError {
inner: ErrorResponse {
message: String::new(),
},
status,
}
}
pub(crate) fn from_err<E: std::error::Error>(err: E, status: StatusCode) -> Self {
Self::new(err.to_string(), status)
}
}
impl IntoResponse for RequestError {
+4 -10
View File
@@ -8,8 +8,8 @@ use crate::routes;
use async_trait::async_trait;
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_http_api_client::{ApiClient, HttpClientError};
use nym_wireguard_types::{ClientMessage, ClientRegistrationResponse};
use crate::api::v1::authenticator::models::Authenticator;
use crate::api::v1::health::models::NodeHealth;
use crate::api::v1::ip_packet_router::models::IpPacketRouter;
use crate::api::v1::network_requester::exit_policy::models::UsedExitPolicy;
@@ -65,15 +65,9 @@ pub trait NymNodeApiClientExt: ApiClient {
.await
}
async fn post_gateway_register_client(
&self,
client_message: &ClientMessage,
) -> Result<ClientRegistrationResponse, NymNodeApiClientError> {
self.post_json_data_to(
routes::api::v1::gateway::client_interfaces::wireguard::client_absolute(),
client_message,
)
.await
async fn get_authenticator(&self) -> Result<Authenticator, NymNodeApiClientError> {
self.get_json_from(routes::api::v1::authenticator_absolute())
.await
}
}
@@ -0,0 +1,4 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod models;
@@ -0,0 +1,18 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Authenticator {
/// Base58 encoded ed25519 EdDSA public key of the authenticator.
pub encoded_identity_key: String,
/// Base58-encoded x25519 public key used for performing key exchange with remote clients.
pub encoded_x25519_key: String,
/// Nym address of this ip packet router.
pub address: String,
}
@@ -2,6 +2,5 @@
// SPDX-License-Identifier: Apache-2.0
pub use nym_wireguard_types::{
ClientMac, ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, Nonce,
PeerPublicKey,
ClientMac, ClientMessage, GatewayClient, InitMessage, Nonce, PeerPublicKey,
};
@@ -1,6 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod authenticator;
pub mod gateway;
pub mod health;
pub mod ip_packet_router;
+2
View File
@@ -42,6 +42,7 @@ pub mod routes {
pub const METRICS: &str = "/metrics";
pub const NETWORK_REQUESTER: &str = "/network-requester";
pub const IP_PACKET_ROUTER: &str = "/ip-packet-router";
pub const AUTHENTICATOR: &str = "/authenticator";
// define helper functions to get absolute routes
absolute_route!(health_absolute, v1_absolute(), HEALTH);
@@ -57,6 +58,7 @@ pub mod routes {
absolute_route!(metrics_absolute, v1_absolute(), METRICS);
absolute_route!(network_requester_absolute, v1_absolute(), NETWORK_REQUESTER);
absolute_route!(ip_packet_router_absolute, v1_absolute(), IP_PACKET_ROUTER);
absolute_route!(authenticator_absolute, v1_absolute(), AUTHENTICATOR);
absolute_route!(swagger_absolute, v1_absolute(), SWAGGER);
pub mod metrics {
+6
View File
@@ -409,6 +409,9 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> {
bind_address: SocketAddr::new(ip, cfg.gateway.clients_port),
announce_ws_port: None,
announce_wss_port: cfg.gateway.clients_wss_port,
authenticator: config::authenticator::Authenticator {
debug: Default::default(),
},
debug: config::entry_gateway::Debug {
message_retrieval_limit: cfg.debug.message_retrieval_limit,
},
@@ -454,6 +457,9 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> {
.unwrap_or_default(),
},
},
authenticator: config::authenticator::Authenticator {
debug: Default::default(),
},
}),
)
.build();
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_client_core_config_types::DebugConfig as ClientDebugConfig;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
pub struct Authenticator {
#[serde(default)]
pub debug: AuthenticatorDebug,
}
#[allow(clippy::derivable_impls)]
impl Default for Authenticator {
fn default() -> Self {
Authenticator {
debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
#[serde(default)]
pub struct AuthenticatorDebug {
/// Specifies whether authenticator service is enabled in this process.
/// This is only here for debugging purposes as exit gateway should always run
/// the authenticator.
pub enabled: bool,
/// Disable Poisson sending rate.
/// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true
/// (or is it (?))
pub disable_poisson_rate: bool,
/// Shared detailed client configuration options
#[serde(flatten)]
pub client_debug: ClientDebugConfig,
}
impl Default for AuthenticatorDebug {
fn default() -> Self {
AuthenticatorDebug {
enabled: true,
disable_poisson_rate: true,
client_debug: Default::default(),
}
}
}
+5
View File
@@ -12,6 +12,8 @@ use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::path::Path;
use super::authenticator::Authenticator;
pub const DEFAULT_WS_PORT: u16 = DEFAULT_CLIENT_LISTENING_PORT;
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -38,6 +40,8 @@ pub struct EntryGatewayConfig {
#[serde(deserialize_with = "de_maybe_port")]
pub announce_wss_port: Option<u16>,
pub authenticator: Authenticator,
#[serde(default)]
pub debug: Debug,
}
@@ -69,6 +73,7 @@ impl EntryGatewayConfig {
bind_address: SocketAddr::new(inaddr_any(), DEFAULT_WS_PORT),
announce_ws_port: None,
announce_wss_port: None,
authenticator: Default::default(),
debug: Default::default(),
}
}
+29 -2
View File
@@ -8,12 +8,14 @@ use crate::error::ExitGatewayError;
use clap::crate_version;
use nym_client_core_config_types::DebugConfig as ClientDebugConfig;
use nym_config::defaults::mainnet;
use nym_gateway::node::{LocalIpPacketRouterOpts, LocalNetworkRequesterOpts};
use nym_gateway::node::{
LocalAuthenticatorOpts, LocalIpPacketRouterOpts, LocalNetworkRequesterOpts,
};
use serde::{Deserialize, Serialize};
use std::path::Path;
use url::Url;
use super::LocalWireguardOpts;
use super::{authenticator::Authenticator, LocalWireguardOpts};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
@@ -30,6 +32,8 @@ pub struct ExitGatewayConfig {
pub network_requester: NetworkRequester,
pub ip_packet_router: IpPacketRouter,
pub authenticator: Authenticator,
}
impl ExitGatewayConfig {
@@ -45,6 +49,7 @@ impl ExitGatewayConfig {
.expect("invalid default exit policy URL"),
network_requester: Default::default(),
ip_packet_router: Default::default(),
authenticator: Default::default(),
}
}
}
@@ -138,6 +143,7 @@ pub struct EphemeralConfig {
pub gateway: nym_gateway::config::Config,
pub nr_opts: LocalNetworkRequesterOpts,
pub ipr_opts: LocalIpPacketRouterOpts,
pub auth_opts: LocalAuthenticatorOpts,
pub wg_opts: LocalWireguardOpts,
}
@@ -234,6 +240,26 @@ pub fn ephemeral_exit_gateway_config(
ipr_opts.config.base.set_no_poisson_process()
}
let auth_opts = LocalAuthenticatorOpts {
config: nym_authenticator::Config {
base: nym_client_core_config_types::Config {
client: base_client_config(&config),
debug: config.exit_gateway.authenticator.debug.client_debug,
},
authenticator: config.wireguard.clone().into(),
storage_paths: nym_authenticator::config::AuthenticatorPaths {
common_paths: config
.exit_gateway
.storage_paths
.authenticator
.to_common_client_paths(),
authenticator_description: Default::default(),
},
logging: config.logging,
},
custom_mixnet_path: None,
};
let pub_id_path = config
.storage_paths
.keys
@@ -266,6 +292,7 @@ pub fn ephemeral_exit_gateway_config(
Ok(EphemeralConfig {
nr_opts,
ipr_opts,
auth_opts,
wg_opts,
gateway,
})
+13
View File
@@ -25,10 +25,12 @@ use std::time::Duration;
use tracing::{debug, error};
use url::Url;
pub mod authenticator;
pub mod entry_gateway;
pub mod exit_gateway;
pub mod helpers;
pub mod mixnode;
mod old_configs;
pub mod persistence;
mod template;
pub mod upgrade_helpers;
@@ -543,6 +545,17 @@ impl From<Wireguard> for nym_wireguard_types::Config {
}
}
impl From<Wireguard> for nym_authenticator::config::Authenticator {
fn from(value: Wireguard) -> Self {
nym_authenticator::config::Authenticator {
bind_address: value.bind_address,
private_ip: value.private_ip,
announced_port: value.announced_port,
private_network_prefix: value.private_network_prefix,
}
}
}
#[derive(Debug, Clone)]
pub struct LocalWireguardOpts {
pub config: Wireguard,
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
mod old_config_1_1_2;
mod old_config_1_1_3;
pub use old_config_1_1_2::try_upgrade_config_1_1_2;
pub use old_config_1_1_3::try_upgrade_config_1_1_3;
@@ -0,0 +1,978 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
#![allow(dead_code)]
use crate::config::*;
use crate::error::KeyIOFailure;
use nym_client_core_config_types::DebugConfig as ClientDebugConfig;
use nym_config::serde_helpers::de_maybe_port;
use nym_crypto::asymmetric::encryption::KeyPair;
use nym_pemstore::store_keypair;
use old_configs::old_config_1_1_3::*;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WireguardPaths1_1_2 {
pub private_diffie_hellman_key_file: PathBuf,
pub public_diffie_hellman_key_file: PathBuf,
}
impl WireguardPaths1_1_2 {
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
let data_dir = data_dir.as_ref();
WireguardPaths1_1_2 {
private_diffie_hellman_key_file: data_dir
.join(persistence::DEFAULT_X25519_WG_DH_KEY_FILENAME),
public_diffie_hellman_key_file: data_dir
.join(persistence::DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME),
}
}
pub fn x25519_wireguard_storage_paths(&self) -> nym_pemstore::KeyPairPath {
nym_pemstore::KeyPairPath::new(
&self.private_diffie_hellman_key_file,
&self.public_diffie_hellman_key_file,
)
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Wireguard1_1_2 {
/// Specifies whether the wireguard service is enabled on this node.
pub enabled: bool,
/// Socket address this node will use for binding its wireguard interface.
/// default: `0.0.0.0:51822`
pub bind_address: SocketAddr,
/// Ip address of the private wireguard network.
/// default: `10.1.0.0`
pub private_network_ip: IpAddr,
/// Port announced to external clients wishing to connect to the wireguard interface.
/// Useful in the instances where the node is behind a proxy.
pub announced_port: u16,
/// 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
pub private_network_prefix: u8,
/// Paths for wireguard keys, client registries, etc.
pub storage_paths: WireguardPaths1_1_2,
}
// a temporary solution until all "types" are run at the same time
#[derive(Debug, Default, Serialize, Deserialize, ValueEnum, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum NodeMode1_1_2 {
#[default]
#[clap(alias = "mix")]
Mixnode,
#[clap(alias = "entry", alias = "gateway")]
EntryGateway,
#[clap(alias = "exit")]
ExitGateway,
}
impl From<NodeMode1_1_2> for NodeMode1_1_3 {
fn from(config: NodeMode1_1_2) -> Self {
match config {
NodeMode1_1_2::Mixnode => NodeMode1_1_3::Mixnode,
NodeMode1_1_2::EntryGateway => NodeMode1_1_3::EntryGateway,
NodeMode1_1_2::ExitGateway => NodeMode1_1_3::ExitGateway,
}
}
}
// TODO: this is very much a WIP. we need proper ssl certificate support here
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct Host1_1_2 {
/// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections.
/// If no values are provided, when this node gets included in the network,
/// its ip addresses will be populated by whatever value is resolved by associated nym-api.
pub public_ips: Vec<IpAddr>,
/// Optional hostname of this node, for example nymtech.net.
// TODO: this is temporary. to be replaced by pulling the data directly from the certs.
#[serde(deserialize_with = "de_maybe_stringified")]
pub hostname: Option<String>,
/// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location
#[serde(deserialize_with = "de_maybe_stringified")]
pub location: Option<Country>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct MixnetDebug1_1_2 {
/// Initial value of an exponential backoff to reconnect to dropped TCP connection when
/// forwarding sphinx packets.
#[serde(with = "humantime_serde")]
pub packet_forwarding_initial_backoff: Duration,
/// Maximum value of an exponential backoff to reconnect to dropped TCP connection when
/// forwarding sphinx packets.
#[serde(with = "humantime_serde")]
pub packet_forwarding_maximum_backoff: Duration,
/// Timeout for establishing initial connection when trying to forward a sphinx packet.
#[serde(with = "humantime_serde")]
pub initial_connection_timeout: Duration,
/// Maximum number of packets that can be stored waiting to get sent to a particular connection.
pub maximum_connection_buffer_size: usize,
/// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented)
pub unsafe_disable_noise: bool,
}
impl MixnetDebug1_1_2 {
const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000);
const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000);
const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500);
const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000;
}
impl Default for MixnetDebug1_1_2 {
fn default() -> Self {
MixnetDebug1_1_2 {
packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF,
packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF,
initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT,
maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE,
// to be changed by @SW once the implementation is there
unsafe_disable_noise: true,
}
}
}
impl Default for Mixnet1_1_2 {
fn default() -> Self {
// SAFETY:
// our hardcoded values should always be valid
#[allow(clippy::expect_used)]
// is if there's anything set in the environment, otherwise fallback to mainnet
let nym_api_urls = if let Ok(env_value) = env::var(var_names::NYM_API) {
parse_urls(&env_value)
} else {
vec![mainnet::NYM_API.parse().expect("Invalid default API URL")]
};
#[allow(clippy::expect_used)]
let nyxd_urls = if let Ok(env_value) = env::var(var_names::NYXD) {
parse_urls(&env_value)
} else {
vec![mainnet::NYXD_URL.parse().expect("Invalid default nyxd URL")]
};
Mixnet1_1_2 {
bind_address: SocketAddr::new(inaddr_any(), DEFAULT_MIXNET_PORT),
nym_api_urls,
nyxd_urls,
debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct Mixnet1_1_2 {
/// Address this node will bind to for listening for mixnet packets
/// default: `0.0.0.0:1789`
pub bind_address: SocketAddr,
/// Addresses to nym APIs from which the node gets the view of the network.
pub nym_api_urls: Vec<Url>,
/// Addresses to nyxd which the node uses to interact with the nyx chain.
pub nyxd_urls: Vec<Url>,
#[serde(default)]
pub debug: MixnetDebug1_1_2,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct KeysPaths1_1_2 {
/// Path to file containing ed25519 identity private key.
pub private_ed25519_identity_key_file: PathBuf,
/// Path to file containing ed25519 identity public key.
pub public_ed25519_identity_key_file: PathBuf,
/// Path to file containing x25519 sphinx private key.
pub private_x25519_sphinx_key_file: PathBuf,
/// Path to file containing x25519 sphinx public key.
pub public_x25519_sphinx_key_file: PathBuf,
/// Path to file containing x25519 noise private key.
pub private_x25519_noise_key_file: PathBuf,
/// Path to file containing x25519 noise public key.
pub public_x25519_noise_key_file: PathBuf,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NymNodePaths1_1_2 {
pub keys: KeysPaths1_1_2,
/// Path to a file containing basic node description: human-readable name, website, details, etc.
pub description: PathBuf,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct Http1_1_2 {
/// Socket address this node will use for binding its http API.
/// default: `0.0.0.0:8080`
pub bind_address: SocketAddr,
/// Path to assets directory of custom landing page of this node.
#[serde(deserialize_with = "de_maybe_stringified")]
pub landing_page_assets_path: Option<PathBuf>,
/// An optional bearer token for accessing certain http endpoints.
/// Currently only used for obtaining mixnode's stats.
#[serde(default)]
pub access_token: Option<String>,
/// Specify whether basic system information should be exposed.
/// default: true
pub expose_system_info: bool,
/// Specify whether basic system hardware information should be exposed.
/// This option is superseded by `expose_system_info`
/// default: true
pub expose_system_hardware: bool,
/// Specify whether detailed system crypto hardware information should be exposed.
/// This option is superseded by `expose_system_hardware`
/// default: true
pub expose_crypto_hardware: bool,
}
impl Default for Http1_1_2 {
fn default() -> Self {
Http1_1_2 {
bind_address: SocketAddr::new(inaddr_any(), DEFAULT_HTTP_PORT),
landing_page_assets_path: None,
access_token: None,
expose_system_info: true,
expose_system_hardware: true,
expose_crypto_hardware: true,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MixnodePaths1_1_2 {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Debug1_1_2 {
/// Delay between each subsequent node statistics being logged to the console
#[serde(with = "humantime_serde")]
pub node_stats_logging_delay: Duration,
/// Delay between each subsequent node statistics being updated
#[serde(with = "humantime_serde")]
pub node_stats_updating_delay: Duration,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct VerlocDebug1_1_2 {
/// Specifies number of echo packets sent to each node during a measurement run.
pub packets_per_node: usize,
/// Specifies maximum amount of time to wait for the connection to get established.
#[serde(with = "humantime_serde")]
pub connection_timeout: Duration,
/// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test.
#[serde(with = "humantime_serde")]
pub packet_timeout: Duration,
/// Specifies delay between subsequent test packets being sent (after receiving a reply).
#[serde(with = "humantime_serde")]
pub delay_between_packets: Duration,
/// Specifies number of nodes being tested at once.
pub tested_nodes_batch_size: usize,
/// Specifies delay between subsequent test runs.
#[serde(with = "humantime_serde")]
pub testing_interval: Duration,
/// Specifies delay between attempting to run the measurement again if the previous run failed
/// due to being unable to get the list of nodes.
#[serde(with = "humantime_serde")]
pub retry_timeout: Duration,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Verloc1_1_2 {
/// Socket address this node will use for binding its verloc API.
/// default: `0.0.0.0:1790`
pub bind_address: SocketAddr,
#[serde(default)]
pub debug: VerlocDebug1_1_2,
}
impl VerlocDebug1_1_2 {
const DEFAULT_PACKETS_PER_NODE: usize = 100;
const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000);
const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500);
const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50);
const DEFAULT_BATCH_SIZE: usize = 50;
const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12);
const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30);
}
impl Default for VerlocDebug1_1_2 {
fn default() -> Self {
VerlocDebug1_1_2 {
packets_per_node: Self::DEFAULT_PACKETS_PER_NODE,
connection_timeout: Self::DEFAULT_CONNECTION_TIMEOUT,
packet_timeout: Self::DEFAULT_PACKET_TIMEOUT,
delay_between_packets: Self::DEFAULT_DELAY_BETWEEN_PACKETS,
tested_nodes_batch_size: Self::DEFAULT_BATCH_SIZE,
testing_interval: Self::DEFAULT_TESTING_INTERVAL,
retry_timeout: Self::DEFAULT_RETRY_TIMEOUT,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MixnodeConfig1_1_2 {
pub storage_paths: MixnodePaths1_1_2,
pub verloc: Verloc1_1_2,
#[serde(default)]
pub debug: Debug1_1_2,
}
impl Debug1_1_2 {
const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000);
const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000);
}
impl Default for Debug1_1_2 {
fn default() -> Self {
Debug1_1_2 {
node_stats_logging_delay: Self::DEFAULT_NODE_STATS_LOGGING_DELAY,
node_stats_updating_delay: Self::DEFAULT_NODE_STATS_UPDATING_DELAY,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EntryGatewayPaths1_1_2 {
/// Path to sqlite database containing all persistent data: messages for offline clients,
/// derived shared keys and available client bandwidths.
pub clients_storage: PathBuf,
/// Path to file containing cosmos account mnemonic used for zk-nym redemption.
pub cosmos_mnemonic: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EntryGatewayConfigDebug1_1_2 {
/// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage.
pub message_retrieval_limit: i64,
}
impl EntryGatewayConfigDebug1_1_2 {
const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100;
}
impl Default for EntryGatewayConfigDebug1_1_2 {
fn default() -> Self {
EntryGatewayConfigDebug1_1_2 {
message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EntryGatewayConfig1_1_2 {
pub storage_paths: EntryGatewayPaths1_1_2,
/// Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet
/// or if it also accepts non-paying clients
pub enforce_zk_nyms: bool,
/// Socket address this node will use for binding its client websocket API.
/// default: `0.0.0.0:9000`
pub bind_address: SocketAddr,
/// Custom announced port for listening for websocket client traffic.
/// If unspecified, the value from the `bind_address` will be used instead
/// default: None
#[serde(deserialize_with = "de_maybe_port")]
pub announce_ws_port: Option<u16>,
/// If applicable, announced port for listening for secure websocket client traffic.
/// (default: None)
#[serde(deserialize_with = "de_maybe_port")]
pub announce_wss_port: Option<u16>,
#[serde(default)]
pub debug: EntryGatewayConfigDebug1_1_2,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkRequesterPaths1_1_2 {
/// Path to file containing network requester ed25519 identity private key.
pub private_ed25519_identity_key_file: PathBuf,
/// Path to file containing network requester ed25519 identity public key.
pub public_ed25519_identity_key_file: PathBuf,
/// Path to file containing network requester x25519 diffie hellman private key.
pub private_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing network requester x25519 diffie hellman public key.
pub public_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing key used for encrypting and decrypting the content of an
/// acknowledgement so that nobody besides the client knows which packet it refers to.
pub ack_key_file: PathBuf,
/// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
pub reply_surb_database: PathBuf,
/// Normally this is a path to the file containing information about gateways used by this client,
/// i.e. details such as their public keys, owner addresses or the network information.
/// but in this case it just has the basic information of "we're using custom gateway".
/// Due to how clients are started up, this file has to exist.
pub gateway_registrations: PathBuf,
// it's possible we might have to add credential storage here for return tickets
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct IpPacketRouterPaths1_1_2 {
/// Path to file containing ip packet router ed25519 identity private key.
pub private_ed25519_identity_key_file: PathBuf,
/// Path to file containing ip packet router ed25519 identity public key.
pub public_ed25519_identity_key_file: PathBuf,
/// Path to file containing ip packet router x25519 diffie hellman private key.
pub private_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing ip packet router x25519 diffie hellman public key.
pub public_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing key used for encrypting and decrypting the content of an
/// acknowledgement so that nobody besides the client knows which packet it refers to.
pub ack_key_file: PathBuf,
/// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
pub reply_surb_database: PathBuf,
/// Normally this is a path to the file containing information about gateways used by this client,
/// i.e. details such as their public keys, owner addresses or the network information.
/// but in this case it just has the basic information of "we're using custom gateway".
/// Due to how clients are started up, this file has to exist.
pub gateway_registrations: PathBuf,
// it's possible we might have to add credential storage here for return tickets
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ExitGatewayPaths1_1_2 {
pub network_requester: NetworkRequesterPaths1_1_2,
pub ip_packet_router: IpPacketRouterPaths1_1_2,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
#[serde(default)]
pub struct IpPacketRouterDebug1_1_2 {
/// Specifies whether ip packet routing service is enabled in this process.
/// This is only here for debugging purposes as exit gateway should always run **both**
/// network requester and an ip packet router.
pub enabled: bool,
/// Disable Poisson sending rate.
/// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true
/// (or is it (?))
pub disable_poisson_rate: bool,
/// Shared detailed client configuration options
#[serde(flatten)]
pub client_debug: ClientDebugConfig,
}
impl Default for IpPacketRouterDebug1_1_2 {
fn default() -> Self {
IpPacketRouterDebug1_1_2 {
enabled: true,
disable_poisson_rate: true,
client_debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
pub struct IpPacketRouter1_1_2 {
#[serde(default)]
pub debug: IpPacketRouterDebug1_1_2,
}
#[allow(clippy::derivable_impls)]
impl Default for IpPacketRouter1_1_2 {
fn default() -> Self {
IpPacketRouter1_1_2 {
debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
pub struct NetworkRequesterDebug1_1_2 {
/// Specifies whether network requester service is enabled in this process.
/// This is only here for debugging purposes as exit gateway should always run **both**
/// network requester and an ip packet router.
pub enabled: bool,
/// Disable Poisson sending rate.
/// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true
/// (or is it (?))
pub disable_poisson_rate: bool,
/// Shared detailed client configuration options
#[serde(flatten)]
pub client_debug: ClientDebugConfig,
}
impl Default for NetworkRequesterDebug1_1_2 {
fn default() -> Self {
NetworkRequesterDebug1_1_2 {
enabled: true,
disable_poisson_rate: true,
client_debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
pub struct NetworkRequester1_1_2 {
#[serde(default)]
pub debug: NetworkRequesterDebug1_1_2,
}
#[allow(clippy::derivable_impls)]
impl Default for NetworkRequester1_1_2 {
fn default() -> Self {
NetworkRequester1_1_2 {
debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExitGatewayConfig1_1_2 {
pub storage_paths: ExitGatewayPaths1_1_2,
/// specifies whether this exit node should run in 'open-proxy' mode
/// and thus would attempt to resolve **ANY** request it receives.
pub open_proxy: bool,
/// Specifies the url for an upstream source of the exit policy used by this node.
pub upstream_exit_policy_url: Url,
pub network_requester: NetworkRequester1_1_2,
pub ip_packet_router: IpPacketRouter1_1_2,
}
#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LoggingSettings1_1_2 {
// well, we need to implement something here at some point...
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config1_1_2 {
// additional metadata holding on-disk location of this config file
#[serde(skip)]
pub(crate) save_path: Option<PathBuf>,
/// Human-readable ID of this particular node.
pub id: String,
/// Current mode of this nym-node.
/// Expect this field to be changed in the future to allow running the node in multiple modes (i.e. mixnode + gateway)
pub mode: NodeMode1_1_2,
pub host: Host1_1_2,
pub mixnet: Mixnet1_1_2,
/// Storage paths to persistent nym-node data, such as its long term keys.
pub storage_paths: NymNodePaths1_1_2,
#[serde(default)]
pub http: Http1_1_2,
pub wireguard: Wireguard1_1_2,
pub mixnode: MixnodeConfig1_1_2,
pub entry_gateway: EntryGatewayConfig1_1_2,
pub exit_gateway: ExitGatewayConfig1_1_2,
#[serde(default)]
pub logging: LoggingSettings1_1_2,
}
impl NymConfigTemplate for Config1_1_2 {
fn template(&self) -> &'static str {
CONFIG_TEMPLATE
}
}
impl Config1_1_2 {
pub fn save(&self) -> Result<(), NymNodeError> {
let save_location = self.save_location();
debug!(
"attempting to save config file to '{}'",
save_location.display()
);
save_formatted_config_to_file(self, &save_location).map_err(|source| {
NymNodeError::ConfigSaveFailure {
id: self.id.clone(),
path: save_location,
source,
}
})
}
pub fn save_location(&self) -> PathBuf {
self.save_path
.clone()
.unwrap_or(self.default_save_location())
}
pub fn default_save_location(&self) -> PathBuf {
default_config_filepath(&self.id)
}
pub fn default_data_directory<P: AsRef<Path>>(config_path: P) -> Result<PathBuf, NymNodeError> {
let config_path = config_path.as_ref();
// we got a proper path to the .toml file
let Some(config_dir) = config_path.parent() else {
error!(
"'{}' does not have a parent directory. Have you pointed to the fs root?",
config_path.display()
);
return Err(NymNodeError::DataDirDerivationFailure);
};
let Some(config_dir_name) = config_dir.file_name() else {
error!(
"could not obtain parent directory name of '{}'. Have you used relative paths?",
config_path.display()
);
return Err(NymNodeError::DataDirDerivationFailure);
};
if config_dir_name != DEFAULT_CONFIG_DIR {
error!(
"the parent directory of '{}' ({}) is not {DEFAULT_CONFIG_DIR}. currently this is not supported",
config_path.display(), config_dir_name.to_str().unwrap_or("UNKNOWN")
);
return Err(NymNodeError::DataDirDerivationFailure);
}
let Some(node_dir) = config_dir.parent() else {
error!(
"'{}' does not have a parent directory. Have you pointed to the fs root?",
config_dir.display()
);
return Err(NymNodeError::DataDirDerivationFailure);
};
Ok(node_dir.join(DEFAULT_DATA_DIR))
}
// simple wrapper that reads config file and assigns path location
fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> {
let path = path.as_ref();
let mut loaded: Config1_1_2 =
read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure {
path: path.to_path_buf(),
source,
})?;
loaded.save_path = Some(path.to_path_buf());
debug!("loaded config file from {}", path.display());
Ok(loaded)
}
pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> {
Self::read_from_path(path)
}
}
fn initialise(config: &Wireguard1_1_3) -> std::io::Result<()> {
let mut rng = OsRng;
let x25519_keys = KeyPair::new(&mut rng);
store_keypair(
&x25519_keys,
&config.storage_paths.x25519_wireguard_storage_paths(),
)?;
Ok(())
}
// currently there are no upgrades
pub async fn try_upgrade_config_1_1_2<P: AsRef<Path>>(path: P) -> Result<(), NymNodeError> {
let old_cfg = Config1_1_2::read_from_path(&path)?;
let wireguard = Wireguard1_1_3 {
enabled: old_cfg.wireguard.enabled,
bind_address: old_cfg.wireguard.bind_address,
private_ip: old_cfg.wireguard.private_network_ip,
announced_port: old_cfg.wireguard.announced_port,
private_network_prefix: old_cfg.wireguard.private_network_prefix,
storage_paths: WireguardPaths1_1_3::new(Config1_1_3::default_data_directory(path)?),
};
initialise(&wireguard).map_err(|err| KeyIOFailure::KeyPairStoreFailure {
keys: "wg-x25519-dh".to_string(),
paths: wireguard.storage_paths.x25519_wireguard_storage_paths(),
err,
})?;
let cfg = Config1_1_3 {
save_path: old_cfg.save_path,
id: old_cfg.id,
mode: old_cfg.mode.into(),
host: Host1_1_3 {
public_ips: old_cfg.host.public_ips,
hostname: old_cfg.host.hostname,
location: old_cfg.host.location,
},
mixnet: Mixnet1_1_3 {
bind_address: old_cfg.mixnet.bind_address,
nym_api_urls: old_cfg.mixnet.nym_api_urls,
nyxd_urls: old_cfg.mixnet.nyxd_urls,
debug: MixnetDebug1_1_3 {
packet_forwarding_initial_backoff: old_cfg
.mixnet
.debug
.packet_forwarding_initial_backoff,
packet_forwarding_maximum_backoff: old_cfg
.mixnet
.debug
.packet_forwarding_maximum_backoff,
initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout,
maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size,
unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise,
},
},
storage_paths: NymNodePaths1_1_3 {
keys: KeysPaths1_1_3 {
private_ed25519_identity_key_file: old_cfg
.storage_paths
.keys
.private_ed25519_identity_key_file,
public_ed25519_identity_key_file: old_cfg
.storage_paths
.keys
.public_ed25519_identity_key_file,
private_x25519_sphinx_key_file: old_cfg
.storage_paths
.keys
.private_x25519_sphinx_key_file,
public_x25519_sphinx_key_file: old_cfg
.storage_paths
.keys
.public_x25519_sphinx_key_file,
private_x25519_noise_key_file: old_cfg
.storage_paths
.keys
.private_x25519_noise_key_file,
public_x25519_noise_key_file: old_cfg
.storage_paths
.keys
.public_x25519_noise_key_file,
},
description: old_cfg.storage_paths.description,
},
http: Http1_1_3 {
bind_address: old_cfg.http.bind_address,
landing_page_assets_path: old_cfg.http.landing_page_assets_path,
access_token: old_cfg.http.access_token,
expose_system_info: old_cfg.http.expose_system_info,
expose_system_hardware: old_cfg.http.expose_system_hardware,
expose_crypto_hardware: old_cfg.http.expose_crypto_hardware,
},
wireguard,
mixnode: MixnodeConfig1_1_3 {
storage_paths: MixnodePaths1_1_3 {},
verloc: Verloc1_1_3 {
bind_address: old_cfg.mixnode.verloc.bind_address,
debug: VerlocDebug1_1_3 {
packets_per_node: old_cfg.mixnode.verloc.debug.packets_per_node,
connection_timeout: old_cfg.mixnode.verloc.debug.connection_timeout,
packet_timeout: old_cfg.mixnode.verloc.debug.packet_timeout,
delay_between_packets: old_cfg.mixnode.verloc.debug.delay_between_packets,
tested_nodes_batch_size: old_cfg.mixnode.verloc.debug.tested_nodes_batch_size,
testing_interval: old_cfg.mixnode.verloc.debug.testing_interval,
retry_timeout: old_cfg.mixnode.verloc.debug.retry_timeout,
},
},
debug: Debug1_1_3 {
node_stats_logging_delay: old_cfg.mixnode.debug.node_stats_logging_delay,
node_stats_updating_delay: old_cfg.mixnode.debug.node_stats_updating_delay,
},
},
entry_gateway: EntryGatewayConfig1_1_3 {
storage_paths: EntryGatewayPaths1_1_3 {
clients_storage: old_cfg.entry_gateway.storage_paths.clients_storage,
cosmos_mnemonic: old_cfg.entry_gateway.storage_paths.cosmos_mnemonic,
},
enforce_zk_nyms: old_cfg.entry_gateway.enforce_zk_nyms,
bind_address: old_cfg.entry_gateway.bind_address,
announce_ws_port: old_cfg.entry_gateway.announce_ws_port,
announce_wss_port: old_cfg.entry_gateway.announce_wss_port,
debug: EntryGatewayConfigDebug1_1_3 {
message_retrieval_limit: old_cfg.entry_gateway.debug.message_retrieval_limit,
},
},
exit_gateway: ExitGatewayConfig1_1_3 {
storage_paths: ExitGatewayPaths1_1_3 {
network_requester: NetworkRequesterPaths1_1_3 {
private_ed25519_identity_key_file: old_cfg
.exit_gateway
.storage_paths
.network_requester
.private_ed25519_identity_key_file,
public_ed25519_identity_key_file: old_cfg
.exit_gateway
.storage_paths
.network_requester
.public_ed25519_identity_key_file,
private_x25519_diffie_hellman_key_file: old_cfg
.exit_gateway
.storage_paths
.network_requester
.private_x25519_diffie_hellman_key_file,
public_x25519_diffie_hellman_key_file: old_cfg
.exit_gateway
.storage_paths
.network_requester
.public_x25519_diffie_hellman_key_file,
ack_key_file: old_cfg
.exit_gateway
.storage_paths
.network_requester
.ack_key_file,
reply_surb_database: old_cfg
.exit_gateway
.storage_paths
.network_requester
.reply_surb_database,
gateway_registrations: old_cfg
.exit_gateway
.storage_paths
.network_requester
.gateway_registrations,
},
ip_packet_router: IpPacketRouterPaths1_1_3 {
private_ed25519_identity_key_file: old_cfg
.exit_gateway
.storage_paths
.ip_packet_router
.private_ed25519_identity_key_file,
public_ed25519_identity_key_file: old_cfg
.exit_gateway
.storage_paths
.ip_packet_router
.public_ed25519_identity_key_file,
private_x25519_diffie_hellman_key_file: old_cfg
.exit_gateway
.storage_paths
.ip_packet_router
.private_x25519_diffie_hellman_key_file,
public_x25519_diffie_hellman_key_file: old_cfg
.exit_gateway
.storage_paths
.ip_packet_router
.public_x25519_diffie_hellman_key_file,
ack_key_file: old_cfg
.exit_gateway
.storage_paths
.ip_packet_router
.ack_key_file,
reply_surb_database: old_cfg
.exit_gateway
.storage_paths
.ip_packet_router
.reply_surb_database,
gateway_registrations: old_cfg
.exit_gateway
.storage_paths
.ip_packet_router
.gateway_registrations,
},
},
open_proxy: old_cfg.exit_gateway.open_proxy,
upstream_exit_policy_url: old_cfg.exit_gateway.upstream_exit_policy_url,
network_requester: NetworkRequester1_1_3 {
debug: NetworkRequesterDebug1_1_3 {
enabled: old_cfg.exit_gateway.network_requester.debug.enabled,
disable_poisson_rate: old_cfg
.exit_gateway
.network_requester
.debug
.disable_poisson_rate,
client_debug: old_cfg.exit_gateway.network_requester.debug.client_debug,
},
},
ip_packet_router: IpPacketRouter1_1_3 {
debug: IpPacketRouterDebug1_1_3 {
enabled: old_cfg.exit_gateway.ip_packet_router.debug.enabled,
disable_poisson_rate: old_cfg
.exit_gateway
.ip_packet_router
.debug
.disable_poisson_rate,
client_debug: old_cfg.exit_gateway.ip_packet_router.debug.client_debug,
},
},
},
logging: LoggingSettings1_1_3 {},
};
cfg.save()?;
Ok(())
}
@@ -0,0 +1,766 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
#![allow(dead_code)]
use crate::config::*;
use nym_client_core_config_types::DebugConfig as ClientDebugConfig;
use nym_config::serde_helpers::de_maybe_port;
use nym_crypto::asymmetric::encryption::KeyPair;
use nym_pemstore::store_keypair;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WireguardPaths1_1_3 {
pub private_diffie_hellman_key_file: PathBuf,
pub public_diffie_hellman_key_file: PathBuf,
}
impl WireguardPaths1_1_3 {
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
let data_dir = data_dir.as_ref();
WireguardPaths1_1_3 {
private_diffie_hellman_key_file: data_dir
.join(persistence::DEFAULT_X25519_WG_DH_KEY_FILENAME),
public_diffie_hellman_key_file: data_dir
.join(persistence::DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME),
}
}
pub fn x25519_wireguard_storage_paths(&self) -> nym_pemstore::KeyPairPath {
nym_pemstore::KeyPairPath::new(
&self.private_diffie_hellman_key_file,
&self.public_diffie_hellman_key_file,
)
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Wireguard1_1_3 {
/// Specifies whether the wireguard service is enabled on this node.
pub enabled: bool,
/// Socket address this node will use for binding its wireguard interface.
/// default: `0.0.0.0:51822`
pub bind_address: SocketAddr,
/// Ip address of the private wireguard network.
/// default: `10.1.0.0`
pub private_ip: IpAddr,
/// Port announced to external clients wishing to connect to the wireguard interface.
/// Useful in the instances where the node is behind a proxy.
pub announced_port: u16,
/// 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
pub private_network_prefix: u8,
/// Paths for wireguard keys, client registries, etc.
pub storage_paths: WireguardPaths1_1_3,
}
// a temporary solution until all "types" are run at the same time
#[derive(Debug, Default, Serialize, Deserialize, ValueEnum, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum NodeMode1_1_3 {
#[default]
#[clap(alias = "mix")]
Mixnode,
#[clap(alias = "entry", alias = "gateway")]
EntryGateway,
#[clap(alias = "exit")]
ExitGateway,
}
// TODO: this is very much a WIP. we need proper ssl certificate support here
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct Host1_1_3 {
/// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections.
/// If no values are provided, when this node gets included in the network,
/// its ip addresses will be populated by whatever value is resolved by associated nym-api.
pub public_ips: Vec<IpAddr>,
/// Optional hostname of this node, for example nymtech.net.
// TODO: this is temporary. to be replaced by pulling the data directly from the certs.
#[serde(deserialize_with = "de_maybe_stringified")]
pub hostname: Option<String>,
/// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location
#[serde(deserialize_with = "de_maybe_stringified")]
pub location: Option<Country>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct MixnetDebug1_1_3 {
/// Initial value of an exponential backoff to reconnect to dropped TCP connection when
/// forwarding sphinx packets.
#[serde(with = "humantime_serde")]
pub packet_forwarding_initial_backoff: Duration,
/// Maximum value of an exponential backoff to reconnect to dropped TCP connection when
/// forwarding sphinx packets.
#[serde(with = "humantime_serde")]
pub packet_forwarding_maximum_backoff: Duration,
/// Timeout for establishing initial connection when trying to forward a sphinx packet.
#[serde(with = "humantime_serde")]
pub initial_connection_timeout: Duration,
/// Maximum number of packets that can be stored waiting to get sent to a particular connection.
pub maximum_connection_buffer_size: usize,
/// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented)
pub unsafe_disable_noise: bool,
}
impl MixnetDebug1_1_3 {
const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000);
const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000);
const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500);
const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000;
}
impl Default for MixnetDebug1_1_3 {
fn default() -> Self {
MixnetDebug1_1_3 {
packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF,
packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF,
initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT,
maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE,
// to be changed by @SW once the implementation is there
unsafe_disable_noise: true,
}
}
}
impl Default for Mixnet1_1_3 {
fn default() -> Self {
// SAFETY:
// our hardcoded values should always be valid
#[allow(clippy::expect_used)]
// is if there's anything set in the environment, otherwise fallback to mainnet
let nym_api_urls = if let Ok(env_value) = env::var(var_names::NYM_API) {
parse_urls(&env_value)
} else {
vec![mainnet::NYM_API.parse().expect("Invalid default API URL")]
};
#[allow(clippy::expect_used)]
let nyxd_urls = if let Ok(env_value) = env::var(var_names::NYXD) {
parse_urls(&env_value)
} else {
vec![mainnet::NYXD_URL.parse().expect("Invalid default nyxd URL")]
};
Mixnet1_1_3 {
bind_address: SocketAddr::new(inaddr_any(), DEFAULT_MIXNET_PORT),
nym_api_urls,
nyxd_urls,
debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct Mixnet1_1_3 {
/// Address this node will bind to for listening for mixnet packets
/// default: `0.0.0.0:1789`
pub bind_address: SocketAddr,
/// Addresses to nym APIs from which the node gets the view of the network.
pub nym_api_urls: Vec<Url>,
/// Addresses to nyxd which the node uses to interact with the nyx chain.
pub nyxd_urls: Vec<Url>,
#[serde(default)]
pub debug: MixnetDebug1_1_3,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct KeysPaths1_1_3 {
/// Path to file containing ed25519 identity private key.
pub private_ed25519_identity_key_file: PathBuf,
/// Path to file containing ed25519 identity public key.
pub public_ed25519_identity_key_file: PathBuf,
/// Path to file containing x25519 sphinx private key.
pub private_x25519_sphinx_key_file: PathBuf,
/// Path to file containing x25519 sphinx public key.
pub public_x25519_sphinx_key_file: PathBuf,
/// Path to file containing x25519 noise private key.
pub private_x25519_noise_key_file: PathBuf,
/// Path to file containing x25519 noise public key.
pub public_x25519_noise_key_file: PathBuf,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NymNodePaths1_1_3 {
pub keys: KeysPaths1_1_3,
/// Path to a file containing basic node description: human-readable name, website, details, etc.
pub description: PathBuf,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct Http1_1_3 {
/// Socket address this node will use for binding its http API.
/// default: `0.0.0.0:8080`
pub bind_address: SocketAddr,
/// Path to assets directory of custom landing page of this node.
#[serde(deserialize_with = "de_maybe_stringified")]
pub landing_page_assets_path: Option<PathBuf>,
/// An optional bearer token for accessing certain http endpoints.
/// Currently only used for obtaining mixnode's stats.
#[serde(default)]
pub access_token: Option<String>,
/// Specify whether basic system information should be exposed.
/// default: true
pub expose_system_info: bool,
/// Specify whether basic system hardware information should be exposed.
/// This option is superseded by `expose_system_info`
/// default: true
pub expose_system_hardware: bool,
/// Specify whether detailed system crypto hardware information should be exposed.
/// This option is superseded by `expose_system_hardware`
/// default: true
pub expose_crypto_hardware: bool,
}
impl Default for Http1_1_3 {
fn default() -> Self {
Http1_1_3 {
bind_address: SocketAddr::new(inaddr_any(), DEFAULT_HTTP_PORT),
landing_page_assets_path: None,
access_token: None,
expose_system_info: true,
expose_system_hardware: true,
expose_crypto_hardware: true,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MixnodePaths1_1_3 {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Debug1_1_3 {
/// Delay between each subsequent node statistics being logged to the console
#[serde(with = "humantime_serde")]
pub node_stats_logging_delay: Duration,
/// Delay between each subsequent node statistics being updated
#[serde(with = "humantime_serde")]
pub node_stats_updating_delay: Duration,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct VerlocDebug1_1_3 {
/// Specifies number of echo packets sent to each node during a measurement run.
pub packets_per_node: usize,
/// Specifies maximum amount of time to wait for the connection to get established.
#[serde(with = "humantime_serde")]
pub connection_timeout: Duration,
/// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test.
#[serde(with = "humantime_serde")]
pub packet_timeout: Duration,
/// Specifies delay between subsequent test packets being sent (after receiving a reply).
#[serde(with = "humantime_serde")]
pub delay_between_packets: Duration,
/// Specifies number of nodes being tested at once.
pub tested_nodes_batch_size: usize,
/// Specifies delay between subsequent test runs.
#[serde(with = "humantime_serde")]
pub testing_interval: Duration,
/// Specifies delay between attempting to run the measurement again if the previous run failed
/// due to being unable to get the list of nodes.
#[serde(with = "humantime_serde")]
pub retry_timeout: Duration,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Verloc1_1_3 {
/// Socket address this node will use for binding its verloc API.
/// default: `0.0.0.0:1790`
pub bind_address: SocketAddr,
#[serde(default)]
pub debug: VerlocDebug1_1_3,
}
impl VerlocDebug1_1_3 {
const DEFAULT_PACKETS_PER_NODE: usize = 100;
const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000);
const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500);
const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50);
const DEFAULT_BATCH_SIZE: usize = 50;
const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12);
const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30);
}
impl Default for VerlocDebug1_1_3 {
fn default() -> Self {
VerlocDebug1_1_3 {
packets_per_node: Self::DEFAULT_PACKETS_PER_NODE,
connection_timeout: Self::DEFAULT_CONNECTION_TIMEOUT,
packet_timeout: Self::DEFAULT_PACKET_TIMEOUT,
delay_between_packets: Self::DEFAULT_DELAY_BETWEEN_PACKETS,
tested_nodes_batch_size: Self::DEFAULT_BATCH_SIZE,
testing_interval: Self::DEFAULT_TESTING_INTERVAL,
retry_timeout: Self::DEFAULT_RETRY_TIMEOUT,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MixnodeConfig1_1_3 {
pub storage_paths: MixnodePaths1_1_3,
pub verloc: Verloc1_1_3,
#[serde(default)]
pub debug: Debug1_1_3,
}
impl Debug1_1_3 {
const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000);
const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000);
}
impl Default for Debug1_1_3 {
fn default() -> Self {
Debug1_1_3 {
node_stats_logging_delay: Self::DEFAULT_NODE_STATS_LOGGING_DELAY,
node_stats_updating_delay: Self::DEFAULT_NODE_STATS_UPDATING_DELAY,
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EntryGatewayPaths1_1_3 {
/// Path to sqlite database containing all persistent data: messages for offline clients,
/// derived shared keys and available client bandwidths.
pub clients_storage: PathBuf,
/// Path to file containing cosmos account mnemonic used for zk-nym redemption.
pub cosmos_mnemonic: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EntryGatewayConfigDebug1_1_3 {
/// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage.
pub message_retrieval_limit: i64,
}
impl EntryGatewayConfigDebug1_1_3 {
const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100;
}
impl Default for EntryGatewayConfigDebug1_1_3 {
fn default() -> Self {
EntryGatewayConfigDebug1_1_3 {
message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EntryGatewayConfig1_1_3 {
pub storage_paths: EntryGatewayPaths1_1_3,
/// Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet
/// or if it also accepts non-paying clients
pub enforce_zk_nyms: bool,
/// Socket address this node will use for binding its client websocket API.
/// default: `0.0.0.0:9000`
pub bind_address: SocketAddr,
/// Custom announced port for listening for websocket client traffic.
/// If unspecified, the value from the `bind_address` will be used instead
/// default: None
#[serde(deserialize_with = "de_maybe_port")]
pub announce_ws_port: Option<u16>,
/// If applicable, announced port for listening for secure websocket client traffic.
/// (default: None)
#[serde(deserialize_with = "de_maybe_port")]
pub announce_wss_port: Option<u16>,
#[serde(default)]
pub debug: EntryGatewayConfigDebug1_1_3,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkRequesterPaths1_1_3 {
/// Path to file containing network requester ed25519 identity private key.
pub private_ed25519_identity_key_file: PathBuf,
/// Path to file containing network requester ed25519 identity public key.
pub public_ed25519_identity_key_file: PathBuf,
/// Path to file containing network requester x25519 diffie hellman private key.
pub private_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing network requester x25519 diffie hellman public key.
pub public_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing key used for encrypting and decrypting the content of an
/// acknowledgement so that nobody besides the client knows which packet it refers to.
pub ack_key_file: PathBuf,
/// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
pub reply_surb_database: PathBuf,
/// Normally this is a path to the file containing information about gateways used by this client,
/// i.e. details such as their public keys, owner addresses or the network information.
/// but in this case it just has the basic information of "we're using custom gateway".
/// Due to how clients are started up, this file has to exist.
pub gateway_registrations: PathBuf,
// it's possible we might have to add credential storage here for return tickets
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct IpPacketRouterPaths1_1_3 {
/// Path to file containing ip packet router ed25519 identity private key.
pub private_ed25519_identity_key_file: PathBuf,
/// Path to file containing ip packet router ed25519 identity public key.
pub public_ed25519_identity_key_file: PathBuf,
/// Path to file containing ip packet router x25519 diffie hellman private key.
pub private_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing ip packet router x25519 diffie hellman public key.
pub public_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing key used for encrypting and decrypting the content of an
/// acknowledgement so that nobody besides the client knows which packet it refers to.
pub ack_key_file: PathBuf,
/// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
pub reply_surb_database: PathBuf,
/// Normally this is a path to the file containing information about gateways used by this client,
/// i.e. details such as their public keys, owner addresses or the network information.
/// but in this case it just has the basic information of "we're using custom gateway".
/// Due to how clients are started up, this file has to exist.
pub gateway_registrations: PathBuf,
// it's possible we might have to add credential storage here for return tickets
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ExitGatewayPaths1_1_3 {
pub network_requester: NetworkRequesterPaths1_1_3,
pub ip_packet_router: IpPacketRouterPaths1_1_3,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
#[serde(default)]
pub struct IpPacketRouterDebug1_1_3 {
/// Specifies whether ip packet routing service is enabled in this process.
/// This is only here for debugging purposes as exit gateway should always run **both**
/// network requester and an ip packet router.
pub enabled: bool,
/// Disable Poisson sending rate.
/// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true
/// (or is it (?))
pub disable_poisson_rate: bool,
/// Shared detailed client configuration options
#[serde(flatten)]
pub client_debug: ClientDebugConfig,
}
impl Default for IpPacketRouterDebug1_1_3 {
fn default() -> Self {
IpPacketRouterDebug1_1_3 {
enabled: true,
disable_poisson_rate: true,
client_debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
pub struct IpPacketRouter1_1_3 {
#[serde(default)]
pub debug: IpPacketRouterDebug1_1_3,
}
#[allow(clippy::derivable_impls)]
impl Default for IpPacketRouter1_1_3 {
fn default() -> Self {
IpPacketRouter1_1_3 {
debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
pub struct NetworkRequesterDebug1_1_3 {
/// Specifies whether network requester service is enabled in this process.
/// This is only here for debugging purposes as exit gateway should always run **both**
/// network requester and an ip packet router.
pub enabled: bool,
/// Disable Poisson sending rate.
/// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true
/// (or is it (?))
pub disable_poisson_rate: bool,
/// Shared detailed client configuration options
#[serde(flatten)]
pub client_debug: ClientDebugConfig,
}
impl Default for NetworkRequesterDebug1_1_3 {
fn default() -> Self {
NetworkRequesterDebug1_1_3 {
enabled: true,
disable_poisson_rate: true,
client_debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)]
pub struct NetworkRequester1_1_3 {
#[serde(default)]
pub debug: NetworkRequesterDebug1_1_3,
}
#[allow(clippy::derivable_impls)]
impl Default for NetworkRequester1_1_3 {
fn default() -> Self {
NetworkRequester1_1_3 {
debug: Default::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExitGatewayConfig1_1_3 {
pub storage_paths: ExitGatewayPaths1_1_3,
/// specifies whether this exit node should run in 'open-proxy' mode
/// and thus would attempt to resolve **ANY** request it receives.
pub open_proxy: bool,
/// Specifies the url for an upstream source of the exit policy used by this node.
pub upstream_exit_policy_url: Url,
pub network_requester: NetworkRequester1_1_3,
pub ip_packet_router: IpPacketRouter1_1_3,
}
#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LoggingSettings1_1_3 {
// well, we need to implement something here at some point...
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config1_1_3 {
// additional metadata holding on-disk location of this config file
#[serde(skip)]
pub(crate) save_path: Option<PathBuf>,
/// Human-readable ID of this particular node.
pub id: String,
/// Current mode of this nym-node.
/// Expect this field to be changed in the future to allow running the node in multiple modes (i.e. mixnode + gateway)
pub mode: NodeMode1_1_3,
pub host: Host1_1_3,
pub mixnet: Mixnet1_1_3,
/// Storage paths to persistent nym-node data, such as its long term keys.
pub storage_paths: NymNodePaths1_1_3,
#[serde(default)]
pub http: Http1_1_3,
pub wireguard: Wireguard1_1_3,
pub mixnode: MixnodeConfig1_1_3,
pub entry_gateway: EntryGatewayConfig1_1_3,
pub exit_gateway: ExitGatewayConfig1_1_3,
#[serde(default)]
pub logging: LoggingSettings1_1_3,
}
impl NymConfigTemplate for Config1_1_3 {
fn template(&self) -> &'static str {
CONFIG_TEMPLATE
}
}
impl Config1_1_3 {
pub fn save(&self) -> Result<(), NymNodeError> {
let save_location = self.save_location();
debug!(
"attempting to save config file to '{}'",
save_location.display()
);
save_formatted_config_to_file(self, &save_location).map_err(|source| {
NymNodeError::ConfigSaveFailure {
id: self.id.clone(),
path: save_location,
source,
}
})
}
pub fn save_location(&self) -> PathBuf {
self.save_path
.clone()
.unwrap_or(self.default_save_location())
}
pub fn default_save_location(&self) -> PathBuf {
default_config_filepath(&self.id)
}
pub fn default_data_directory<P: AsRef<Path>>(config_path: P) -> Result<PathBuf, NymNodeError> {
let config_path = config_path.as_ref();
// we got a proper path to the .toml file
let Some(config_dir) = config_path.parent() else {
error!(
"'{}' does not have a parent directory. Have you pointed to the fs root?",
config_path.display()
);
return Err(NymNodeError::DataDirDerivationFailure);
};
let Some(config_dir_name) = config_dir.file_name() else {
error!(
"could not obtain parent directory name of '{}'. Have you used relative paths?",
config_path.display()
);
return Err(NymNodeError::DataDirDerivationFailure);
};
if config_dir_name != DEFAULT_CONFIG_DIR {
error!(
"the parent directory of '{}' ({}) is not {DEFAULT_CONFIG_DIR}. currently this is not supported",
config_path.display(), config_dir_name.to_str().unwrap_or("UNKNOWN")
);
return Err(NymNodeError::DataDirDerivationFailure);
}
let Some(node_dir) = config_dir.parent() else {
error!(
"'{}' does not have a parent directory. Have you pointed to the fs root?",
config_dir.display()
);
return Err(NymNodeError::DataDirDerivationFailure);
};
Ok(node_dir.join(DEFAULT_DATA_DIR))
}
// simple wrapper that reads config file and assigns path location
fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> {
let path = path.as_ref();
let mut loaded: Config1_1_3 =
read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure {
path: path.to_path_buf(),
source,
})?;
loaded.save_path = Some(path.to_path_buf());
debug!("loaded config file from {}", path.display());
Ok(loaded)
}
pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> {
Self::read_from_path(path)
}
}
fn initialise(config: &Wireguard1_1_3) -> std::io::Result<()> {
let mut rng = OsRng;
let x25519_keys = KeyPair::new(&mut rng);
store_keypair(
&x25519_keys,
&config.storage_paths.x25519_wireguard_storage_paths(),
)?;
Ok(())
}
// currently there are no upgrades
pub async fn try_upgrade_config_1_1_3<P: AsRef<Path>>(path: P) -> Result<(), NymNodeError> {
let old_cfg = Config::read_from_path(&path)?;
let cfg = Config {
save_path: old_cfg.save_path,
id: old_cfg.id,
mode: old_cfg.mode,
host: old_cfg.host,
mixnet: old_cfg.mixnet,
storage_paths: old_cfg.storage_paths,
http: old_cfg.http,
wireguard: old_cfg.wireguard,
mixnode: old_cfg.mixnode,
entry_gateway: old_cfg.entry_gateway,
exit_gateway: old_cfg.exit_gateway,
logging: old_cfg.logging,
};
cfg.save()?;
Ok(())
}
+91
View File
@@ -43,6 +43,14 @@ pub const DEFAULT_IPR_ACK_KEY_FILENAME: &str = "aes128ctr_ipr_ack";
pub const DEFAULT_IPR_REPLY_SURB_DB_FILENAME: &str = "ipr_persistent_reply_store.sqlite";
pub const DEFAULT_IPR_GATEWAYS_DB_FILENAME: &str = "ipr_gateways_info_store.sqlite";
pub const DEFAULT_ED25519_AUTH_PRIVATE_IDENTITY_KEY_FILENAME: &str = "ed25519_auth_identity";
pub const DEFAULT_ED25519_AUTH_PUBLIC_IDENTITY_KEY_FILENAME: &str = "ed25519_auth_identity.pub";
pub const DEFAULT_X25519_AUTH_PRIVATE_DH_KEY_FILENAME: &str = "x25519_auth_dh";
pub const DEFAULT_X25519_AUTH_PUBLIC_DH_KEY_FILENAME: &str = "x25519_auth_dh.pub";
pub const DEFAULT_AUTH_ACK_KEY_FILENAME: &str = "aes128ctr_auth_ack";
pub const DEFAULT_AUTH_REPLY_SURB_DB_FILENAME: &str = "auth_persistent_reply_store.sqlite";
pub const DEFAULT_AUTH_GATEWAYS_DB_FILENAME: &str = "auth_gateways_info_store.sqlite";
// Wireguard
pub const DEFAULT_X25519_WG_DH_KEY_FILENAME: &str = "x25519_wg_dh";
pub const DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME: &str = "x25519_wg_dh.pub";
@@ -195,6 +203,8 @@ pub struct ExitGatewayPaths {
pub network_requester: NetworkRequesterPaths,
pub ip_packet_router: IpPacketRouterPaths,
pub authenticator: AuthenticatorPaths,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
@@ -357,12 +367,93 @@ impl IpPacketRouterPaths {
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AuthenticatorPaths {
/// Path to file containing authenticator ed25519 identity private key.
pub private_ed25519_identity_key_file: PathBuf,
/// Path to file containing authenticator ed25519 identity public key.
pub public_ed25519_identity_key_file: PathBuf,
/// Path to file containing authenticator x25519 diffie hellman private key.
pub private_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing authenticator x25519 diffie hellman public key.
pub public_x25519_diffie_hellman_key_file: PathBuf,
/// Path to file containing key used for encrypting and decrypting the content of an
/// acknowledgement so that nobody besides the client knows which packet it refers to.
pub ack_key_file: PathBuf,
/// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
pub reply_surb_database: PathBuf,
/// Normally this is a path to the file containing information about gateways used by this client,
/// i.e. details such as their public keys, owner addresses or the network information.
/// but in this case it just has the basic information of "we're using custom gateway".
/// Due to how clients are started up, this file has to exist.
pub gateway_registrations: PathBuf,
// it's possible we might have to add credential storage here for return tickets
}
impl AuthenticatorPaths {
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
let data_dir = data_dir.as_ref();
AuthenticatorPaths {
private_ed25519_identity_key_file: data_dir
.join(DEFAULT_ED25519_AUTH_PRIVATE_IDENTITY_KEY_FILENAME),
public_ed25519_identity_key_file: data_dir
.join(DEFAULT_ED25519_AUTH_PUBLIC_IDENTITY_KEY_FILENAME),
private_x25519_diffie_hellman_key_file: data_dir
.join(DEFAULT_X25519_AUTH_PRIVATE_DH_KEY_FILENAME),
public_x25519_diffie_hellman_key_file: data_dir
.join(DEFAULT_X25519_AUTH_PUBLIC_DH_KEY_FILENAME),
ack_key_file: data_dir.join(DEFAULT_AUTH_ACK_KEY_FILENAME),
reply_surb_database: data_dir.join(DEFAULT_AUTH_REPLY_SURB_DB_FILENAME),
gateway_registrations: data_dir.join(DEFAULT_AUTH_GATEWAYS_DB_FILENAME),
}
}
pub fn to_common_client_paths(&self) -> CommonClientPaths {
CommonClientPaths {
keys: ClientKeysPaths {
private_identity_key_file: self.private_ed25519_identity_key_file.clone(),
public_identity_key_file: self.public_ed25519_identity_key_file.clone(),
private_encryption_key_file: self.private_x25519_diffie_hellman_key_file.clone(),
public_encryption_key_file: self.public_x25519_diffie_hellman_key_file.clone(),
ack_key_file: self.ack_key_file.clone(),
},
gateway_registrations: self.gateway_registrations.clone(),
// not needed for embedded providers
credentials_database: Default::default(),
reply_surb_database: self.reply_surb_database.clone(),
}
}
pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath {
nym_pemstore::KeyPairPath::new(
&self.private_ed25519_identity_key_file,
&self.public_ed25519_identity_key_file,
)
}
pub fn x25519_diffie_hellman_storage_paths(&self) -> nym_pemstore::KeyPairPath {
nym_pemstore::KeyPairPath::new(
&self.private_x25519_diffie_hellman_key_file,
&self.public_x25519_diffie_hellman_key_file,
)
}
}
impl ExitGatewayPaths {
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
let data_dir = data_dir.as_ref();
ExitGatewayPaths {
network_requester: NetworkRequesterPaths::new(data_dir),
ip_packet_router: IpPacketRouterPaths::new(data_dir),
authenticator: AuthenticatorPaths::new(data_dir),
}
}
}
+8 -137
View File
@@ -1,151 +1,22 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::config::persistence::WireguardPaths;
use tracing::debug;
use crate::config::old_configs::*;
use crate::config::Config;
use crate::error::NymNodeError;
use std::path::Path;
// currently there are no upgrades
async fn try_upgrade_config<P: AsRef<Path>>(path: P) -> Result<(), NymNodeError> {
use crate::config::*;
use crate::error::KeyIOFailure;
use nym_crypto::asymmetric::encryption::KeyPair;
use nym_pemstore::store_keypair;
use rand::rngs::OsRng;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct OldWireguardPaths {
// pub keys:
async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> {
if try_upgrade_config_1_1_2(path).await.is_ok() {
debug!("Updated from 1.1.2 or previous");
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct OldWireguard {
/// Specifies whether the wireguard service is enabled on this node.
pub enabled: bool,
/// Socket address this node will use for binding its wireguard interface.
/// default: `0.0.0.0:51822`
pub bind_address: SocketAddr,
/// Ip address of the private wireguard network.
/// default: `10.1.0.0`
pub private_network_ip: IpAddr,
/// Port announced to external clients wishing to connect to the wireguard interface.
/// Useful in the instances where the node is behind a proxy.
pub announced_port: u16,
/// 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
pub private_network_prefix: u8,
/// Paths for wireguard keys, client registries, etc.
pub storage_paths: OldWireguardPaths,
if try_upgrade_config_1_1_3(path).await.is_ok() {
debug!("Updated from 1.1.3");
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OldConfig {
// additional metadata holding on-disk location of this config file
#[serde(skip)]
pub(crate) save_path: Option<PathBuf>,
/// Human-readable ID of this particular node.
pub id: String,
/// Current mode of this nym-node.
/// Expect this field to be changed in the future to allow running the node in multiple modes (i.e. mixnode + gateway)
pub mode: NodeMode,
pub host: Host,
pub mixnet: Mixnet,
/// Storage paths to persistent nym-node data, such as its long term keys.
pub storage_paths: NymNodePaths,
#[serde(default)]
pub http: Http,
pub wireguard: OldWireguard,
pub mixnode: MixnodeConfig,
pub entry_gateway: EntryGatewayConfig,
pub exit_gateway: ExitGatewayConfig,
#[serde(default)]
pub logging: LoggingSettings,
}
impl NymConfigTemplate for OldConfig {
fn template(&self) -> &'static str {
CONFIG_TEMPLATE
}
}
impl OldConfig {
fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> {
let path = path.as_ref();
let mut loaded: OldConfig = read_config_from_toml_file(path).map_err(|source| {
NymNodeError::ConfigLoadFailure {
path: path.to_path_buf(),
source,
}
})?;
loaded.save_path = Some(path.to_path_buf());
debug!("loaded config file from {}", path.display());
Ok(loaded)
}
}
fn initialise(config: &Wireguard) -> std::io::Result<()> {
let mut rng = OsRng;
let x25519_keys = KeyPair::new(&mut rng);
store_keypair(
&x25519_keys,
&config.storage_paths.x25519_wireguard_storage_paths(),
)?;
Ok(())
}
let old_cfg = OldConfig::read_from_path(&path)?;
let wireguard = Wireguard {
enabled: old_cfg.wireguard.enabled,
bind_address: old_cfg.wireguard.bind_address,
private_ip: old_cfg.wireguard.private_network_ip,
announced_port: old_cfg.wireguard.announced_port,
private_network_prefix: old_cfg.wireguard.private_network_prefix,
storage_paths: WireguardPaths::new(Config::default_data_directory(path)?),
};
initialise(&wireguard).map_err(|err| KeyIOFailure::KeyPairStoreFailure {
keys: "wg-x25519-dh".to_string(),
paths: wireguard.storage_paths.x25519_wireguard_storage_paths(),
err,
})?;
let cfg = Config {
save_path: old_cfg.save_path,
id: old_cfg.id,
mode: old_cfg.mode,
host: old_cfg.host,
mixnet: old_cfg.mixnet,
storage_paths: old_cfg.storage_paths,
http: old_cfg.http,
wireguard,
mixnode: old_cfg.mixnode,
entry_gateway: old_cfg.entry_gateway,
exit_gateway: old_cfg.exit_gateway,
logging: old_cfg.logging,
};
cfg.save()?;
Ok(())
}
+37 -26
View File
@@ -8,7 +8,6 @@ use crate::node::helpers::{
store_x25519_sphinx_keypair, DisplayDetails,
};
use crate::node::http::{sign_host_details, system_info::get_system_info};
use ipnetwork::IpNetwork;
use nym_bin_common::bin_info_owned;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_gateway::Gateway;
@@ -26,7 +25,6 @@ use nym_node::config::{
use nym_node::error::{EntryGatewayError, ExitGatewayError, MixnodeError, NymNodeError};
use nym_node_http_api::api::api_requests;
use nym_node_http_api::api::api_requests::v1::node::models::NodeDescription;
use nym_node_http_api::router::WireguardAppState;
use nym_node_http_api::state::metrics::{SharedMixingStats, SharedVerlocStats};
use nym_node_http_api::state::AppState;
use nym_node_http_api::{NymNodeHTTPServer, NymNodeRouter};
@@ -68,7 +66,6 @@ impl MixnodeData {
pub struct EntryGatewayData {
mnemonic: Zeroizing<bip39::Mnemonic>,
client_storage: nym_gateway::node::PersistentStorage,
wireguard_data: WireguardGatewayData,
}
impl EntryGatewayData {
@@ -86,10 +83,7 @@ impl EntryGatewayData {
Ok(())
}
async fn new(
config: &EntryGatewayConfig,
wireguard_data: WireguardGatewayData,
) -> Result<EntryGatewayData, EntryGatewayError> {
async fn new(config: &EntryGatewayConfig) -> Result<EntryGatewayData, EntryGatewayError> {
Ok(EntryGatewayData {
mnemonic: config.storage_paths.load_mnemonic_from_file()?,
client_storage: nym_gateway::node::PersistentStorage::init(
@@ -98,7 +92,6 @@ impl EntryGatewayData {
)
.await
.map_err(nym_gateway::GatewayError::from)?,
wireguard_data: wireguard_data.clone(),
})
}
}
@@ -113,6 +106,9 @@ pub struct ExitGatewayData {
ipr_ed25519: ed25519::PublicKey,
ipr_x25519: x25519::PublicKey,
auth_ed25519: ed25519::PublicKey,
auth_x25519: x25519::PublicKey,
}
impl ExitGatewayData {
@@ -243,11 +239,24 @@ impl ExitGatewayData {
"ip packet router x25519",
)?;
let auth_paths = &config.storage_paths.authenticator;
let auth_ed25519 = load_key(
&auth_paths.public_ed25519_identity_key_file,
"authenticator ed25519",
)?;
let auth_x25519 = load_key(
&auth_paths.public_x25519_diffie_hellman_key_file,
"authenticator x25519",
)?;
Ok(ExitGatewayData {
nr_ed25519,
nr_x25519,
ipr_ed25519,
ipr_x25519,
auth_ed25519,
auth_x25519,
})
}
}
@@ -386,11 +395,7 @@ impl NymNode {
description: load_node_description(&config.storage_paths.description)?,
verloc_stats: Default::default(),
mixnode: MixnodeData::new(&config.mixnode)?,
entry_gateway: EntryGatewayData::new(
&config.entry_gateway,
wireguard_data.inner.clone(),
)
.await?,
entry_gateway: EntryGatewayData::new(&config.entry_gateway).await?,
exit_gateway: ExitGatewayData::new(&config.exit_gateway)?,
wireguard: wireguard_data,
config,
@@ -422,6 +427,14 @@ impl NymNode {
)
}
fn exit_authenticator_address(&self) -> Recipient {
Recipient::new(
self.exit_gateway.auth_ed25519,
self.exit_gateway.auth_x25519,
*self.ed25519_identity_keys.public_key(),
)
}
fn x25519_wireguard_key(&self) -> &x25519::PublicKey {
self.wireguard.inner.keypair().public_key()
}
@@ -487,6 +500,7 @@ impl NymNode {
config,
None,
None,
None,
self.ed25519_identity_keys.clone(),
self.x25519_sphinx_keys.clone(),
self.entry_gateway.client_storage.clone(),
@@ -514,6 +528,7 @@ impl NymNode {
config.gateway,
Some(config.nr_opts),
Some(config.ipr_opts),
Some(config.auth_opts),
self.ed25519_identity_keys.clone(),
self.x25519_sphinx_keys.clone(),
self.entry_gateway.client_storage.clone(),
@@ -584,6 +599,13 @@ impl NymNode {
encoded_x25519_key: self.exit_gateway.ipr_x25519.to_base58_string(),
address: self.exit_ip_packet_router_address().to_string(),
};
let auth_details = api_requests::v1::authenticator::models::Authenticator {
encoded_identity_key: self.exit_gateway.auth_ed25519.to_base58_string(),
encoded_x25519_key: self.exit_gateway.auth_x25519.to_base58_string(),
address: self.exit_authenticator_address().to_string(),
};
let exit_policy_details =
api_requests::v1::network_requester::exit_policy::models::UsedExitPolicy {
enabled: true,
@@ -597,24 +619,13 @@ impl NymNode {
policy: None,
};
let wireguard_private_network = IpNetwork::new(
self.config.wireguard.private_ip,
self.config.wireguard.private_network_prefix,
)?;
let wg_state = WireguardAppState::new(
self.entry_gateway.wireguard_data.clone(),
Default::default(),
self.config.wireguard.bind_address.port(),
wireguard_private_network,
)?;
let mut config = nym_node_http_api::Config::new(bin_info_owned!(), host_details)
.with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref())
.with_mixnode_details(mixnode_details)
.with_gateway_details(gateway_details)
.with_network_requester_details(nr_details)
.with_ip_packet_router_details(ipr_details)
.with_authenticator_details(auth_details)
.with_used_exit_policy(exit_policy_details)
.with_description(self.description.clone())
.with_auxiliary_details(auxiliary_details);
@@ -640,7 +651,7 @@ impl NymNode {
.with_verloc_stats(self.verloc_stats.clone())
.with_metrics_key(self.config.http.access_token.clone());
Ok(NymNodeRouter::new(config, Some(app_state), Some(wg_state))
Ok(NymNodeRouter::new(config, Some(app_state))
.build_server(&self.config.http.bind_address)
.await?)
}
@@ -0,0 +1,43 @@
[package]
name = "nym-authenticator"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
anyhow = { workspace = true }
bincode = { workspace = true }
bs58 = { workspace = true }
bytes = { workspace = true }
clap = { workspace = true }
fastrand = { workspace = true }
futures = { workspace = true }
ipnetwork = { workspace = true }
log = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "net"] }
tokio-stream = { workspace = true }
tokio-util = { workspace = true, features = ["codec"] }
url = { workspace = true }
nym-authenticator-requests = { path = "../../common/authenticator-requests" }
nym-bin-common = { path = "../../common/bin-common" }
nym-client-core = { path = "../../common/client-core" }
nym-config = { path = "../../common/config" }
nym-crypto = { path = "../../common/crypto" }
nym-id = { path = "../../common/nym-id" }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
nym-service-providers-common = { path = "../common" }
nym-sphinx = { path = "../../common/nymsphinx" }
nym-task = { path = "../../common/task" }
nym-types = { path = "../../common/types" }
nym-wireguard = { path = "../../common/wireguard" }
nym-wireguard-types = { path = "../../common/wireguard-types" }
@@ -0,0 +1,130 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::path::Path;
use futures::channel::oneshot;
use ipnetwork::IpNetwork;
use nym_client_core::{HardcodedTopologyProvider, TopologyProvider};
use nym_sdk::mixnet::Recipient;
use nym_task::{TaskClient, TaskHandle};
use nym_wireguard::WireguardGatewayData;
use crate::{config::Config, error::AuthenticatorError};
pub struct OnStartData {
// to add more fields as required
pub address: Recipient,
}
impl OnStartData {
pub fn new(address: Recipient) -> Self {
Self { address }
}
}
pub struct Authenticator {
#[allow(unused)]
config: Config,
wait_for_gateway: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
wireguard_gateway_data: WireguardGatewayData,
shutdown: Option<TaskClient>,
on_start: Option<oneshot::Sender<OnStartData>>,
}
impl Authenticator {
pub fn new(config: Config, wireguard_gateway_data: WireguardGatewayData) -> Self {
Self {
config,
wait_for_gateway: false,
custom_topology_provider: None,
wireguard_gateway_data,
shutdown: None,
on_start: None,
}
}
#[must_use]
#[allow(unused)]
pub fn with_shutdown(mut self, shutdown: TaskClient) -> Self {
self.shutdown = Some(shutdown);
self
}
#[must_use]
#[allow(unused)]
pub fn with_wait_for_gateway(mut self, wait_for_gateway: bool) -> Self {
self.wait_for_gateway = wait_for_gateway;
self
}
#[must_use]
#[allow(unused)]
pub fn with_on_start(mut self, on_start: oneshot::Sender<OnStartData>) -> Self {
self.on_start = Some(on_start);
self
}
#[must_use]
#[allow(unused)]
pub fn with_custom_topology_provider(
mut self,
topology_provider: Box<dyn TopologyProvider + Send + Sync>,
) -> Self {
self.custom_topology_provider = Some(topology_provider);
self
}
pub fn with_stored_topology<P: AsRef<Path>>(
mut self,
file: P,
) -> Result<Self, AuthenticatorError> {
self.custom_topology_provider =
Some(Box::new(HardcodedTopologyProvider::new_from_file(file)?));
Ok(self)
}
pub async fn run_service_provider(self) -> Result<(), AuthenticatorError> {
// Used to notify tasks to shutdown. Not all tasks fully supports this (yet).
let task_handle: TaskHandle = self.shutdown.map(Into::into).unwrap_or_default();
// Connect to the mixnet
let mixnet_client = crate::mixnet_client::create_mixnet_client(
&self.config.base,
task_handle.get_handle().named("nym_sdk::MixnetClient"),
None,
self.custom_topology_provider,
self.wait_for_gateway,
&self.config.storage_paths.common_paths,
)
.await?;
let self_address = *mixnet_client.nym_address();
let private_ip_network = IpNetwork::new(
self.config.authenticator.private_ip,
self.config.authenticator.private_network_prefix,
)?;
let mixnet_listener = crate::mixnet_listener::MixnetListener::new(
self.config,
private_ip_network,
self.wireguard_gateway_data,
mixnet_client,
task_handle,
);
log::info!("The address of this client is: {self_address}");
log::info!("All systems go. Press CTRL-C to stop the server.");
if let Some(on_start) = self.on_start {
if on_start.send(OnStartData::new(self_address)).is_err() {
// the parent has dropped the channel before receiving the response
return Err(AuthenticatorError::DisconnectedParent);
}
}
mixnet_listener.run().await
}
}
@@ -0,0 +1,30 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CliAuthenticatorClient;
use nym_authenticator::error::AuthenticatorError;
use nym_bin_common::output_format::OutputFormat;
use nym_client_core::cli_helpers::client_add_gateway::{add_gateway, CommonClientAddGatewayArgs};
#[derive(clap::Args)]
pub(crate) struct Args {
#[command(flatten)]
common_args: CommonClientAddGatewayArgs,
#[arg(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
impl AsRef<CommonClientAddGatewayArgs> for Args {
fn as_ref(&self) -> &CommonClientAddGatewayArgs {
&self.common_args
}
}
pub(crate) async fn execute(args: Args) -> Result<(), AuthenticatorError> {
let output = args.output;
let res = add_gateway::<CliAuthenticatorClient, _>(args, None).await?;
println!("{}", output.format(&res));
Ok(())
}
@@ -0,0 +1,16 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Args;
use nym_bin_common::bin_info_owned;
use nym_bin_common::output_format::OutputFormat;
#[derive(Args)]
pub(crate) struct BuildInfo {
#[arg(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
pub(crate) fn execute(args: BuildInfo) {
println!("{}", args.output.format(&bin_info_owned!()))
}
@@ -0,0 +1,16 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CliAuthenticatorClient;
use nym_authenticator::error::AuthenticatorError;
use nym_client_core::cli_helpers::client_import_credential::{
import_credential, CommonClientImportCredentialArgs,
};
pub(crate) async fn execute(
args: CommonClientImportCredentialArgs,
) -> Result<(), AuthenticatorError> {
import_credential::<CliAuthenticatorClient, _>(args).await?;
println!("successfully imported credential!");
Ok(())
}
@@ -0,0 +1,96 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::{override_config, CliAuthenticatorClient, OverrideConfig};
use clap::Args;
use nym_authenticator::{
config::{default_config_directory, default_config_filepath, default_data_directory, Config},
error::AuthenticatorError,
};
use nym_bin_common::output_format::OutputFormat;
use nym_client_core::cli_helpers::client_init::{
initialise_client, CommonClientInitArgs, InitResultsWithConfig, InitialisableClient,
};
use serde::Serialize;
use std::{fmt::Display, fs, path::PathBuf};
impl InitialisableClient for CliAuthenticatorClient {
type InitArgs = Init;
fn initialise_storage_paths(id: &str) -> Result<(), Self::Error> {
fs::create_dir_all(default_data_directory(id))?;
fs::create_dir_all(default_config_directory(id))?;
Ok(())
}
fn default_config_path(id: &str) -> PathBuf {
default_config_filepath(id)
}
fn construct_config(init_args: &Self::InitArgs) -> Self::Config {
override_config(
Config::new(&init_args.common_args.id),
OverrideConfig::from(init_args.clone()),
)
}
}
#[derive(Args, Clone, Debug)]
pub(crate) struct Init {
#[command(flatten)]
common_args: CommonClientInitArgs,
#[clap(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
impl From<Init> for OverrideConfig {
fn from(init_config: Init) -> Self {
OverrideConfig {
nym_apis: init_config.common_args.nym_apis,
nyxd_urls: init_config.common_args.nyxd_urls,
enabled_credentials_mode: init_config.common_args.enabled_credentials_mode,
}
}
}
impl AsRef<CommonClientInitArgs> for Init {
fn as_ref(&self) -> &CommonClientInitArgs {
&self.common_args
}
}
#[derive(Debug, Serialize)]
pub struct InitResults {
#[serde(flatten)]
client_core: nym_client_core::init::types::InitResults,
client_address: String,
}
impl InitResults {
fn new(res: InitResultsWithConfig<Config>) -> Self {
Self {
client_address: res.init_results.address.to_string(),
client_core: res.init_results,
}
}
}
impl Display for InitResults {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.client_core)?;
write!(f, "Address of this authenticator: {}", self.client_address)
}
}
pub(crate) async fn execute(args: Init) -> Result<(), AuthenticatorError> {
eprintln!("Initialising client...");
let output = args.output;
let res = initialise_client::<CliAuthenticatorClient>(args, None).await?;
let init_results = InitResults::new(res);
println!("{}", output.format(&init_results));
Ok(())
}
@@ -0,0 +1,32 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CliAuthenticatorClient;
use nym_authenticator::error::AuthenticatorError;
use nym_bin_common::output_format::OutputFormat;
use nym_client_core::cli_helpers::client_list_gateways::{
list_gateways, CommonClientListGatewaysArgs,
};
#[derive(clap::Args)]
pub(crate) struct Args {
#[command(flatten)]
common_args: CommonClientListGatewaysArgs,
#[arg(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
impl AsRef<CommonClientListGatewaysArgs> for Args {
fn as_ref(&self) -> &CommonClientListGatewaysArgs {
&self.common_args
}
}
pub(crate) async fn execute(args: Args) -> Result<(), AuthenticatorError> {
let output = args.output;
let res = list_gateways::<CliAuthenticatorClient, _>(args).await?;
println!("{}", output.format(&res));
Ok(())
}
@@ -0,0 +1,196 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{CommandFactory, Parser, Subcommand};
use log::error;
use nym_authenticator::{
config::{helpers::try_upgrade_config, BaseClientConfig, Config},
error::AuthenticatorError,
};
use nym_bin_common::completions::{fig_generate, ArgShell};
use nym_bin_common::{bin_info, version_checker};
use nym_client_core::cli_helpers::client_import_credential::CommonClientImportCredentialArgs;
use nym_client_core::cli_helpers::CliClient;
use std::sync::OnceLock;
mod add_gateway;
mod build_info;
mod import_credential;
mod init;
mod list_gateways;
mod peer_handler;
mod run;
mod sign;
mod switch_gateway;
pub(crate) struct CliAuthenticatorClient;
impl CliClient for CliAuthenticatorClient {
const NAME: &'static str = "authenticator";
type Error = AuthenticatorError;
type Config = Config;
async fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> {
try_upgrade_config(id).await
}
async fn try_load_current_config(id: &str) -> Result<Self::Config, Self::Error> {
try_load_current_config(id).await
}
}
fn pretty_build_info_static() -> &'static str {
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
}
#[derive(Parser)]
#[command(author = "Nymtech", version, about, long_version = pretty_build_info_static())]
pub(crate) struct Cli {
/// Path pointing to an env file that configures the client.
#[arg(short, long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
/// Flag used for disabling the printed banner in tty.
#[arg(long)]
pub(crate) no_banner: bool,
#[command(subcommand)]
command: Commands,
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Initialize an authenticator. Do this first!
Init(init::Init),
/// Run the authenticator with the provided configuration and optionally override
/// parameters.
Run(run::Run),
/// Import a pre-generated credential
ImportCredential(CommonClientImportCredentialArgs),
/// List all registered with gateways
ListGateways(list_gateways::Args),
/// Add new gateway to this client
AddGateway(add_gateway::Args),
/// Change the currently active gateway. Note that you must have already registered with the new gateway!
SwitchGateway(switch_gateway::Args),
/// Sign to prove ownership of this authenticator
Sign(sign::Sign),
/// Show build information of this binary
BuildInfo(build_info::BuildInfo),
/// Generate shell completions
Completions(ArgShell),
/// Generate Fig specification
GenerateFigSpec,
}
// Configuration that can be overridden.
pub(crate) struct OverrideConfig {
nym_apis: Option<Vec<url::Url>>,
nyxd_urls: Option<Vec<url::Url>>,
enabled_credentials_mode: Option<bool>,
}
pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config {
config
.with_optional_base_custom_env(
BaseClientConfig::with_custom_nym_apis,
args.nym_apis,
nym_network_defaults::var_names::NYM_API,
nym_config::parse_urls,
)
.with_optional_base_custom_env(
BaseClientConfig::with_custom_nyxd,
args.nyxd_urls,
nym_network_defaults::var_names::NYXD,
nym_config::parse_urls,
)
.with_optional_base(
BaseClientConfig::with_disabled_credentials,
args.enabled_credentials_mode.map(|b| !b),
)
}
pub(crate) async fn execute(args: Cli) -> Result<(), AuthenticatorError> {
let bin_name = "nym-authenticator";
match args.command {
Commands::Init(m) => init::execute(m).await?,
Commands::Run(m) => run::execute(&m).await?,
Commands::ImportCredential(m) => import_credential::execute(m).await?,
Commands::ListGateways(args) => list_gateways::execute(args).await?,
Commands::AddGateway(args) => add_gateway::execute(args).await?,
Commands::SwitchGateway(args) => switch_gateway::execute(args).await?,
Commands::Sign(m) => sign::execute(&m).await?,
Commands::BuildInfo(m) => build_info::execute(m),
Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name),
}
Ok(())
}
async fn try_load_current_config(id: &str) -> Result<Config, AuthenticatorError> {
// try to load the config as is
if let Ok(cfg) = Config::read_from_default_path(id) {
return if !cfg.validate() {
Err(AuthenticatorError::ConfigValidationFailure)
} else {
Ok(cfg)
};
}
// we couldn't load it - try upgrading it from older revisions
try_upgrade_config(id).await?;
let config = match Config::read_from_default_path(id) {
Ok(cfg) => cfg,
Err(err) => {
error!("Failed to load config for {id}. Are you sure you have run `init` before? (Error was: {err})");
return Err(AuthenticatorError::FailedToLoadConfig(id.to_string()));
}
};
if !config.validate() {
return Err(AuthenticatorError::ConfigValidationFailure);
}
Ok(config)
}
// this only checks compatibility between config the binary. It does not take into consideration
// network version. It might do so in the future.
fn version_check(cfg: &Config) -> bool {
let binary_version = env!("CARGO_PKG_VERSION");
let config_version = &cfg.base.client.version;
if binary_version == config_version {
true
} else {
log::warn!(
"The native-client binary has different version than what is specified \
in config file! {binary_version} and {config_version}",
);
if version_checker::is_minor_version_compatible(binary_version, config_version) {
log::info!(
"but they are still semver compatible. \
However, consider running the `upgrade` command"
);
true
} else {
log::error!(
"and they are semver incompatible! - \
please run the `upgrade` command before attempting `run` again"
);
false
}
}
}
@@ -0,0 +1,49 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_sdk::TaskClient;
use nym_wireguard::peer_controller::PeerControlMessage;
use tokio::sync::mpsc;
pub struct DummyHandler {
peer_rx: mpsc::UnboundedReceiver<PeerControlMessage>,
task_client: TaskClient,
}
impl DummyHandler {
pub fn new(
peer_rx: mpsc::UnboundedReceiver<PeerControlMessage>,
task_client: TaskClient,
) -> Self {
DummyHandler {
peer_rx,
task_client,
}
}
pub async fn run(mut self) {
while !self.task_client.is_shutdown() {
tokio::select! {
msg = self.peer_rx.recv() => {
if let Some(msg) = msg {
match msg {
PeerControlMessage::AddPeer(peer) => {
log::info!("[DUMMY] Adding peer {:?}", peer);
}
PeerControlMessage::RemovePeer(key) => {
log::info!("[DUMMY] Removing peer {:?}", key);
}
}
} else {
break;
}
}
_ = self.task_client.recv() => {
log::trace!("DummyHandler: Received shutdown");
}
}
}
log::debug!("DummyHandler: Exiting");
}
}
@@ -0,0 +1,61 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::sync::Arc;
use crate::cli::peer_handler::DummyHandler;
use crate::cli::{override_config, OverrideConfig};
use crate::cli::{try_load_current_config, version_check};
use clap::Args;
use log::error;
use nym_authenticator::error::AuthenticatorError;
use nym_client_core::cli_helpers::client_run::CommonClientRunArgs;
use nym_crypto::asymmetric::x25519::KeyPair;
use nym_task::TaskHandle;
use nym_wireguard::WireguardGatewayData;
use rand::rngs::OsRng;
#[allow(clippy::struct_excessive_bools)]
#[derive(Args, Clone)]
pub(crate) struct Run {
#[command(flatten)]
common_args: CommonClientRunArgs,
}
impl From<Run> for OverrideConfig {
fn from(run_config: Run) -> Self {
OverrideConfig {
nym_apis: None,
nyxd_urls: run_config.common_args.nyxd_urls,
enabled_credentials_mode: run_config.common_args.enabled_credentials_mode,
}
}
}
pub(crate) async fn execute(args: &Run) -> Result<(), AuthenticatorError> {
let mut config = try_load_current_config(&args.common_args.id).await?;
config = override_config(config, OverrideConfig::from(args.clone()));
log::debug!("Using config: {:#?}", config);
if !version_check(&config) {
error!("failed the local version check");
return Err(AuthenticatorError::FailedLocalVersionCheck);
}
log::info!("Starting authenticator service provider");
let (wireguard_gateway_data, peer_rx) = WireguardGatewayData::new(
config.authenticator.clone().into(),
Arc::new(KeyPair::new(&mut OsRng)),
);
let task_handler = TaskHandle::default();
let handler = DummyHandler::new(peer_rx, task_handler.fork("peer-handler"));
tokio::spawn(async move {
handler.run().await;
});
let mut server = nym_authenticator::Authenticator::new(config, wireguard_gateway_data);
if let Some(custom_mixnet) = &args.common_args.custom_mixnet {
server = server.with_stored_topology(custom_mixnet)?
}
server.run_service_provider().await
}
@@ -0,0 +1,79 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::{try_load_current_config, version_check};
use clap::Args;
use nym_authenticator::error::AuthenticatorError;
use nym_bin_common::output_format::OutputFormat;
use nym_client_core::client::key_manager::persistence::OnDiskKeys;
use nym_client_core::error::ClientCoreError;
use nym_crypto::asymmetric::identity;
use nym_types::helpers::ConsoleSigningOutput;
#[derive(Args, Clone)]
pub(crate) struct Sign {
/// The id of the mixnode you want to sign with
#[arg(long)]
id: String,
/// Signs a transaction-specific payload, that is going to be sent to the smart contract, with your identity key
#[arg(long)]
contract_msg: String,
#[arg(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
fn print_signed_contract_msg(
private_key: &identity::PrivateKey,
raw_msg: &str,
output: OutputFormat,
) {
let trimmed = raw_msg.trim();
eprintln!(">>> attempting to sign {trimmed}");
let Ok(decoded) = bs58::decode(trimmed).into_vec() else {
println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters");
return;
};
eprintln!(">>> decoding the message...");
// we don't really care about what particular information is embedded inside of it,
// we just want to know if user correctly copied the string, i.e. whether it's a valid bs58 encoded json
if serde_json::from_slice::<serde_json::Value>(&decoded).is_err() {
println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters");
return;
};
// if this is a valid json, it MUST be a valid string
let decoded_string = String::from_utf8(decoded.clone()).unwrap();
let signature = private_key.sign(&decoded).to_base58_string();
let sign_output = ConsoleSigningOutput::new(decoded_string, signature);
println!("{}", output.format(&sign_output));
}
pub(crate) async fn execute(args: &Sign) -> Result<(), AuthenticatorError> {
let config = try_load_current_config(&args.id).await?;
if !version_check(&config) {
log::error!("Failed the local version check");
return Err(AuthenticatorError::FailedLocalVersionCheck);
}
let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys);
let identity_keypair = key_store.load_identity_keypair().map_err(|source| {
AuthenticatorError::ClientCoreError(ClientCoreError::KeyStoreError {
source: Box::new(source),
})
})?;
print_signed_contract_msg(
identity_keypair.private_key(),
&args.contract_msg,
args.output,
);
Ok(())
}
@@ -0,0 +1,24 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CliAuthenticatorClient;
use nym_authenticator::error::AuthenticatorError;
use nym_client_core::cli_helpers::client_switch_gateway::{
switch_gateway, CommonClientSwitchGatewaysArgs,
};
#[derive(clap::Args, Clone, Debug)]
pub struct Args {
#[command(flatten)]
common_args: CommonClientSwitchGatewaysArgs,
}
impl AsRef<CommonClientSwitchGatewaysArgs> for Args {
fn as_ref(&self) -> &CommonClientSwitchGatewaysArgs {
&self.common_args
}
}
pub(crate) async fn execute(args: Args) -> Result<(), AuthenticatorError> {
switch_gateway::<CliAuthenticatorClient, _>(args).await
}
@@ -0,0 +1,13 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use log::trace;
use std::path::Path;
use crate::error::AuthenticatorError;
pub async fn try_upgrade_config<P: AsRef<Path>>(_config_path: P) -> Result<(), AuthenticatorError> {
trace!("Attempting to upgrade config");
Ok(())
}
@@ -0,0 +1,226 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_bin_common::logging::LoggingSettings;
pub use nym_client_core::config::Config as BaseClientConfig;
use nym_client_core::{cli_helpers::CliClientConfig, config::disk_persistence::CommonClientPaths};
use nym_config::{
must_get_home, save_formatted_config_to_file, NymConfigTemplate, OptionalSet,
DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR,
};
use nym_network_defaults::WG_PORT;
use nym_service_providers_common::DEFAULT_SERVICE_PROVIDERS_DIR;
pub use persistence::AuthenticatorPaths;
use serde::{Deserialize, Serialize};
use std::{
io,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::{Path, PathBuf},
str::FromStr,
};
use template::CONFIG_TEMPLATE;
pub mod helpers;
pub mod persistence;
pub mod template;
const DEFAULT_AUTHENTICATOR_DIR: &str = "authenticator";
/// Derive default path to authenticator's config directory.
/// It should get resolved to `$HOME/.nym/service-providers/authenticator/<id>/config`
pub fn default_config_directory<P: AsRef<Path>>(id: P) -> PathBuf {
must_get_home()
.join(NYM_DIR)
.join(DEFAULT_SERVICE_PROVIDERS_DIR)
.join(DEFAULT_AUTHENTICATOR_DIR)
.join(id)
.join(DEFAULT_CONFIG_DIR)
}
/// Derive default path to authenticator's config file.
/// It should get resolved to `$HOME/.nym/service-providers/authenticator/<id>/config/config.toml`
pub fn default_config_filepath<P: AsRef<Path>>(id: P) -> PathBuf {
default_config_directory(id).join(DEFAULT_CONFIG_FILENAME)
}
/// Derive default path to authenticator's data directory where files, such as keys, are stored.
/// It should get resolved to `$HOME/.nym/service-providers/authenticator/<id>/data`
pub fn default_data_directory<P: AsRef<Path>>(id: P) -> PathBuf {
must_get_home()
.join(NYM_DIR)
.join(DEFAULT_SERVICE_PROVIDERS_DIR)
.join(DEFAULT_AUTHENTICATOR_DIR)
.join(id)
.join(DEFAULT_DATA_DIR)
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(flatten)]
pub base: BaseClientConfig,
#[serde(default)]
pub authenticator: Authenticator,
pub storage_paths: AuthenticatorPaths,
pub logging: LoggingSettings,
}
impl NymConfigTemplate for Config {
fn template(&self) -> &'static str {
CONFIG_TEMPLATE
}
}
impl CliClientConfig for Config {
fn common_paths(&self) -> &CommonClientPaths {
&self.storage_paths.common_paths
}
fn core_config(&self) -> &BaseClientConfig {
&self.base
}
fn default_store_location(&self) -> PathBuf {
self.default_location()
}
fn save_to<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
save_formatted_config_to_file(self, path)
}
}
impl Config {
pub fn new<S: AsRef<str>>(id: S) -> Self {
Config {
base: BaseClientConfig::new(id.as_ref(), env!("CARGO_PKG_VERSION")),
authenticator: Default::default(),
storage_paths: AuthenticatorPaths::new_base(default_data_directory(id.as_ref())),
logging: Default::default(),
}
}
#[allow(unused)]
pub fn with_data_directory<P: AsRef<Path>>(mut self, data_directory: P) -> Self {
self.storage_paths = AuthenticatorPaths::new_base(data_directory);
self
}
pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> io::Result<Self> {
nym_config::read_config_from_toml_file(path)
}
pub fn read_from_default_path<P: AsRef<Path>>(id: P) -> io::Result<Self> {
Self::read_from_toml_file(default_config_filepath(id))
}
pub fn default_location(&self) -> PathBuf {
default_config_filepath(&self.base.client.id)
}
#[allow(unused)]
pub fn save_to_default_location(&self) -> io::Result<()> {
let config_save_location: PathBuf = self.default_location();
save_formatted_config_to_file(self, config_save_location)
}
pub fn validate(&self) -> bool {
// no other sections have explicit requirements (yet)
self.base.validate()
}
#[doc(hidden)]
pub fn set_no_poisson_process(&mut self) {
self.base.set_no_poisson_process()
}
// poor man's 'builder' method
#[allow(unused)]
pub fn with_base<F, T>(mut self, f: F, val: T) -> Self
where
F: Fn(BaseClientConfig, T) -> BaseClientConfig,
{
self.base = f(self.base, val);
self
}
// helper methods to use `OptionalSet` trait. Those are defined due to very... ehm. 'specific' structure of this config
// (plz, lets refactor it)
pub fn with_optional_base<F, T>(mut self, f: F, val: Option<T>) -> Self
where
F: Fn(BaseClientConfig, T) -> BaseClientConfig,
{
self.base = self.base.with_optional(f, val);
self
}
#[allow(unused)]
pub fn with_optional_base_env<F, T>(mut self, f: F, val: Option<T>, env_var: &str) -> Self
where
F: Fn(BaseClientConfig, T) -> BaseClientConfig,
T: FromStr,
<T as FromStr>::Err: std::fmt::Debug,
{
self.base = self.base.with_optional_env(f, val, env_var);
self
}
pub fn with_optional_base_custom_env<F, T, G>(
mut self,
f: F,
val: Option<T>,
env_var: &str,
parser: G,
) -> Self
where
F: Fn(BaseClientConfig, T) -> BaseClientConfig,
G: Fn(&str) -> T,
{
self.base = self.base.with_optional_custom_env(f, val, env_var, parser);
self
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct Authenticator {
/// Socket address this node will use for binding its wireguard interface.
/// default: `0.0.0.0:51822`
pub bind_address: SocketAddr,
/// Private IP address of the wireguard gateway.
/// default: `10.1.0.1`
pub private_ip: IpAddr,
/// Port announced to external clients wishing to connect to the wireguard interface.
/// Useful in the instances where the node is behind a proxy.
pub announced_port: u16,
/// 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
pub private_network_prefix: u8,
}
impl Default for Authenticator {
fn default() -> Self {
Self {
bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 51822),
private_ip: IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1)),
announced_port: WG_PORT,
private_network_prefix: 16,
}
}
}
impl From<Authenticator> for nym_wireguard_types::Config {
fn from(value: Authenticator) -> Self {
nym_wireguard_types::Config {
bind_address: value.bind_address,
private_ip: value.private_ip,
announced_port: value.announced_port,
private_network_prefix: value.private_network_prefix,
}
}
}
@@ -0,0 +1,28 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_client_core::config::disk_persistence::CommonClientPaths;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub const DEFAULT_DESCRIPTION_FILENAME: &str = "description.toml";
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone)]
pub struct AuthenticatorPaths {
#[serde(flatten)]
pub common_paths: CommonClientPaths,
/// Location of the file containing our description
pub authenticator_description: PathBuf,
}
impl AuthenticatorPaths {
pub fn new_base<P: AsRef<Path>>(base_data_directory: P) -> Self {
let base_dir = base_data_directory.as_ref();
Self {
common_paths: CommonClientPaths::new_base(base_dir),
authenticator_description: base_dir.join(DEFAULT_DESCRIPTION_FILENAME),
}
}
}
@@ -0,0 +1,101 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub(crate) const CONFIG_TEMPLATE: &str =
// While using normal toml marshalling would have been way simpler with less overhead,
// I think it's useful to have comments attached to the saved config file to explain behaviour of
// particular fields.
// Note: any changes to the template must be reflected in the appropriate structs.
r#"
# This is a TOML config file.
# For more information, see https://github.com/toml-lang/toml
##### main base client config options #####
[client]
# Version of the client for which this configuration was created.
version = '{{ client.version }}'
# Human readable ID of this particular client.
id = '{{ client.id }}'
# Indicates whether this client is running in a disabled credentials mode, thus attempting
# to claim bandwidth without presenting bandwidth credentials.
disabled_credentials_mode = {{ client.disabled_credentials_mode }}
# Addresses to nyxd validators via which the client can communicate with the chain.
nyxd_urls = [
{{#each client.nyxd_urls }}
'{{this}}',
{{/each}}
]
# Addresses to APIs running on validator from which the client gets the view of the network.
nym_api_urls = [
{{#each client.nym_api_urls }}
'{{this}}',
{{/each}}
]
[storage_paths]
# Path to file containing private identity key.
keys.private_identity_key_file = '{{ storage_paths.keys.private_identity_key_file }}'
# Path to file containing public identity key.
keys.public_identity_key_file = '{{ storage_paths.keys.public_identity_key_file }}'
# Path to file containing private encryption key.
keys.private_encryption_key_file = '{{ storage_paths.keys.private_encryption_key_file }}'
# Path to file containing public encryption key.
keys.public_encryption_key_file = '{{ storage_paths.keys.public_encryption_key_file }}'
# Path to file containing key used for encrypting and decrypting the content of an
# acknowledgement so that nobody besides the client knows which packet it refers to.
keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}'
# Path to the database containing bandwidth credentials
credentials_database = '{{ storage_paths.credentials_database }}'
# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
reply_surb_database = '{{ storage_paths.reply_surb_database }}'
# Path to the file containing information about gateways used by this client,
# i.e. details such as their public keys, owner addresses or the network information.
gateway_registrations = '{{ storage_paths.gateway_registrations }}'
# Location of the file containing our allow.list
allowed_list_location = '{{ storage_paths.allowed_list_location }}'
# Location of the file containing our unknown.list
unknown_list_location = '{{ storage_paths.unknown_list_location }}'
# Path to file containing description of this authenticator.
authenticator_description = '{{ storage_paths.authenticator_description }}'
##### logging configuration options #####
[logging]
# TODO
##### debug configuration options #####
# The following options should not be modified unless you know EXACTLY what you are doing
# as if set incorrectly, they may impact your anonymity.
[debug]
[debug.traffic]
average_packet_delay = '{{ debug.traffic.average_packet_delay }}'
message_sending_average_delay = '{{ debug.traffic.message_sending_average_delay }}'
[debug.acknowledgements]
average_ack_delay = '{{ debug.acknowledgements.average_ack_delay }}'
[debug.cover_traffic]
loop_cover_traffic_average_delay = '{{ debug.cover_traffic.loop_cover_traffic_average_delay }}'
"#;
@@ -0,0 +1,72 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use ipnetwork::IpNetworkError;
use nym_client_core::error::ClientCoreError;
use nym_id::NymIdError;
#[derive(thiserror::Error, Debug)]
pub enum AuthenticatorError {
#[error("client-core error: {0}")]
ClientCoreError(#[from] ClientCoreError),
// TODO: add more details here
#[error("failed to validate the loaded config")]
ConfigValidationFailure,
#[error("the entity wrapping the network requester has disconnected")]
DisconnectedParent,
#[error("received empty packet")]
EmptyPacket,
#[error("failed local version check, client and config mismatch")]
FailedLocalVersionCheck,
#[error("failed to connect to mixnet: {source}")]
FailedToConnectToMixnet { source: nym_sdk::Error },
#[error("failed to deserialize tagged packet: {source}")]
FailedToDeserializeTaggedPacket { source: bincode::Error },
#[error("failed to load configuration file: {0}")]
FailedToLoadConfig(String),
#[error("failed to send packet to mixnet: {source}")]
FailedToSendPacketToMixnet { source: nym_sdk::Error },
#[error("failed to serialize response packet: {source}")]
FailedToSerializeResponsePacket { source: Box<bincode::ErrorKind> },
#[error("failed to setup mixnet client: {source}")]
FailedToSetupMixnetClient { source: nym_sdk::Error },
#[error("internal error: {0}")]
InternalError(String),
#[error("received packet has an invalid version: {0}")]
InvalidPacketVersion(u8),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("{0}")]
IpNetworkError(#[from] IpNetworkError),
#[error("mac does not verify")]
MacVerificationFailure,
#[error("no more space in the network")]
NoFreeIp,
#[error(transparent)]
NymIdError(#[from] NymIdError),
#[error("registration is not in progress for the given key")]
RegistrationNotInProgress,
#[error("internal data corruption: {0}")]
InternalDataCorruption(String),
}
pub type Result<T> = std::result::Result<T, AuthenticatorError>;
@@ -0,0 +1,11 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use authenticator::{Authenticator, OnStartData};
pub use config::Config;
pub mod authenticator;
pub mod config;
pub mod error;
pub mod mixnet_client;
pub mod mixnet_listener;
@@ -0,0 +1,20 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod cli;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
use clap::Parser;
let args = cli::Cli::parse();
nym_bin_common::logging::setup_logging();
nym_network_defaults::setup_env(args.config_env_file.as_ref());
if !args.no_banner {
nym_bin_common::logging::maybe_print_banner(clap::crate_name!(), clap::crate_version!());
}
cli::execute(args).await?;
Ok(())
}
@@ -0,0 +1,52 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_client_core::{config::disk_persistence::CommonClientPaths, TopologyProvider};
use nym_sdk::{GatewayTransceiver, NymNetworkDetails};
use nym_task::TaskClient;
use crate::{config::BaseClientConfig, error::AuthenticatorError};
// Helper function to create the mixnet client.
// This is NOT in the SDK since we don't want to expose any of the client-core config types.
// We could however consider moving it to a crate in common in the future.
// TODO: refactor this function and its arguments
pub(crate) async fn create_mixnet_client(
config: &BaseClientConfig,
shutdown: TaskClient,
custom_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
wait_for_gateway: bool,
paths: &CommonClientPaths,
) -> Result<nym_sdk::mixnet::MixnetClient, AuthenticatorError> {
let debug_config = config.debug;
let storage_paths = nym_sdk::mixnet::StoragePaths::from(paths.clone());
let mut client_builder =
nym_sdk::mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths)
.await
.map_err(|err| AuthenticatorError::FailedToSetupMixnetClient { source: err })?
.network_details(NymNetworkDetails::new_from_env())
.debug_config(debug_config)
.custom_shutdown(shutdown)
.with_wait_for_gateway(wait_for_gateway);
if !config.get_disabled_credentials_mode() {
client_builder = client_builder.enable_credentials_mode();
}
if let Some(gateway_transceiver) = custom_transceiver {
client_builder = client_builder.custom_gateway_transceiver(gateway_transceiver);
}
if let Some(topology_provider) = custom_topology_provider {
client_builder = client_builder.custom_topology_provider(topology_provider);
}
let mixnet_client = client_builder
.build()
.map_err(|err| AuthenticatorError::FailedToSetupMixnetClient { source: err })?;
mixnet_client
.connect_to_mixnet()
.await
.map_err(|err| AuthenticatorError::FailedToConnectToMixnet { source: err })
}
@@ -0,0 +1,330 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{
sync::Arc,
time::{Duration, SystemTime},
};
use crate::error::AuthenticatorError;
use futures::StreamExt;
use ipnetwork::IpNetwork;
use nym_authenticator_requests::v1::{
self,
request::{AuthenticatorRequest, AuthenticatorRequestData},
response::AuthenticatorResponse,
};
use nym_sdk::mixnet::{InputMessage, MixnetMessageSender, Recipient, TransmissionLane};
use nym_sphinx::receiver::ReconstructedMessage;
use nym_task::TaskHandle;
use nym_wireguard::WireguardGatewayData;
use nym_wireguard_types::{
registration::{PendingRegistrations, PrivateIPs, RegistrationData},
GatewayClient, InitMessage, PeerPublicKey,
};
use rand::{prelude::IteratorRandom, thread_rng};
use tokio_stream::wrappers::IntervalStream;
use crate::{config::Config, error::*};
type AuthenticatorHandleResult = Result<AuthenticatorResponse>;
const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute
pub(crate) struct MixnetListener {
// The configuration for the mixnet listener
pub(crate) config: Config,
// The mixnet client that we use to send and receive packets from the mixnet
pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient,
// The task handle for the main loop
pub(crate) task_handle: TaskHandle,
// Registrations awaiting confirmation
pub(crate) registration_in_progres: Arc<PendingRegistrations>,
pub(crate) wireguard_gateway_data: WireguardGatewayData,
pub(crate) free_private_network_ips: Arc<PrivateIPs>,
pub(crate) timeout_check_interval: IntervalStream,
}
impl MixnetListener {
pub fn new(
config: Config,
private_ip_network: IpNetwork,
wireguard_gateway_data: WireguardGatewayData,
mixnet_client: nym_sdk::mixnet::MixnetClient,
task_handle: TaskHandle,
) -> Self {
let timeout_check_interval =
IntervalStream::new(tokio::time::interval(DEFAULT_REGISTRATION_TIMEOUT_CHECK));
MixnetListener {
config,
mixnet_client,
task_handle,
registration_in_progres: Default::default(),
wireguard_gateway_data,
free_private_network_ips: Arc::new(
private_ip_network.iter().map(|ip| (ip, None)).collect(),
),
timeout_check_interval,
}
}
fn remove_from_registry(
&self,
remote_public: &PeerPublicKey,
gateway_client: &GatewayClient,
) -> Result<()> {
self.wireguard_gateway_data
.remove_peer(gateway_client)
.map_err(|err| {
AuthenticatorError::InternalError(format!("could not remove peer: {:?}", err))
})?;
self.wireguard_gateway_data
.client_registry()
.remove(remote_public);
Ok(())
}
fn remove_stale_registrations(&self) -> Result<()> {
for reg in self.registration_in_progres.iter().map(|reg| reg.clone()) {
let mut ip = self
.free_private_network_ips
.get_mut(&reg.gateway_data.private_ip)
.ok_or(AuthenticatorError::InternalDataCorruption(format!(
"IP {} should be present",
reg.gateway_data.private_ip
)))?;
let timestamp = ip.ok_or(AuthenticatorError::InternalDataCorruption(format!(
"timestamp should be set for IP {}",
ip.key()
)))?;
let duration = SystemTime::now().duration_since(timestamp).map_err(|_| {
AuthenticatorError::InternalDataCorruption(
"set timestamp shouldn't have been set in the future".to_string(),
)
})?;
if duration > DEFAULT_REGISTRATION_TIMEOUT_CHECK {
*ip = None;
self.registration_in_progres
.remove(&reg.gateway_data.pub_key());
log::debug!(
"Removed stale registration of {}",
reg.gateway_data.pub_key()
);
}
}
Ok(())
}
fn on_initial_request(
&mut self,
init_message: InitMessage,
request_id: u64,
reply_to: Recipient,
) -> AuthenticatorHandleResult {
let remote_public = init_message.pub_key();
let nonce: u64 = fastrand::u64(..);
if let Some(registration_data) = self.registration_in_progres.get(&remote_public) {
return Ok(AuthenticatorResponse::new_pending_registration_success(
registration_data.value().clone(),
request_id,
reply_to,
));
}
let gateway_client_opt = if let Some(gateway_client) = self
.wireguard_gateway_data
.client_registry()
.get(&remote_public)
{
let mut private_ip_ref = self
.free_private_network_ips
.get_mut(&gateway_client.private_ip)
.ok_or(AuthenticatorError::InternalError(String::from(
"could not find private IP",
)))?;
*private_ip_ref = None;
Some(gateway_client.clone())
} else {
None
};
if let Some(gateway_client) = gateway_client_opt {
self.remove_from_registry(&remote_public, &gateway_client)?;
}
let mut private_ip_ref = self
.free_private_network_ips
.iter_mut()
.filter(|r| r.is_none())
.choose(&mut thread_rng())
.ok_or(AuthenticatorError::NoFreeIp)?;
// mark it as used, even though it's not final
*private_ip_ref = Some(SystemTime::now());
let gateway_data = GatewayClient::new(
self.wireguard_gateway_data.keypair().private_key(),
remote_public.inner(),
*private_ip_ref.key(),
nonce,
);
let registration_data = RegistrationData {
nonce,
gateway_data,
wg_port: self.config.authenticator.announced_port,
};
self.registration_in_progres
.insert(remote_public, registration_data.clone());
Ok(AuthenticatorResponse::new_pending_registration_success(
registration_data,
request_id,
reply_to,
))
}
fn on_final_request(
&mut self,
gateway_client: GatewayClient,
request_id: u64,
reply_to: Recipient,
) -> AuthenticatorHandleResult {
let registration_data = self
.registration_in_progres
.get(&gateway_client.pub_key())
.ok_or(AuthenticatorError::RegistrationNotInProgress)?
.value()
.clone();
if gateway_client
.verify(
self.wireguard_gateway_data.keypair().private_key(),
registration_data.nonce,
)
.is_ok()
{
self.wireguard_gateway_data
.add_peer(&gateway_client)
.map_err(|err| {
AuthenticatorError::InternalError(format!("could not add peer: {:?}", err))
})?;
self.registration_in_progres
.remove(&gateway_client.pub_key());
self.wireguard_gateway_data
.client_registry()
.insert(gateway_client.pub_key(), gateway_client);
Ok(AuthenticatorResponse::new_registered(reply_to, request_id))
} else {
Err(AuthenticatorError::MacVerificationFailure)
}
}
async fn on_reconstructed_message(
&mut self,
reconstructed: ReconstructedMessage,
) -> AuthenticatorHandleResult {
log::debug!(
"Received message with sender_tag: {:?}",
reconstructed.sender_tag
);
let request = match deserialize_request(&reconstructed) {
Err(AuthenticatorError::InvalidPacketVersion(version)) => {
return self.on_version_mismatch(version, &reconstructed);
}
req => req,
}?;
match request.data {
AuthenticatorRequestData::Initial(init_msg) => {
self.on_initial_request(init_msg, request.request_id, request.reply_to)
}
AuthenticatorRequestData::Final(client) => {
self.on_final_request(client, request.request_id, request.reply_to)
}
}
}
fn on_version_mismatch(
&self,
version: u8,
_reconstructed: &ReconstructedMessage,
) -> AuthenticatorHandleResult {
// If it's possible to parse, do so and return back a response, otherwise just drop
Err(AuthenticatorError::InvalidPacketVersion(version))
}
// When an incoming mixnet message triggers a response that we send back.
async fn handle_response(&self, response: AuthenticatorResponse) -> Result<()> {
let recipient = response.recipient();
let response_packet = response.to_bytes().map_err(|err| {
log::error!("Failed to serialize response packet");
AuthenticatorError::FailedToSerializeResponsePacket { source: err }
})?;
let input_message =
InputMessage::new_regular(recipient, response_packet, TransmissionLane::General, None);
self.mixnet_client
.send(input_message)
.await
.map_err(|err| AuthenticatorError::FailedToSendPacketToMixnet { source: err })
}
pub(crate) async fn run(mut self) -> Result<()> {
let mut task_client = self.task_handle.fork("main_loop");
while !task_client.is_shutdown() {
tokio::select! {
_ = task_client.recv() => {
log::debug!("Authenticator [main loop]: received shutdown");
},
_ = self.timeout_check_interval.next() => {
if let Err(e) = self.remove_stale_registrations() {
log::error!("Could not clear stale registrations. The registration process might get jammed soon - {:?}", e);
}
}
msg = self.mixnet_client.next() => {
if let Some(msg) = msg {
match self.on_reconstructed_message(msg).await {
Ok(response) => {
if let Err(err) = self.handle_response(response).await {
log::error!("Mixnet listener failed to handle response: {err}");
}
}
Err(err) => {
log::error!("Error handling reconstructed mixnet message: {err}");
}
};
} else {
log::trace!("Authenticator [main loop]: stopping since channel closed");
break;
};
},
}
}
log::debug!("Authenticator: stopping");
Ok(())
}
}
fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result<AuthenticatorRequest> {
let request_version = *reconstructed
.message
.first()
.ok_or(AuthenticatorError::EmptyPacket)?;
// Check version of the request and convert to the latest version if necessary
match request_version {
1 => v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed)
.map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }),
_ => {
log::info!("Received packet with invalid version: v{request_version}");
Err(AuthenticatorError::InvalidPacketVersion(request_version))
}
}
}