Lp/dvpn psk injection (#6378)

* feat: clients to generate pseudorandom PSK for wg reg

* updating PSK of existing peers

* gateway probe fixes
This commit is contained in:
Jędrzej Stuczyński
2026-01-27 15:39:07 +00:00
committed by GitHub
parent 0d290b6028
commit d99eff9178
12 changed files with 193 additions and 21 deletions
@@ -0,0 +1,7 @@
/*
* Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
ALTER TABLE wireguard_peer
ADD COLUMN psk VARCHAR;
+17
View File
@@ -577,4 +577,21 @@ impl BandwidthGatewayStorage for GatewayStorage {
.await?;
Ok(())
}
/// Update the stored PSK of the wireguard peer.
///
/// # Arguments
///
/// * `public_key`: the unique public key of the wireguard peer.
/// * `psk`: the PSK of the wireguard peer.
async fn update_peer_psk(
&self,
public_key: &str,
psk: Option<&str>,
) -> Result<(), GatewayStorageError> {
self.wireguard_peer_manager
.update_peer_psk(public_key, psk)
.await?;
Ok(())
}
}
+8
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::{error::GatewayStorageError, make_bincode_serializer};
use defguard_wireguard_rs::key::Key;
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
use nym_gateway_requests::shared_key::SharedSymmetricKey;
use sqlx::FromRow;
@@ -95,6 +96,7 @@ pub struct WireguardPeer {
pub public_key: String,
pub allowed_ips: Vec<u8>,
pub client_id: i64,
pub psk: Option<String>,
}
impl WireguardPeer {
@@ -110,6 +112,7 @@ impl WireguardPeer {
source,
})?,
client_id,
psk: value.preshared_key.map(|psk| psk.to_lower_hex()),
})
}
}
@@ -132,6 +135,11 @@ impl TryFrom<WireguardPeer> for defguard_wireguard_rs::host::Peer {
field_key: "allowed_ips",
source,
})?,
preshared_key: value
.psk
.map(|psk| Key::decode(&psk))
.transpose()
.map_err(|_| Self::Error::TypeConversion { field_key: "psk" })?,
..Default::default()
})
}
+23
View File
@@ -170,6 +170,18 @@ pub trait BandwidthGatewayStorage: dyn_clone::DynClone {
/// * `peer_public_key`: wireguard public key of the peer to be removed.
async fn remove_wireguard_peer(&self, peer_public_key: &str)
-> Result<(), GatewayStorageError>;
/// Update the stored PSK of the wireguard peer.
///
/// # Arguments
///
/// * `public_key`: the unique public key of the wireguard peer.
/// * `psk`: the PSK of the wireguard peer.
async fn update_peer_psk(
&self,
public_key: &str,
psk: Option<&str>,
) -> Result<(), GatewayStorageError>;
}
#[cfg(feature = "mock")]
@@ -507,5 +519,16 @@ pub mod mock {
self.write().await.wireguard_peers.remove(peer_public_key);
Ok(())
}
async fn update_peer_psk(
&self,
public_key: &str,
psk: Option<&str>,
) -> Result<(), GatewayStorageError> {
if let Some(peer) = self.write().await.wireguard_peers.get_mut(public_key) {
peer.psk = psk.map(|psk| psk.to_owned())
}
Ok(())
}
}
}
@@ -98,4 +98,29 @@ impl WgPeerManager {
.await?;
Ok(())
}
/// Update the stored PSK of the wireguard peer.
///
/// # Arguments
///
/// * `public_key`: the unique public key of the wireguard peer.
/// * `psk`: the PSK of the wireguard peer.
pub(crate) async fn update_peer_psk(
&self,
public_key: &str,
psk: Option<&str>,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE wireguard_peer
SET psk = ?
WHERE public_key = ?
"#,
psk,
public_key,
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
+14 -2
View File
@@ -6,6 +6,7 @@
use crate::WireguardConfiguration;
use crate::serialisation::{BincodeError, BincodeOptions, lp_bincode_serializer};
use nym_credentials_interface::{CredentialSpendingData, TicketType};
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
@@ -50,6 +51,9 @@ pub struct LpDvpnRegistrationRequest {
/// Ticket type for bandwidth allocation
pub ticket_type: TicketType,
/// Preshared key to be used for the connection
pub psk: [u8; 32],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -108,17 +112,25 @@ pub struct LpRegistrationResponse {
impl LpRegistrationRequest {
/// Create a new dVPN registration request
pub fn new_dvpn(
pub fn new_dvpn<R>(
rng: &mut R,
wg_public_key: nym_wireguard_types::PeerPublicKey,
credential: CredentialSpendingData,
ticket_type: TicketType,
) -> Self {
) -> Self
where
R: RngCore + CryptoRng,
{
let mut psk = [0u8; 32];
rng.fill_bytes(&mut psk);
Self {
registration_data: LpRegistrationData::Dvpn {
data: Box::new(LpDvpnRegistrationRequest {
wg_public_key,
credential,
ticket_type,
psk,
}),
},
#[allow(clippy::expect_used)]
+28 -1
View File
@@ -22,6 +22,7 @@ use nym_registration_common::{
LpRegistrationData, LpRegistrationRequest, LpRegistrationResponse, WireguardConfiguration,
};
use nym_wireguard::PeerControlRequest;
use nym_wireguard_types::PeerPublicKey;
use std::sync::Arc;
use time::OffsetDateTime;
use tracing::*;
@@ -194,6 +195,20 @@ async fn check_existing_registration(
))
}
/// In the case of an already registered WG peer, update its PSK.
async fn update_peer_psk(
peer: PeerPublicKey,
psk: Key,
state: &LpHandlerState,
) -> Result<(), GatewayError> {
let encoded_psk = psk.to_lower_hex();
state
.storage
.update_peer_psk(&peer.to_string(), Some(&encoded_psk))
.await?;
Ok(())
}
async fn process_dvpn_registration(
request: Box<LpDvpnRegistrationRequest>,
state: &LpHandlerState,
@@ -206,6 +221,13 @@ async fn process_dvpn_registration(
// without wasting credentials
let wg_key_str = request.wg_public_key.to_string();
if let Some(existing_response) = check_existing_registration(&wg_key_str, state).await {
// TODO: this flow will be changed in subsequent PRs as it's wasting credentials regardless
if let Err(err) = update_peer_psk(request.wg_public_key, Key::new(request.psk), state).await
{
return LpRegistrationResponse::error(format!(
"WireGuard peer PSK update failed: {err}"
));
}
info!("LP dVPN re-registration for existing peer {wg_key_str} (idempotent)",);
inc!("lp_registration_dvpn_idempotent");
return existing_response;
@@ -215,6 +237,7 @@ async fn process_dvpn_registration(
let (gateway_data, client_id) = match register_wg_peer(
request.wg_public_key.inner().as_ref(),
request.ticket_type,
Key::new(request.psk),
state,
)
.await
@@ -349,6 +372,7 @@ pub async fn process_registration(
async fn register_wg_peer(
public_key_bytes: &[u8],
ticket_type: nym_credentials_interface::TicketType,
psk: Key,
state: &LpHandlerState,
) -> Result<(WireguardConfiguration, i64), GatewayError> {
let Some(wg_controller) = &state.wg_peer_controller else {
@@ -374,8 +398,10 @@ async fn register_wg_peer(
let peer_key = Key::new(key_bytes);
// Allocate IPs from centralized pool managed by PeerController
let registration_data = nym_wireguard::PeerRegistrationData::new(peer_key.clone());
let registration_data =
nym_wireguard::PeerRegistrationData::new(peer_key.clone()).with_preshared_key(psk);
let psk = registration_data.preshared_key.clone();
// Request IP allocation from PeerController
let (tx, rx) = oneshot::channel();
wg_controller
@@ -415,6 +441,7 @@ async fn register_wg_peer(
format!("{client_ipv6}/128").parse()?,
];
peer.persistent_keepalive_interval = Some(25);
peer.preshared_key = psk;
// Store peer in database FIRST (before adding to controller)
// This ensures bandwidth storage exists when controller's generate_bandwidth_manager() is called
+4
View File
@@ -441,6 +441,7 @@ mod tests {
let gateway_identity = entry.base.peer.ed25519().public_key();
let registration_result = client
.register(
&mut client_rng,
&wg_keypair,
gateway_identity,
&client_data.ticket_provider,
@@ -506,6 +507,7 @@ mod tests {
let gateway_identity = entry.base.peer.ed25519().public_key();
let registration_result = client
.register(
&mut client_rng,
&wg_keypair,
gateway_identity,
&client_data.ticket_provider,
@@ -646,6 +648,7 @@ mod tests {
let exit_registration_result = nested_session
.handshake_and_register(
&mut entry_client,
&mut client_rng,
&client_data.base.x25519_wg_keys,
exit.base.peer.ed25519().public_key(),
&client_data.ticket_provider,
@@ -657,6 +660,7 @@ mod tests {
// 14. complete registration with the entry
let entry_registration_result = entry_client
.register(
&mut client_rng,
&client_data.base.x25519_wg_keys,
entry.base.peer.ed25519().public_key(),
&client_data.ticket_provider,
+11 -2
View File
@@ -1168,7 +1168,7 @@ where
);
match client
.register_with_credential(&wg_keypair, credential, ticket_type)
.register_with_credential(&mut rng, &wg_keypair, credential, ticket_type)
.await
{
Ok(data) => data,
@@ -1183,6 +1183,7 @@ where
info!("Using real bandwidth controller for LP registration");
match client
.register(
&mut rng,
&wg_keypair,
&gateway_ed25519_pubkey,
bandwidth_controller,
@@ -1311,6 +1312,7 @@ where
match nested_session
.handshake_and_register_with_credential(
&mut entry_client,
&mut rng,
&exit_wg_keypair,
credential,
TicketType::V1WireguardExit,
@@ -1327,6 +1329,7 @@ where
match nested_session
.handshake_and_register(
&mut entry_client,
&mut rng,
&exit_wg_keypair,
&exit_gateway_pubkey,
bandwidth_controller,
@@ -1357,7 +1360,12 @@ where
TicketType::V1WireguardEntry,
);
match entry_client
.register_with_credential(&entry_wg_keypair, credential, TicketType::V1WireguardEntry)
.register_with_credential(
&mut rng,
&entry_wg_keypair,
credential,
TicketType::V1WireguardEntry,
)
.await
{
Ok(data) => data,
@@ -1369,6 +1377,7 @@ where
} else {
match entry_client
.register(
&mut rng,
&entry_wg_keypair,
&entry_gateway_pubkey,
bandwidth_controller,
+18 -2
View File
@@ -23,6 +23,7 @@ pub use builder::config::{
pub use config::RegistrationMode;
pub use error::RegistrationClientError;
pub use lp_client::{LpConfig, LpRegistrationClient, NestedLpSession, error::LpClientError};
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
pub use types::{
LpRegistrationResult, MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult,
};
@@ -153,7 +154,14 @@ impl RegistrationClient {
)))
}
async fn register_lp(self) -> Result<RegistrationResult, RegistrationClientError> {
// create dedicated method taking RNG instance for tests
async fn register_lp_with_rng<R>(
self,
rng: &mut R,
) -> Result<RegistrationResult, RegistrationClientError>
where
R: RngCore + CryptoRng,
{
// Extract and validate LP data
let entry_lp_data = self.config.entry.node.lp_data.ok_or(
RegistrationClientError::LpRegistrationNotPossible {
@@ -210,8 +218,9 @@ impl RegistrationClient {
// Perform handshake and registration with exit gateway (all via entry forwarding)
let exit_gateway_data = nested_session
.handshake_and_register::<TcpStream>(
.handshake_and_register::<TcpStream, _>(
&mut entry_client,
rng,
&self.config.exit.keys,
&self.config.exit.node.identity,
&*self.bandwidth_controller,
@@ -230,6 +239,7 @@ impl RegistrationClient {
tracing::info!("Registering with entry gateway");
let entry_gateway_data = entry_client
.register(
rng,
&self.config.entry.keys,
&self.config.entry.node.identity,
&*self.bandwidth_controller,
@@ -257,6 +267,12 @@ impl RegistrationClient {
})))
}
async fn register_lp(self) -> Result<RegistrationResult, RegistrationClientError> {
let mut rng = rand::thread_rng();
self.register_lp_with_rng(&mut rng).await
}
pub async fn register(self) -> Result<RegistrationResult, RegistrationClientError> {
self.cancel_token
.clone()
@@ -21,6 +21,7 @@ use nym_lp::state_machine::{LpAction, LpData, LpInput, LpStateMachine};
use nym_lp_transport::traits::LpTransport;
use nym_registration_common::{LpRegistrationRequest, WireguardConfiguration};
use nym_wireguard_types::PeerPublicKey;
use rand::{CryptoRng, RngCore};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -662,6 +663,7 @@ where
/// for that please use [`Self::register_with_retry`] instead
///
/// # Arguments
/// * `rng` - RNG instance for generating PSK
/// * `wg_keypair` - Client's WireGuard x25519 keypair
/// * `gateway_identity` - Gateway's ed25519 identity for credential verification
/// * `bandwidth_controller` - Provider for bandwidth credentials
@@ -678,13 +680,17 @@ where
/// - Network communication fails
/// - Gateway rejected the registration
/// - Response times out (see LpConfig::registration_timeout)
pub async fn register(
pub async fn register<R>(
&mut self,
rng: &mut R,
wg_keypair: &x25519::KeyPair,
gateway_identity: &ed25519::PublicKey,
bandwidth_controller: &dyn BandwidthTicketProvider,
ticket_type: TicketType,
) -> Result<WireguardConfiguration> {
) -> Result<WireguardConfiguration>
where
R: RngCore + CryptoRng,
{
tracing::debug!("Acquiring bandwidth credential for registration");
// Get bandwidth credential from controller
@@ -698,7 +704,7 @@ where
})?
.data;
self.register_with_credential(wg_keypair, credential, ticket_type)
self.register_with_credential(rng, wg_keypair, credential, ticket_type)
.await
}
@@ -708,6 +714,7 @@ where
/// Uses the persistent TCP connection established during handshake.
///
/// # Arguments
/// * `rng` - RNG instance for generating PSK
/// * `wg_keypair` - Client's WireGuard x25519 keypair
/// * `credential` - Pre-generated bandwidth credential
/// * `ticket_type` - Type of bandwidth ticket
@@ -721,17 +728,21 @@ where
///
/// # Panics / Errors
/// Returns error if handshake not completed or if connection was closed.
pub async fn register_with_credential(
pub async fn register_with_credential<R>(
&mut self,
rng: &mut R,
wg_keypair: &x25519::KeyPair,
credential: CredentialSpendingData,
ticket_type: TicketType,
) -> Result<WireguardConfiguration> {
) -> Result<WireguardConfiguration>
where
R: RngCore + CryptoRng,
{
tracing::debug!("Sending registration request (persistent connection)");
// 1. Build registration request
let wg_public_key = PeerPublicKey::new(wg_keypair.public_key().to_bytes().into());
let request = LpRegistrationRequest::new_dvpn(wg_public_key, credential, ticket_type);
let request = LpRegistrationRequest::new_dvpn(rng, wg_public_key, credential, ticket_type);
tracing::trace!("Built registration request: {:?}", request);
@@ -850,6 +861,7 @@ where
/// will return the cached result instead of spending a new credential.
///
/// # Arguments
/// * `rng` - RNG instance for generating PSK
/// * `wg_keypair` - Client's WireGuard x25519 keypair (same key used for all retries)
/// * `gateway_identity` - Gateway's ed25519 identity for credential verification
/// * `bandwidth_controller` - Provider for bandwidth credentials
@@ -865,14 +877,18 @@ where
/// # Note
/// Unlike `register()`, this method handles the full flow including handshake.
/// Do NOT call `perform_handshake()` before this method.
pub async fn register_with_retry(
pub async fn register_with_retry<R>(
&mut self,
rng: &mut R,
wg_keypair: &x25519::KeyPair,
gateway_identity: &ed25519::PublicKey,
bandwidth_controller: &dyn BandwidthTicketProvider,
ticket_type: TicketType,
max_retries: u32,
) -> Result<WireguardConfiguration> {
) -> Result<WireguardConfiguration>
where
R: RngCore + CryptoRng,
{
tracing::debug!("Starting resilient registration (max_retries={max_retries})",);
// Acquire credential ONCE before any attempts
@@ -916,7 +932,7 @@ where
}
match self
.register_with_credential(wg_keypair, credential.clone(), ticket_type)
.register_with_credential(rng, wg_keypair, credential.clone(), ticket_type)
.await
{
Ok(data) => {
@@ -32,6 +32,7 @@ use nym_lp::{LpMessage, LpPacket};
use nym_lp_transport::traits::LpTransport;
use nym_registration_common::{LpRegistrationRequest, WireguardConfiguration};
use nym_wireguard_types::PeerPublicKey;
use rand::{CryptoRng, RngCore};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -287,15 +288,17 @@ impl NestedLpSession {
///
/// # Returns
/// * `Ok(GatewayData)` - Exit gateway configuration data on successful registration
pub async fn handshake_and_register_with_credential<S>(
pub async fn handshake_and_register_with_credential<S, R>(
&mut self,
outer_client: &mut LpRegistrationClient<S>,
rng: &mut R,
wg_keypair: &x25519::KeyPair,
credential: nym_credentials_interface::CredentialSpendingData,
ticket_type: TicketType,
) -> Result<WireguardConfiguration>
where
S: LpTransport + Unpin,
R: RngCore + CryptoRng,
{
// Step 1: Perform handshake with exit gateway via forwarding
self.perform_handshake(outer_client).await?;
@@ -311,7 +314,7 @@ impl NestedLpSession {
// Step 3: Build registration request (credential already provided)
let wg_public_key = PeerPublicKey::new(wg_keypair.public_key().to_bytes().into());
let request = LpRegistrationRequest::new_dvpn(wg_public_key, credential, ticket_type);
let request = LpRegistrationRequest::new_dvpn(rng, wg_public_key, credential, ticket_type);
tracing::trace!("Built registration request: {:?}", request);
@@ -425,9 +428,10 @@ impl NestedLpSession {
/// - Forwarding through entry gateway fails
/// - Response decryption/deserialization fails
/// - Gateway rejects the registration
pub async fn handshake_and_register<S>(
pub async fn handshake_and_register<S, R>(
&mut self,
outer_client: &mut LpRegistrationClient<S>,
rng: &mut R,
wg_keypair: &x25519::KeyPair,
gateway_identity: &ed25519::PublicKey,
bandwidth_controller: &dyn BandwidthTicketProvider,
@@ -435,6 +439,7 @@ impl NestedLpSession {
) -> Result<WireguardConfiguration>
where
S: LpTransport + Unpin,
R: RngCore + CryptoRng,
{
// Step 1: Perform handshake with exit gateway via forwarding
self.perform_handshake(outer_client).await?;
@@ -461,7 +466,7 @@ impl NestedLpSession {
// Step 4: Build registration request
let wg_public_key = PeerPublicKey::new(wg_keypair.public_key().to_bytes().into());
let request = LpRegistrationRequest::new_dvpn(wg_public_key, credential, ticket_type);
let request = LpRegistrationRequest::new_dvpn(rng, wg_public_key, credential, ticket_type);
tracing::trace!("Built registration request: {:?}", request);
@@ -577,8 +582,9 @@ impl NestedLpSession {
/// # Errors
/// Returns an error if all retry attempts fail.
#[allow(clippy::too_many_arguments)]
pub async fn handshake_and_register_with_retry<S>(
pub async fn handshake_and_register_with_retry<S, R>(
&mut self,
rng: &mut R,
outer_client: &mut LpRegistrationClient<S>,
wg_keypair: &x25519::KeyPair,
gateway_identity: &ed25519::PublicKey,
@@ -588,6 +594,7 @@ impl NestedLpSession {
) -> Result<WireguardConfiguration>
where
S: LpTransport + Unpin,
R: RngCore + CryptoRng,
{
tracing::debug!(
"Starting resilient exit registration (max_retries={})",
@@ -635,6 +642,7 @@ impl NestedLpSession {
match self
.handshake_and_register_with_credential(
outer_client,
rng,
wg_keypair,
credential.clone(),
ticket_type,