diff --git a/Cargo.lock b/Cargo.lock index 745b3b68cb9..4fa18755175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4282,6 +4282,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "ipnetwork" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8eca9f51da27bc908ef3dd85c21e1bbba794edaf94d7841e37356275b82d31e" +dependencies = [ + "serde", +] + [[package]] name = "ipnetwork" version = "0.18.0" @@ -6557,6 +6566,7 @@ dependencies = [ "futures", "humantime-serde", "hyper", + "ipnetwork 0.16.0", "lazy_static", "log", "nym-api-requests", @@ -6926,6 +6936,7 @@ dependencies = [ "fastrand 2.0.1", "hmac 0.12.1", "hyper", + "ipnetwork 0.16.0", "mime", "nym-config", "nym-crypto", diff --git a/common/wireguard-types/src/registration.rs b/common/wireguard-types/src/registration.rs index 1da614aaa0c..0fa9536161a 100644 --- a/common/wireguard-types/src/registration.rs +++ b/common/wireguard-types/src/registration.rs @@ -6,6 +6,7 @@ use crate::PeerPublicKey; use base64::{engine::general_purpose, Engine}; use dashmap::DashMap; use serde::{Deserialize, Serialize}; +use std::net::IpAddr; use std::{fmt, ops::Deref, str::FromStr}; #[cfg(feature = "verify")] @@ -17,11 +18,13 @@ use sha2::Sha256; pub type GatewayClientRegistry = DashMap; pub type PendingRegistrations = DashMap; +pub type PrivateIPs = DashMap; #[cfg(feature = "verify")] pub type HmacSha256 = Hmac; pub type Nonce = u64; +pub type Free = bool; #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type", rename_all = "camelCase")] @@ -72,6 +75,9 @@ pub struct GatewayClient { #[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))] pub pub_key: PeerPublicKey, + /// Assigned private IP + pub private_ip: IpAddr, + /// Sha256 hmac on the data (alongside the prior nonce) #[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))] pub mac: ClientMac, @@ -79,7 +85,12 @@ pub struct GatewayClient { impl GatewayClient { #[cfg(feature = "verify")] - pub fn new(local_secret: &PrivateKey, remote_public: PublicKey, nonce: u64) -> Self { + pub fn new( + local_secret: &PrivateKey, + remote_public: PublicKey, + private_ip: IpAddr, + nonce: u64, + ) -> Self { // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek #[allow(clippy::expect_used)] let static_secret = boringtun::x25519::StaticSecret::try_from(local_secret.to_bytes()) @@ -96,10 +107,12 @@ impl GatewayClient { .expect("x25519 shared secret is always 32 bytes long"); mac.update(local_public.as_bytes()); + mac.update(private_ip.to_string().as_bytes()); mac.update(&nonce.to_le_bytes()); GatewayClient { pub_key: PeerPublicKey::new(local_public), + private_ip, mac: ClientMac(mac.finalize().into_bytes().to_vec()), } } @@ -121,6 +134,7 @@ impl GatewayClient { .expect("x25519 shared secret is always 32 bytes long"); mac.update(self.pub_key.as_bytes()); + mac.update(self.private_ip.to_string().as_bytes()); mac.update(&nonce.to_le_bytes()); mac.verify_slice(&self.mac) @@ -209,6 +223,7 @@ mod tests { let client = GatewayClient::new( client_key_pair.private_key(), *gateway_key_pair.public_key(), + "10.0.0.42".parse().unwrap(), nonce, ); assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index cec0929f6d0..3ee6a136436 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -27,6 +27,7 @@ dirs = "4.0" dotenvy = { workspace = true } futures = { workspace = true } humantime-serde = "1.0.1" +ipnetwork = "0.16" lazy_static = "1.4.0" log = { workspace = true } once_cell = "1.7.2" diff --git a/gateway/src/config/old_config_v1_1_31.rs b/gateway/src/config/old_config_v1_1_31.rs index 6391b181158..04d76e62902 100644 --- a/gateway/src/config/old_config_v1_1_31.rs +++ b/gateway/src/config/old_config_v1_1_31.rs @@ -128,6 +128,7 @@ impl From for Config { enabled: value.wireguard.enabled, bind_address: value.wireguard.bind_address, announced_port: value.wireguard.announced_port, + private_network_prefix: Default::default(), storage_paths: nym_node::config::persistence::WireguardPaths { // no fields (yet) }, diff --git a/gateway/src/error.rs b/gateway/src/error.rs index 470e5421c42..4ae56f813c3 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -134,6 +134,12 @@ pub(crate) enum GatewayError { // TODO: in the future this should work the other way, i.e. NymNode depending on Gateway errors #[error(transparent)] NymNodeError(#[from] nym_node::error::NymNodeError), + + #[error("there was an issue with wireguard IP network: {source}")] + IpNetworkError { + #[from] + source: ipnetwork::IpNetworkError, + }, } impl From for GatewayError { diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs index d27b7873c68..401d1ccbf2b 100644 --- a/gateway/src/http/mod.rs +++ b/gateway/src/http/mod.rs @@ -4,6 +4,7 @@ use crate::config::Config; use crate::error::GatewayError; use crate::node::helpers::load_public_key; +use ipnetwork::IpNetwork; use log::warn; use nym_bin_common::bin_info_owned; use nym_crypto::asymmetric::{encryption, identity}; @@ -16,6 +17,7 @@ use nym_node::http::router::WireguardAppState; use nym_node::wireguard::types::GatewayClientRegistry; use nym_sphinx::addressing::clients::Recipient; use nym_task::TaskClient; +use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; fn load_gateway_details( @@ -223,12 +225,17 @@ impl<'a> HttpApiBuilder<'a> { } } + let wireguard_private_network = IpNetwork::new( + IpAddr::from(Ipv4Addr::new(10, 1, 0, 0)), + self.gateway_config.wireguard.private_network_prefix, + )?; let wg_state = self.client_registry.map(|client_registry| { WireguardAppState::new( self.sphinx_keypair, client_registry, Default::default(), self.gateway_config.wireguard.bind_address.port(), + wireguard_private_network, ) }); diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 7b70ae86e0d..ded34c9d48c 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -14,6 +14,8 @@ license.workspace = true anyhow = { workspace = true } bytes = "1.5.0" colored = "2" +ipnetwork = "0.16" +rand = "0.7.3" serde = { workspace = true, features = ["derive"] } serde_yaml = "0.9.25" serde_json = { workspace = true } diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index ad4a9a8823c..c6a6610d4b7 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -12,6 +12,7 @@ pub mod persistence; mod serde_helpers; pub const DEFAULT_WIREGUARD_PORT: u16 = WG_PORT; +pub const DEFAULT_WIREGUARD_PREFIX: u8 = 16; pub const DEFAULT_HTTP_PORT: u16 = DEFAULT_NYM_NODE_HTTP_PORT; // TODO: this is very much a WIP. we need proper ssl certificate support here @@ -75,6 +76,10 @@ pub struct Wireguard { /// 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: persistence::WireguardPaths, } @@ -88,6 +93,7 @@ impl Default for Wireguard { DEFAULT_WIREGUARD_PORT, ), announced_port: DEFAULT_WIREGUARD_PORT, + private_network_prefix: DEFAULT_WIREGUARD_PREFIX, storage_paths: persistence::WireguardPaths {}, } } diff --git a/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs index 850443e325f..dacc42afafc 100644 --- a/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs +++ b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs @@ -14,6 +14,7 @@ use nym_crypto::asymmetric::encryption::PublicKey; use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::{ ClientMessage, ClientRegistrationResponse, GatewayClient, InitMessage, Nonce, PeerPublicKey, }; +use rand::{prelude::IteratorRandom, thread_rng}; async fn process_final_message( client: GatewayClient, @@ -91,8 +92,23 @@ pub(crate) async fn register_client( let remote_public = PublicKey::from_bytes(init.pub_key().as_bytes()) .map_err(|_| RequestError::new_status(StatusCode::BAD_REQUEST))?; let nonce = process_init_message(init, state).await; - let gateway_data = - GatewayClient::new(state.dh_keypair.private_key(), remote_public, nonce); + 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.dh_keypair.private_key(), + remote_public, + *private_ip_ref.key(), + nonce, + ); let response = ClientRegistrationResponse::PendingRegistration { nonce, gateway_data, diff --git a/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs index 37348be94e6..622b12935d1 100644 --- a/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs +++ b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs @@ -7,8 +7,10 @@ use crate::http::api::v1::gateway::client_interfaces::wireguard::client_registry use crate::wireguard::types::{GatewayClientRegistry, PendingRegistrations}; use axum::routing::{get, post}; use axum::Router; +use ipnetwork::IpNetwork; use nym_crypto::asymmetric::encryption; use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard; +use nym_wireguard_types::registration::PrivateIPs; use std::sync::Arc; pub(crate) mod client_registry; @@ -26,6 +28,7 @@ impl WireguardAppState { client_registry: Arc, registration_in_progress: Arc, binding_port: u16, + private_ip_network: IpNetwork, ) -> Self { WireguardAppState { inner: Some(WireguardAppStateInner { @@ -33,6 +36,9 @@ impl WireguardAppState { client_registry, registration_in_progress, binding_port, + free_private_network_ips: Arc::new( + private_ip_network.iter().map(|ip| (ip, true)).collect(), + ), }), } } @@ -77,6 +83,7 @@ pub(crate) struct WireguardAppStateInner { client_registry: Arc, registration_in_progress: Arc, binding_port: u16, + free_private_network_ips: Arc, } pub(crate) fn routes(initial_state: WireguardAppState) -> Router { @@ -98,6 +105,7 @@ mod test { use axum::http::StatusCode; 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, @@ -105,6 +113,8 @@ mod test { }; use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard; use nym_wireguard_types::registration::HmacSha256; + use std::net::IpAddr; + use std::str::FromStr; use std::sync::Arc; use tower::Service; use tower::ServiceExt; @@ -136,6 +146,14 @@ mod test { let registration_in_progress = Arc::new(DashMap::new()); let client_registry = 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 state = WireguardAppState { inner: Some(WireguardAppStateInner { @@ -143,6 +161,7 @@ mod test { dh_keypair: Arc::new(gateway_key_pair), registration_in_progress: Arc::clone(®istration_in_progress), binding_port: 8080, + free_private_network_ips, }), }; @@ -186,11 +205,13 @@ mod test { 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()), });