Moving clients crate from vpn-client repo to here (#6015)

* moving crates as is

* changes due to crate moving

* cargo fmt
This commit is contained in:
Simon Wicky
2025-09-08 10:50:18 +02:00
committed by GitHub
parent e95aca715c
commit f90fc4f2f0
18 changed files with 2455 additions and 12 deletions
Generated
+55 -12
View File
@@ -5010,6 +5010,25 @@ dependencies = [
"tokio",
]
[[package]]
name = "nym-authenticator-client"
version = "0.1.0"
dependencies = [
"bincode",
"futures",
"nym-authenticator-requests",
"nym-credentials-interface",
"nym-crypto",
"nym-sdk",
"nym-service-provider-requests-common",
"nym-wireguard-types",
"semver 1.0.26",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "nym-authenticator-requests"
version = "0.1.0"
@@ -5607,6 +5626,7 @@ dependencies = [
"aead",
"aes",
"aes-gcm-siv",
"base64 0.22.1",
"blake3",
"bs58",
"cipher",
@@ -6005,6 +6025,20 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "nym-ip-packet-client"
version = "0.1.0"
dependencies = [
"bytes",
"futures",
"nym-ip-packet-requests",
"nym-sdk",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "nym-ip-packet-requests"
version = "0.1.0"
@@ -6409,7 +6443,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "3.3.1"
version = "3.3.2"
dependencies = [
"ammonia",
"anyhow",
@@ -7375,6 +7409,26 @@ dependencies = [
"ts-rs",
]
[[package]]
name = "nym-wg-gateway-client"
version = "0.1.0"
dependencies = [
"nym-authenticator-client",
"nym-authenticator-requests",
"nym-bandwidth-controller",
"nym-credentials-interface",
"nym-crypto",
"nym-node-requests",
"nym-pemstore",
"nym-sdk",
"nym-statistics-common",
"nym-validator-client",
"rand 0.8.5",
"thiserror 2.0.12",
"tracing",
"url",
]
[[package]]
name = "nym-wireguard"
version = "0.1.0"
@@ -12002,17 +12056,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-result"
version = "0.3.4"
+3
View File
@@ -111,10 +111,12 @@ members = [
"gateway",
"nym-api",
"nym-api/nym-api-requests",
"nym-authenticator-client",
"nym-browser-extension/storage",
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-credential-proxy/vpn-api-lib-wasm",
"nym-ip-packet-client",
"nym-network-monitor",
"nym-node",
"nym-node-status-api/nym-node-status-agent",
@@ -125,6 +127,7 @@ members = [
"nym-outfox", "nym-signers-monitor",
"nym-statistics-api",
"nym-validator-rewarder",
"nym-wg-gateway-client",
"nyx-chain-watcher",
"sdk/ffi/cpp",
"sdk/ffi/go",
+1
View File
@@ -11,6 +11,7 @@ repository = { workspace = true }
aes-gcm-siv = { workspace = true, optional = true }
aes = { workspace = true, optional = true }
aead = { workspace = true, optional = true }
base64.workspace = true
bs58 = { workspace = true }
blake3 = { workspace = true, features = ["traits-preview"], optional = true }
ctr = { workspace = true, optional = true }
@@ -1,6 +1,7 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use base64::Engine;
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
@@ -158,6 +159,15 @@ impl PublicKey {
.map_err(|source| KeyRecoveryError::MalformedPublicKeyString { source })?;
Self::from_bytes(&bytes)
}
pub fn from_base64(s: &str) -> Option<Self> {
let bytes = base64::engine::general_purpose::STANDARD.decode(s).ok()?;
Self::from_bytes(&bytes).ok()
}
pub fn to_base64(&self) -> String {
base64::engine::general_purpose::STANDARD.encode(self.as_bytes())
}
}
impl FromStr for PublicKey {
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "nym-authenticator-client"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
bincode.workspace = true
futures.workspace = true
semver.workspace = true
thiserror.workspace = true
tokio-util.workspace = true
tokio.workspace = true
tracing.workspace = true
nym-authenticator-requests = { path = "../common/authenticator-requests" }
nym-credentials-interface = { path = "../common/credentials-interface" }
nym-crypto = { path = "../common/crypto" }
nym-sdk = { path = "../sdk/rust/nym-sdk" }
nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" }
nym-wireguard-types = { path = "../common/wireguard-types" }
+42
View File
@@ -0,0 +1,42 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("mixnet client stopped returning responses")]
NoMixnetMessagesReceived,
#[error("failed to get version from message")]
NoVersionInMessage,
#[error(
"received response with version v{received}, the client is too new and can only understand v{expected}"
)]
ReceivedResponseWithOldVersion { expected: u8, received: u8 },
#[error(
"received response with version v{received}, the client is too old and can only understand v{expected}"
)]
ReceivedResponseWithNewVersion { expected: u8, received: u8 },
#[error("failed to send mixnet message")]
SendMixnetMessage(#[source] Box<nym_sdk::Error>),
#[error("timeout waiting for connect response from exit gateway (authenticator)")]
TimeoutWaitingForConnectResponse,
#[error("unable to get mixnet handle when sending authenticator message")]
UnableToGetMixnetHandle,
#[error("unknown version number")]
UnknownVersion,
#[error(transparent)]
Bincode(#[from] bincode::Error),
#[error("gateway doesn't support this type of message")]
UnsupportedMessage,
#[error(transparent)]
AuthenticatorRequests(#[from] nym_authenticator_requests::Error),
}
// Result type based on our error type
pub type Result<T> = std::result::Result<T, Error>;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,113 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
// To remove with the Registration Client PR
#![allow(clippy::unwrap_used)]
use std::sync::Arc;
use futures::StreamExt;
use nym_sdk::mixnet::{MixnetClient, ReconstructedMessage};
use tokio::{sync::broadcast, task::JoinHandle};
use tokio_util::sync::CancellationToken;
use crate::AuthenticatorMixnetClient;
pub type SharedMixnetClient = Arc<tokio::sync::Mutex<Option<MixnetClient>>>;
pub type MixnetMessageBroadcastSender = broadcast::Sender<Arc<ReconstructedMessage>>;
pub type MixnetMessageBroadcastReceiver = broadcast::Receiver<Arc<ReconstructedMessage>>;
// The AuthClientsMixnetListener listens to mixnet messages and rebroadcasts them to the
// AuthClients, or whoever else is interested.
// While it is running, it has a lock on the shared mixnet client. This is the reason it's
// designed to be able to start and stop, so that the lock can be released when it's not needed.
//
// NOTE: this is potentially bit wasteful. Ideally we should have proper channels where the
// recipient only gets messages they're interested in.
pub struct AuthClientMixnetListener {
// The shared mixnet client that we're listening to
mixnet_client: SharedMixnetClient,
// Broadcast channel for the messages that we re-broadcast to the AuthClients
message_broadcast: MixnetMessageBroadcastSender,
// Listen to cancel from the outside world
shutdown_token: CancellationToken,
}
impl AuthClientMixnetListener {
pub fn new(mixnet_client: SharedMixnetClient, shutdown_token: CancellationToken) -> Self {
let (message_broadcast, _) = broadcast::channel(100);
Self {
mixnet_client,
message_broadcast,
shutdown_token,
}
}
pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver {
self.message_broadcast.subscribe()
}
async fn run(self) {
let mut mixnet_client = self.mixnet_client.lock().await.take().unwrap();
self.shutdown_token
.run_until_cancelled(async {
while let Some(event) = mixnet_client.next().await {
if let Err(err) = self.message_broadcast.send(Arc::new(event)) {
tracing::error!("Failed to broadcast mixnet message: {err}");
}
}
tracing::error!("Mixnet client stream ended unexpectedly");
})
.await;
self.mixnet_client.lock().await.replace(mixnet_client);
}
pub fn start(self) -> AuthClientMixnetListenerHandle {
let mixnet_client = self.mixnet_client.clone();
let message_broadcast = self.message_broadcast.clone();
let handle = tokio::spawn(self.run());
AuthClientMixnetListenerHandle {
mixnet_client,
message_broadcast,
handle,
}
}
}
pub struct AuthClientMixnetListenerHandle {
mixnet_client: SharedMixnetClient,
message_broadcast: MixnetMessageBroadcastSender,
handle: JoinHandle<()>,
}
impl AuthClientMixnetListenerHandle {
/// Returns new `AuthClient` or `None` if `MixnetClient` is already moved from shared reference.
pub async fn new_auth_client(&self) -> Option<AuthenticatorMixnetClient> {
let mixnet_client_guard = self.mixnet_client.lock().await;
let mixnet_client_ref = mixnet_client_guard.as_ref()?;
let mixnet_sender = mixnet_client_ref.split_sender();
let nym_address = *mixnet_client_ref.nym_address();
Some(
AuthenticatorMixnetClient::new(
mixnet_sender,
self.message_broadcast.subscribe(),
nym_address,
)
.await,
)
}
pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver {
self.message_broadcast.subscribe()
}
pub async fn wait(self) {
if let Err(err) = self.handle.await {
tracing::error!("Error waiting for auth clients mixnet listener to stop: {err}");
}
}
}
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "nym-ip-packet-client"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
bytes.workspace = true
futures.workspace = true
thiserror.workspace = true
tokio-util.workspace = true
tokio.workspace = true
tracing.workspace = true
nym-sdk = { path = "../sdk/rust/nym-sdk" }
nym-ip-packet-requests = { path = "../common/ip-packet-requests" }
+199
View File
@@ -0,0 +1,199 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
// To remove with the Registration Client PR
#![allow(clippy::unwrap_used)]
use std::{sync::Arc, time::Duration};
use nym_ip_packet_requests::IpPair;
use nym_sdk::mixnet::{
InputMessage, MixnetClient, MixnetClientSender, MixnetMessageSender, Recipient,
TransmissionLane,
};
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error};
use crate::{
current::{
request::IpPacketRequest,
response::{
ConnectResponse, ConnectResponseReply, ControlResponse, IpPacketResponse,
IpPacketResponseData,
},
},
error::{Error, Result},
helpers::check_ipr_message_version,
};
pub type SharedMixnetClient = Arc<tokio::sync::Mutex<Option<MixnetClient>>>;
const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone, Debug, PartialEq, Eq)]
enum ConnectionState {
Disconnected,
Connecting,
Connected,
#[allow(unused)]
Disconnecting,
}
pub struct IprClientConnect {
// During connection we need the mixnet client, but once connected we expect to setup a channel
// from the main mixnet listener at the top-level.
// As such, we drop the shared mixnet client once we're connected.
mixnet_client: SharedMixnetClient,
mixnet_sender: MixnetClientSender,
connected: ConnectionState,
cancel_token: CancellationToken,
}
impl IprClientConnect {
pub async fn new(mixnet_client: SharedMixnetClient, cancel_token: CancellationToken) -> Self {
let mixnet_sender = mixnet_client.lock().await.as_ref().unwrap().split_sender();
Self {
mixnet_client,
mixnet_sender,
connected: ConnectionState::Disconnected,
cancel_token,
}
}
pub async fn connect(&mut self, ip_packet_router_address: Recipient) -> Result<IpPair> {
if self.connected != ConnectionState::Disconnected {
return Err(Error::AlreadyConnected);
}
tracing::info!("Connecting to exit gateway");
self.connected = ConnectionState::Connecting;
match self.connect_inner(ip_packet_router_address).await {
Ok(ips) => {
debug!("Successfully connected to the ip-packet-router");
self.connected = ConnectionState::Connected;
Ok(ips)
}
Err(err) => {
error!("Failed to connect to the ip-packet-router: {:?}", err);
self.connected = ConnectionState::Disconnected;
Err(err)
}
}
}
async fn connect_inner(&mut self, ip_packet_router_address: Recipient) -> Result<IpPair> {
let request_id = self.send_connect_request(ip_packet_router_address).await?;
debug!("Waiting for reply...");
self.listen_for_connect_response(request_id).await
}
async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result<u64> {
let (request, request_id) = IpPacketRequest::new_connect_request(None);
// We use 20 surbs for the connect request because typically the IPR is configured to have
// a min threshold of 10 surbs that it reserves for itself to request additional surbs.
let surbs = 20;
self.mixnet_sender
.send(create_input_message(
ip_packet_router_address,
request,
surbs,
))
.await
.map_err(|err| Error::SdkError(Box::new(err)))?;
Ok(request_id)
}
async fn handle_connect_response(&self, response: ConnectResponse) -> Result<IpPair> {
debug!("Handling dynamic connect response");
match response.reply {
ConnectResponseReply::Success(r) => Ok(r.ips),
ConnectResponseReply::Failure(reason) => Err(Error::ConnectRequestDenied { reason }),
}
}
async fn handle_ip_packet_router_response(&self, response: IpPacketResponse) -> Result<IpPair> {
let control_response = match response.data {
IpPacketResponseData::Control(control_response) => control_response,
_ => {
error!("Received non-control response while waiting for connect response");
return Err(Error::UnexpectedConnectResponse);
}
};
match *control_response {
ControlResponse::Connect(resp) => self.handle_connect_response(resp).await,
response => {
error!("Unexpected response: {response:?}");
Err(Error::UnexpectedConnectResponse)
}
}
}
async fn listen_for_connect_response(&self, request_id: u64) -> Result<IpPair> {
// Connecting is basically synchronous from the perspective of the mixnet client, so it's safe
// to just grab ahold of the mutex and keep it until we get the response.
let mut mixnet_client_handle = self.mixnet_client.lock().await;
let mixnet_client = mixnet_client_handle.as_mut().unwrap();
let timeout = sleep(IPR_CONNECT_TIMEOUT);
tokio::pin!(timeout);
loop {
tokio::select! {
_ = self.cancel_token.cancelled() => {
error!("Cancelled while waiting for reply to connect request");
return Err(Error::Cancelled);
},
_ = &mut timeout => {
error!("Timed out waiting for reply to connect request");
return Err(Error::TimeoutWaitingForConnectResponse);
},
msgs = mixnet_client.wait_for_messages() => match msgs {
None => {
return Err(Error::NoMixnetMessagesReceived);
}
Some(msgs) => {
for msg in msgs {
// Confirm that the version is correct
if let Err(err) = check_ipr_message_version(&msg) {
tracing::info!("Mixnet message version mismatch: {err}");
continue;
}
// Then we deserialize the message
tracing::debug!("IprClient: got message while waiting for connect response");
let Ok(response) = IpPacketResponse::from_reconstructed_message(&msg) else {
// This is ok, it's likely just one of our self-pings
tracing::debug!("Failed to deserialize mixnet message");
continue;
};
if response.id() == Some(request_id) {
tracing::debug!("Got response with matching id");
return self.handle_ip_packet_router_response(response).await;
}
}
}
}
}
}
}
}
fn create_input_message(
recipient: Recipient,
request: IpPacketRequest,
surbs: u32,
) -> InputMessage {
InputMessage::new_anonymous(
recipient,
request.to_bytes().unwrap(),
surbs,
TransmissionLane::General,
None,
)
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::current::response::ConnectFailureReason;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("nym sdk")]
SdkError(#[source] Box<nym_sdk::Error>),
#[error(
"received response with version v{received}, the client is too new and can only understand v{expected}"
)]
ReceivedResponseWithOldVersion { expected: u8, received: u8 },
#[error(
"received response with version v{received}, the client is too old and can only understand v{expected}"
)]
ReceivedResponseWithNewVersion { expected: u8, received: u8 },
#[error("got reply for connect request, but it appears intended for the wrong address?")]
GotReplyIntendedForWrongAddress,
#[error("unexpected connect response")]
UnexpectedConnectResponse,
#[error("mixnet client stopped returning responses")]
NoMixnetMessagesReceived,
#[error("timeout waiting for connect response from exit gateway (ipr)")]
TimeoutWaitingForConnectResponse,
#[error("connection cancelled")]
Cancelled,
#[error("connect request denied: {reason}")]
ConnectRequestDenied { reason: ConnectFailureReason },
#[error("failed to get version from message")]
NoVersionInMessage,
#[error("already connected to the mixnet")]
AlreadyConnected,
#[error("failed to create connect request")]
FailedToCreateConnectRequest {
source: nym_ip_packet_requests::sign::SignatureError,
},
}
// Result type based on our error type
pub type Result<T> = std::result::Result<T, Error>;
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::cmp::Ordering;
use nym_sdk::mixnet::ReconstructedMessage;
use crate::{current::VERSION as CURRENT_VERSION, error::Result, Error};
pub(crate) fn check_ipr_message_version(message: &ReconstructedMessage) -> Result<()> {
// Assuming it's a IPR message, it will have a version as its first byte
if let Some(version) = message.message.first() {
match version.cmp(&CURRENT_VERSION) {
Ordering::Greater => Err(Error::ReceivedResponseWithNewVersion {
expected: CURRENT_VERSION,
received: *version,
}),
Ordering::Less => Err(Error::ReceivedResponseWithOldVersion {
expected: CURRENT_VERSION,
received: *version,
}),
Ordering::Equal => {
// We're good
Ok(())
}
}
} else {
Err(Error::NoVersionInMessage)
}
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
mod connect;
mod error;
mod helpers;
mod listener;
pub use connect::IprClientConnect;
pub use error::Error;
pub use listener::{IprListener, MixnetMessageOutcome};
// Re-export the currently used version
pub use nym_ip_packet_requests::v8 as current;
+121
View File
@@ -0,0 +1,121 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use bytes::Bytes;
use futures::StreamExt;
use nym_ip_packet_requests::{codec::MultiIpPacketCodec, v8::response::ControlResponse};
use nym_sdk::mixnet::ReconstructedMessage;
use tokio_util::codec::FramedRead;
use tracing::{debug, error, info, warn};
use crate::{
current::{
request::{ControlRequest, IpPacketRequest, IpPacketRequestData},
response::{InfoLevel, IpPacketResponse, IpPacketResponseData},
},
helpers::check_ipr_message_version,
};
pub enum MixnetMessageOutcome {
IpPackets(Vec<Bytes>),
MixnetSelfPing,
Disconnect,
}
pub struct IprListener {}
#[derive(Debug, thiserror::Error)]
pub enum IprListenerError {
#[error(transparent)]
IprClientError(#[from] crate::Error),
}
impl IprListener {
pub fn new() -> Self {
Self {}
}
fn is_mix_ping(&self, request: &IpPacketRequest) -> bool {
match request.data {
IpPacketRequestData::Control(ref control) => {
matches!(**control, ControlRequest::Ping(_))
}
_ => {
debug!("Received unexpected request: {request:?}");
false
}
}
}
pub async fn handle_reconstructed_message(
&mut self,
message: ReconstructedMessage,
) -> Result<Option<MixnetMessageOutcome>, IprListenerError> {
check_ipr_message_version(&message)?;
match IpPacketResponse::from_reconstructed_message(&message) {
Ok(response) => {
match response.data {
IpPacketResponseData::Data(data_response) => {
// Un-bundle the mixnet message and send the individual IP packets
// to the tun device
let framed_reader = FramedRead::new(
data_response.ip_packet.as_ref(),
MultiIpPacketCodec::new(),
);
let responses: Vec<Bytes> = framed_reader
.filter_map(|res| async { res.ok().map(|packet| packet.into_bytes()) })
.collect()
.await;
return Ok(Some(MixnetMessageOutcome::IpPackets(responses)));
}
IpPacketResponseData::Control(control_response) => match *control_response {
ControlResponse::Connect(_) => {
info!("Received connect response when already connected - ignoring");
}
ControlResponse::Disconnect(_) => {
info!("Received disconnect response");
return Ok(Some(MixnetMessageOutcome::Disconnect));
}
ControlResponse::UnrequestedDisconnect(_) => {
info!("Received unrequested disconnect response, ignoring for now");
}
ControlResponse::Pong(_) => {
info!("Received pong response, ignoring for now");
}
ControlResponse::Health(_) => {
info!("Received health response, ignoring for now");
}
ControlResponse::Info(info) => {
let msg =
format!("Received info response from the mixnet: {}", info.reply);
match info.level {
InfoLevel::Info => info!("{msg}"),
InfoLevel::Warn => warn!("{msg}"),
InfoLevel::Error => error!("{msg}"),
}
}
},
}
}
Err(err) => {
// The exception to when we are not expecting a response, is when we
// are sending a ping to ourselves.
if let Ok(request) = IpPacketRequest::from_reconstructed_message(&message) {
if self.is_mix_ping(&request) {
return Ok(Some(MixnetMessageOutcome::MixnetSelfPing));
}
} else {
warn!("Failed to deserialize reconstructed message: {err}");
}
}
}
Ok(None)
}
}
impl Default for IprListener {
fn default() -> Self {
Self::new()
}
}
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "nym-wg-gateway-client"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
nym-authenticator-client = { path = "../nym-authenticator-client" }
nym-authenticator-requests = { path = "../common/authenticator-requests" }
nym-bandwidth-controller = { path = "../common/bandwidth-controller" }
nym-credentials-interface = { path = "../common/credentials-interface" }
nym-crypto = { path = "../common/crypto" }
nym-node-requests = { path = "../nym-node/nym-node-requests" }
nym-pemstore = { path = "../common/pemstore" }
nym-sdk = { path = "../sdk/rust/nym-sdk" }
nym-statistics-common = { path = "../common/statistics" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
rand.workspace = true
thiserror.workspace = true
tracing.workspace = true
url.workspace = true
+154
View File
@@ -0,0 +1,154 @@
use std::time::Duration;
use nym_authenticator_client::{
AuthenticatorClient, AuthenticatorResponse, AuthenticatorVersion, ClientMessage,
QueryMessageImpl,
};
use nym_authenticator_requests::{v3, v4, v5};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::encryption;
use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::PeerPublicKey;
use nym_sdk::mixnet::Recipient;
use crate::error::{Error, Result};
const RETRY_PERIOD: Duration = Duration::from_secs(30);
impl crate::WgGatewayClient {
pub fn light_client(&self) -> WgGatewayLightClient {
WgGatewayLightClient {
public_key: *self.keypair.public_key(),
auth_client: self.auth_client.clone(),
}
}
}
#[derive(Clone)]
pub struct WgGatewayLightClient {
public_key: encryption::PublicKey,
auth_client: AuthenticatorClient,
}
impl WgGatewayLightClient {
pub fn auth_recipient(&self) -> Recipient {
self.auth_client.auth_recipient()
}
pub fn auth_client(&self) -> &AuthenticatorClient {
&self.auth_client
}
pub fn set_auth_client(&mut self, auth_client: AuthenticatorClient) {
self.auth_client = auth_client;
}
pub async fn query_bandwidth(&mut self) -> Result<Option<i64>> {
let query_message = match self.auth_client.auth_version() {
AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
version: AuthenticatorVersion::V2,
})),
AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
version: AuthenticatorVersion::V3,
})),
AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
version: AuthenticatorVersion::V4,
})),
AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
version: AuthenticatorVersion::V5,
})),
AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion),
};
let response = self.auth_client.send(&query_message).await?;
let available_bandwidth = match response {
nym_authenticator_client::AuthenticatorResponse::RemainingBandwidth(
remaining_bandwidth_response,
) => {
if let Some(available_bandwidth) =
remaining_bandwidth_response.available_bandwidth()
{
available_bandwidth
} else {
return Ok(None);
}
}
_ => return Err(Error::InvalidGatewayAuthResponse),
};
let remaining_pretty = if available_bandwidth > 1024 * 1024 {
format!("{:.2} MB", available_bandwidth as f64 / 1024.0 / 1024.0)
} else {
format!("{} KB", available_bandwidth / 1024)
};
tracing::debug!(
"Remaining wireguard bandwidth with gateway {} for today: {}",
self.auth_client.auth_recipient().gateway(),
remaining_pretty
);
if available_bandwidth < 1024 * 1024 {
tracing::warn!(
"Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon"
);
}
Ok(Some(available_bandwidth))
}
async fn send(&mut self, msg: ClientMessage) -> Result<AuthenticatorResponse> {
let now = std::time::Instant::now();
while now.elapsed() < RETRY_PERIOD {
match self.auth_client.send(&msg).await {
Ok(response) => return Ok(response),
Err(nym_authenticator_client::Error::TimeoutWaitingForConnectResponse) => continue,
Err(source) => {
if msg.is_wasteful() {
return Err(Error::NoRetry { source });
} else {
return Err(Error::AuthenticatorClientError(source));
}
}
}
}
if msg.is_wasteful() {
Err(Error::NoRetry {
source: nym_authenticator_client::Error::TimeoutWaitingForConnectResponse,
})
} else {
Err(Error::AuthenticatorClientError(
nym_authenticator_client::Error::TimeoutWaitingForConnectResponse,
))
}
}
pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result<i64> {
let top_up_message = match self.auth_client.auth_version() {
AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
credential,
})),
// NOTE: looks like a bug here using v3. But we're leaving it as is since it's working
// and V4 is deprecated in favour of V5
AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
credential,
})),
AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
credential,
})),
AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => {
return Err(Error::UnsupportedAuthenticatorVersion);
}
};
let response = self.send(top_up_message).await?;
let remaining_bandwidth = match response {
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.available_bandwidth()
}
_ => return Err(Error::InvalidGatewayAuthResponse),
};
Ok(remaining_bandwidth)
}
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_credentials_interface::TicketType;
use nym_sdk::mixnet::NodeIdentity;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("received invalid response from gateway authenticator")]
InvalidGatewayAuthResponse,
#[error("unknown authenticator version number")]
UnsupportedAuthenticatorVersion,
#[error(transparent)]
AuthenticatorClientError(#[from] nym_authenticator_client::Error),
#[error("error that should stop auto retrying")]
NoRetry {
#[source]
source: nym_authenticator_client::Error,
},
#[error("verification failure")]
VerificationFailed(#[source] nym_authenticator_requests::Error),
#[error("failed to parse entry gateway socket addr")]
FailedToParseEntryGatewaySocketAddr(#[source] std::net::AddrParseError),
#[error("failed to get {ticketbook_type} ticket")]
GetTicket {
ticketbook_type: TicketType,
#[source]
source: nym_bandwidth_controller::error::BandwidthControllerError,
},
}
#[derive(Debug, thiserror::Error)]
pub enum ErrorMessage {
#[error("out of bandwidth for gateway: {gateway_id}")]
OutOfBandwidth { gateway_id: Box<NodeIdentity> },
#[error("gateway {gateway_id} is erroring out")]
ErrorsFromGateway { gateway_id: Box<NodeIdentity> },
}
// Result type based on our error type
pub type Result<T> = std::result::Result<T, Error>;
+302
View File
@@ -0,0 +1,302 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub mod deprecated;
mod error;
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
str::FromStr,
};
pub use error::{Error, ErrorMessage};
use nym_authenticator_client::{
AuthenticatorClient, AuthenticatorMixnetClient, AuthenticatorResponse, AuthenticatorVersion,
ClientMessage,
};
use nym_authenticator_requests::{v2, v3, v4, v5};
use nym_bandwidth_controller::PreparedCredential;
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::{encryption, x25519::KeyPair, x25519::PublicKey};
use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::PeerPublicKey;
use nym_pemstore::KeyPairPath;
use nym_sdk::mixnet::{CredentialStorage, NodeIdentity, Recipient};
use nym_validator_client::QueryHttpRpcNyxdClient;
use rand::{rngs::OsRng, CryptoRng, RngCore};
use tracing::{debug, error, trace};
use crate::error::Result;
pub const DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_private_entry_wireguard.pem";
pub const DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_public_entry_wireguard.pem";
pub const DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_private_exit_wireguard.pem";
pub const DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_public_exit_wireguard.pem";
pub const TICKETS_TO_SPEND: u32 = 1;
#[derive(Clone, Debug)]
pub struct GatewayData {
pub public_key: PublicKey,
pub endpoint: SocketAddr,
pub private_ipv4: Ipv4Addr,
pub private_ipv6: Ipv6Addr,
}
pub struct WgGatewayClient {
keypair: encryption::KeyPair,
auth_client: AuthenticatorClient,
}
impl WgGatewayClient {
fn new_type(
data_path: &Option<PathBuf>,
auth_mix_client: AuthenticatorMixnetClient,
auth_recipient: Recipient,
auth_version: AuthenticatorVersion,
private_file_name: &str,
public_file_name: &str,
) -> Self {
let mut rng = OsRng;
let auth_client = AuthenticatorClient::new(auth_mix_client, auth_recipient, auth_version);
if let Some(data_path) = data_path {
let paths = KeyPairPath::new(
data_path.join(private_file_name),
data_path.join(public_file_name),
);
let keypair = load_or_generate_keypair(&mut rng, paths);
WgGatewayClient {
keypair,
auth_client,
}
} else {
WgGatewayClient {
keypair: KeyPair::new(&mut rng),
auth_client,
}
}
}
pub fn new_entry(
data_path: &Option<PathBuf>,
auth_mix_client: AuthenticatorMixnetClient,
auth_recipient: Recipient,
auth_version: AuthenticatorVersion,
) -> Self {
Self::new_type(
data_path,
auth_mix_client,
auth_recipient,
auth_version,
DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME,
DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME,
)
}
pub fn new_exit(
data_path: &Option<PathBuf>,
auth_mix_client: AuthenticatorMixnetClient,
auth_recipient: Recipient,
auth_version: AuthenticatorVersion,
) -> Self {
Self::new_type(
data_path,
auth_mix_client,
auth_recipient,
auth_version,
DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME,
DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME,
)
}
pub fn keypair(&self) -> &encryption::KeyPair {
&self.keypair
}
pub fn auth_recipient(&self) -> Recipient {
self.auth_client.auth_recipient()
}
pub fn auth_version(&self) -> AuthenticatorVersion {
self.auth_client.auth_version()
}
pub async fn request_bandwidth<St: CredentialStorage>(
gateway_id: NodeIdentity,
controller: &nym_bandwidth_controller::BandwidthController<QueryHttpRpcNyxdClient, St>,
ticketbook_type: TicketType,
) -> Result<PreparedCredential>
where
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
let credential = controller
.prepare_ecash_ticket(ticketbook_type, gateway_id.to_bytes(), TICKETS_TO_SPEND)
.await
.map_err(|source| Error::GetTicket {
ticketbook_type,
source,
})?;
Ok(credential)
}
pub async fn register_wireguard<St: CredentialStorage>(
&mut self,
gateway_host: IpAddr,
controller: &nym_bandwidth_controller::BandwidthController<QueryHttpRpcNyxdClient, St>,
ticketbook_type: TicketType,
) -> Result<GatewayData>
where
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
debug!("Registering with the wg gateway...");
let init_message = match self.auth_version() {
AuthenticatorVersion::V2 => {
ClientMessage::Initial(Box::new(v2::registration::InitMessage {
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
}))
}
AuthenticatorVersion::V3 => {
ClientMessage::Initial(Box::new(v3::registration::InitMessage {
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
}))
}
AuthenticatorVersion::V4 => {
ClientMessage::Initial(Box::new(v4::registration::InitMessage {
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
}))
}
AuthenticatorVersion::V5 => {
ClientMessage::Initial(Box::new(v5::registration::InitMessage {
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
}))
}
AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion),
};
trace!("sending init msg to {}: {:?}", &gateway_host, &init_message);
let response = self.auth_client.send(&init_message).await?;
let registered_data = match response {
AuthenticatorResponse::PendingRegistration(pending_registration_response) => {
// Unwrap since we have already checked that we have the keypair.
debug!("Verifying data");
if let Err(e) = pending_registration_response.verify(self.keypair.private_key()) {
return Err(Error::VerificationFailed(e));
}
trace!(
"received \"pending-registration\" msg from {}: {:?}",
&gateway_host,
&pending_registration_response
);
let credential = Some(
Self::request_bandwidth(
self.auth_recipient().gateway(),
controller,
ticketbook_type,
)
.await?
.data,
);
let finalized_message = match self.auth_version() {
AuthenticatorVersion::V2 => {
ClientMessage::Final(Box::new(v2::registration::FinalMessage {
gateway_client: v2::registration::GatewayClient::new(
self.keypair.private_key(),
pending_registration_response.pub_key().inner(),
pending_registration_response.private_ips().ipv4.into(),
pending_registration_response.nonce(),
),
credential,
}))
}
AuthenticatorVersion::V3 => {
ClientMessage::Final(Box::new(v3::registration::FinalMessage {
gateway_client: v3::registration::GatewayClient::new(
self.keypair.private_key(),
pending_registration_response.pub_key().inner(),
pending_registration_response.private_ips().ipv4.into(),
pending_registration_response.nonce(),
),
credential,
}))
}
AuthenticatorVersion::V4 => {
ClientMessage::Final(Box::new(v4::registration::FinalMessage {
gateway_client: v4::registration::GatewayClient::new(
self.keypair.private_key(),
pending_registration_response.pub_key().inner(),
pending_registration_response.private_ips().into(),
pending_registration_response.nonce(),
),
credential,
}))
}
AuthenticatorVersion::V5 => {
ClientMessage::Final(Box::new(v5::registration::FinalMessage {
gateway_client: v5::registration::GatewayClient::new(
self.keypair.private_key(),
pending_registration_response.pub_key().inner(),
pending_registration_response.private_ips(),
pending_registration_response.nonce(),
),
credential,
}))
}
AuthenticatorVersion::UNKNOWN => {
return Err(Error::UnsupportedAuthenticatorVersion);
}
};
trace!(
"sending final msg to {}: {:?}",
&gateway_host,
&finalized_message
);
let response = self.auth_client.send(&finalized_message).await?;
let AuthenticatorResponse::Registered(registered_response) = response else {
return Err(Error::InvalidGatewayAuthResponse);
};
registered_response
}
AuthenticatorResponse::Registered(registered_response) => registered_response,
_ => return Err(Error::InvalidGatewayAuthResponse),
};
trace!(
"received \"registered\" msg from {}: {:?}",
&gateway_host,
&registered_data
);
let gateway_data = GatewayData {
public_key: registered_data.pub_key().inner().into(),
endpoint: SocketAddr::from_str(&format!(
"{}:{}",
gateway_host,
registered_data.wg_port()
))
.map_err(Error::FailedToParseEntryGatewaySocketAddr)?,
private_ipv4: registered_data.private_ips().ipv4,
private_ipv6: registered_data.private_ips().ipv6,
};
Ok(gateway_data)
}
}
fn load_or_generate_keypair<R: RngCore + CryptoRng>(rng: &mut R, paths: KeyPairPath) -> KeyPair {
match nym_pemstore::load_keypair(&paths) {
Ok(keypair) => keypair,
Err(_) => {
let keypair = KeyPair::new(rng);
if let Err(e) = nym_pemstore::store_keypair(&keypair, &paths) {
error!(
"could not store generated keypair at {:?} - {:?}; will use ephemeral keys",
paths, e
);
}
keypair
}
}
}