1use std::path::PathBuf;
13use std::sync::Arc;
14use std::time::Duration;
15
16use nym_bandwidth_controller::config::BandwidthControllerConfig;
17use nym_bandwidth_controller::requests::BandwidthControllerRequestSender;
18use nym_bandwidth_controller::{BandwidthController, BandwidthTicketProvider, TicketType};
19use nym_bandwidth_fetcher::NyxdCredentialFetcher;
20use nym_credentials_interface::BandwidthCredential;
21use nym_crypto::asymmetric::{ed25519, x25519};
22use nym_lp::peer::{DHKeyPair, LpRemotePeer};
23use nym_network_defaults::NymNetworkDetails;
24use nym_registration_client::{LpRegistrationClient, NestedLpSession};
25use nym_registration_common::WireguardConfiguration;
26use nym_task::ShutdownToken;
27use nym_validator_client::nym_api::NymApiClientExt;
28use nym_validator_client::DirectSigningHttpRpcNyxdClient;
29use rand09::SeedableRng;
30use sha2::{Digest, Sha256};
31use time::OffsetDateTime;
32use tokio::net::TcpStream;
33use tokio::task::JoinHandle;
34use tokio_util::sync::CancellationToken;
35use url::Url;
36use zeroize::Zeroizing;
37
38use nym_api_requests::models::described::v2::NymNodeDescriptionV2;
39
40use crate::config::{RestockPolicy, SessionConfig};
41use crate::dvpn::{DvpnDirectory, QuicBridge};
42use crate::error::SessionError;
43use crate::fetcher::TimeoutFetcher;
44use crate::gateway::{self, GatewayInfo, GatewaySpec, SelectedGateway, WgRole};
45use crate::registration_cache::RegistrationCache;
46
47const TICKETS_TO_SPEND: u32 = 1;
49const API_TIMEOUT: Duration = Duration::from_secs(30);
51const PROVISIONING_TIMEOUT: Duration = Duration::from_secs(5 * 60);
54
55fn wireguard_ticket_types() -> Vec<TicketType> {
58 vec![TicketType::V1WireguardEntry, TicketType::V1WireguardExit]
59}
60
61fn needed_ticket_types(two_hop: bool) -> Vec<TicketType> {
63 let mut types = vec![TicketType::V1WireguardEntry];
64 if two_hop {
65 types.push(TicketType::V1WireguardExit);
66 }
67 types
68}
69
70pub struct HopConfig {
72 pub wg_config: WireguardConfiguration,
74 pub client_private_key: x25519::PrivateKey,
76 pub gateway_identity: ed25519::PublicKey,
78 pub gateway: GatewayInfo,
80 pub bridge: Option<QuicBridge>,
83}
84
85pub struct Registration {
87 pub entry: HopConfig,
89 pub exit: Option<HopConfig>,
91}
92
93struct OwnedController {
96 sender: BandwidthControllerRequestSender,
97 task: JoinHandle<()>,
98}
99
100pub struct Session {
102 api: nym_http_api_client::Client,
103 provider: Arc<dyn BandwidthTicketProvider>,
106 owned: Option<OwnedController>,
108 cancel: CancellationToken,
109 directory: Option<DvpnDirectory>,
111 reg_cache: Option<std::sync::Mutex<RegistrationCache>>,
115}
116
117impl Session {
118 pub async fn new(
122 config: SessionConfig,
123 cancel: CancellationToken,
124 ) -> Result<Self, SessionError> {
125 let SessionConfig {
126 mnemonic,
127 network,
128 credential_store_path,
129 data_path,
130 dvpn_directory_url,
131 automatic_topups,
132 bandwidth_provider,
133 reuse_registrations,
134 } = config;
135
136 let reg_cache = reuse_registrations.then(|| {
139 std::sync::Mutex::new(RegistrationCache::load(
140 &data_path,
141 network.network_name.clone(),
142 ))
143 });
144
145 let api_url_str = network
146 .endpoints
147 .iter()
148 .find_map(|e| e.api_url.clone())
149 .ok_or(SessionError::MissingEndpoint { which: "nym-api" })?;
150 let api_url = Url::parse(&api_url_str).map_err(|source| SessionError::InvalidUrl {
151 which: "nym-api",
152 url: api_url_str.clone(),
153 source,
154 })?;
155 let api = nym_http_api_client::Client::new(api_url, Some(API_TIMEOUT));
156
157 let directory = match dvpn_directory_url {
159 Some(url) => match DvpnDirectory::fetch(&url).await {
160 Ok(dir) => Some(dir),
161 Err(e) => {
162 tracing::warn!("failed to fetch dVPN directory at {url}: {e}");
163 Some(DvpnDirectory::default())
164 }
165 },
166 None => None,
167 };
168
169 let (provider, owned) = match bandwidth_provider {
171 Some(external) => (external, None),
172 None => {
173 let (provider, owned) = Self::spawn_controller(
174 mnemonic,
175 network,
176 credential_store_path,
177 data_path,
178 automatic_topups,
179 cancel.clone(),
180 )
181 .await?;
182 (provider, Some(owned))
183 }
184 };
185
186 Ok(Self {
187 api,
188 provider,
189 owned,
190 cancel,
191 directory,
192 reg_cache,
193 })
194 }
195
196 async fn spawn_controller(
198 mnemonic: bip39::Mnemonic,
199 network: NymNetworkDetails,
200 credential_store_path: Option<PathBuf>,
201 data_path: PathBuf,
202 automatic_topups: Option<RestockPolicy>,
203 cancel: CancellationToken,
204 ) -> Result<(Arc<dyn BandwidthTicketProvider>, OwnedController), SessionError> {
205 let nyxd_url = network
206 .endpoints
207 .first()
208 .map(|e| e.nyxd_url.clone())
209 .ok_or(SessionError::MissingEndpoint { which: "nyxd" })?;
210
211 let client_id = derive_client_id(&mnemonic);
214
215 let nyxd = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic_and_network_details(
217 nyxd_url.as_str(),
218 network,
219 mnemonic,
220 )?;
221 let nyxd = Arc::new(nyxd);
222
223 let store_path = credential_store_path.unwrap_or_else(|| data_path.join("credentials.db"));
225 if let Some(parent) = store_path.parent() {
226 std::fs::create_dir_all(parent).map_err(|e| SessionError::Storage(e.to_string()))?;
227 }
228 let storage = nym_credential_storage::initialise_persistent_storage(&store_path).await;
229
230 let fetcher_db = data_path.join("fetcher-requests.db");
237 let fetcher = NyxdCredentialFetcher::new(nyxd, &fetcher_db, client_id)
238 .await
239 .map_err(|e| SessionError::Issuance(e.to_string()))?;
240 let fetcher = TimeoutFetcher::new(fetcher);
241
242 let config = match automatic_topups {
250 Some(policy) => {
251 let mut config: BandwidthControllerConfig = policy.into();
252 config.managed_ticket_types = wireguard_ticket_types();
253 config
254 }
255 None => BandwidthControllerConfig {
256 managed_ticket_types: Vec::new(),
257 ..Default::default()
258 },
259 };
260 let controller = BandwidthController::new(storage)
261 .with_config(config)
262 .with_credential_fetcher(fetcher);
263
264 let sender = controller.get_request_sender();
265 let shutdown = ShutdownToken::new_from_tokio_token(cancel.clone());
266 let task = tokio::spawn(async move { controller.run(shutdown).await });
267
268 let provider: Arc<dyn BandwidthTicketProvider> = Arc::new(sender.clone());
269 Ok((provider, OwnedController { sender, task }))
270 }
271
272 pub async fn ensure_ticketbooks(&self, two_hop: bool) -> Result<(), SessionError> {
277 self.ensure_ticket_types(needed_ticket_types(two_hop)).await
278 }
279
280 async fn ensure_ticket_types(&self, types: Vec<TicketType>) -> Result<(), SessionError> {
285 if types.is_empty() {
286 return Ok(());
287 }
288 let Some(owned) = &self.owned else {
289 return Ok(());
291 };
292 tokio::select! {
303 biased;
304 _ = self.cancel.cancelled() => Err(SessionError::Cancelled),
305 res = tokio::time::timeout(PROVISIONING_TIMEOUT, async {
306 owned
307 .sender
308 .restock_ticketbooks(types.clone())
309 .await
310 .map_err(|e| SessionError::Issuance(e.to_string()))?;
311 owned
312 .sender
313 .wait_for_ticketbooks(types)
314 .await
315 .map_err(|e| SessionError::Issuance(e.to_string()))
316 }) => res.unwrap_or(Err(SessionError::ProvisioningTimeout {
317 after: PROVISIONING_TIMEOUT,
318 })),
319 }
320 }
321
322 pub async fn obtain_wireguard_credential(
326 &self,
327 gateway_id: ed25519::PublicKey,
328 role: WgRole,
329 ) -> Result<BandwidthCredential, SessionError> {
330 let ticket_type = match role {
331 WgRole::Entry => TicketType::V1WireguardEntry,
332 WgRole::Exit => TicketType::V1WireguardExit,
333 };
334 let prepared = self
335 .provider
336 .get_ecash_ticket(
337 ticket_type,
338 gateway_id,
339 TICKETS_TO_SPEND,
340 OffsetDateTime::now_utc(),
341 )
342 .await
343 .map_err(|e| SessionError::Issuance(e.to_string()))?
344 .ok_or_else(|| {
345 SessionError::Issuance("no stored ticket available for top-up".into())
346 })?;
347 Ok(BandwidthCredential::from(prepared.data))
348 }
349
350 pub fn bandwidth_provider(&self) -> Arc<dyn BandwidthTicketProvider> {
353 self.provider.clone()
354 }
355
356 async fn fetch_topology(&self) -> Result<Vec<NymNodeDescriptionV2>, SessionError> {
358 Ok(self.api.get_all_described_nodes_v2().await?)
359 }
360
361 async fn fetch_topology_cancellable(&self) -> Result<Vec<NymNodeDescriptionV2>, SessionError> {
365 tokio::select! {
366 biased;
367 _ = self.cancel.cancelled() => Err(SessionError::Cancelled),
368 res = self.fetch_topology() => res,
369 }
370 }
371
372 pub async fn select_gateway(
374 &self,
375 spec: &GatewaySpec,
376 role: WgRole,
377 ) -> Result<SelectedGateway, SessionError> {
378 tokio::select! {
379 biased;
380 _ = self.cancel.cancelled() => Err(SessionError::Cancelled),
381 res = async {
382 let nodes = self.fetch_topology().await?;
383 gateway::select(&nodes, spec, role, self.directory.as_ref(), false, None)
384 } => res,
385 }
386 }
387
388 pub async fn register_single_hop(
391 &self,
392 gateway: &GatewaySpec,
393 ) -> Result<Registration, SessionError> {
394 self.register_single_inner(gateway).await
395 }
396
397 fn with_cache<R>(&self, f: impl FnOnce(&mut RegistrationCache) -> R) -> Option<R> {
400 self.reg_cache.as_ref().map(|cache| {
401 f(&mut cache
402 .lock()
403 .unwrap_or_else(|poisoned| poisoned.into_inner()))
404 })
405 }
406
407 fn cached_hop(
412 &self,
413 identity: &ed25519::PublicKey,
414 gateway: GatewayInfo,
415 role: WgRole,
416 ) -> Option<HopConfig> {
417 let cached = self.with_cache(|cache| cache.lookup(identity, role))??;
418 tracing::info!(
419 "reusing cached registration for {} ({role:?}) — no ticket spent",
420 identity.to_base58_string()
421 );
422 Some(HopConfig {
423 wg_config: cached.wg_config,
424 client_private_key: cached.client_private_key,
425 gateway_identity: *identity,
426 gateway,
427 bridge: None,
428 })
429 }
430
431 fn finalize_hop(
435 &self,
436 identity: &ed25519::PublicKey,
437 gateway: GatewayInfo,
438 role: WgRole,
439 client_private_key: x25519::PrivateKey,
440 wg_config: WireguardConfiguration,
441 ) -> HopConfig {
442 self.with_cache(|cache| cache.insert(identity, role, &client_private_key, &wg_config));
443 HopConfig {
444 wg_config,
445 client_private_key,
446 gateway_identity: *identity,
447 gateway,
448 bridge: None,
449 }
450 }
451
452 pub fn invalidate_registration(&self, gateway: &ed25519::PublicKey, role: WgRole) {
457 self.with_cache(|cache| cache.remove(gateway, role));
458 }
459
460 async fn register_single_inner(
461 &self,
462 gateway: &GatewaySpec,
463 ) -> Result<Registration, SessionError> {
464 let nodes = self.fetch_topology_cancellable().await?;
469 let selected = gateway::select(
470 &nodes,
471 gateway,
472 WgRole::Entry,
473 self.directory.as_ref(),
474 false,
475 None,
476 )?;
477 if let Some(hop) = self.cached_hop(&selected.identity, selected.info(), WgRole::Entry) {
479 return Ok(Registration {
480 entry: hop,
481 exit: None,
482 });
483 }
484 self.ensure_ticket_types(vec![TicketType::V1WireguardEntry])
485 .await?;
486 let hop = self
487 .register_hop(&selected, TicketType::V1WireguardEntry)
488 .await?;
489 Ok(Registration {
490 entry: hop,
491 exit: None,
492 })
493 }
494
495 pub async fn register_two_hop(
498 &self,
499 entry: &GatewaySpec,
500 exit: &GatewaySpec,
501 ) -> Result<Registration, SessionError> {
502 self.register_two_hop_inner(entry, exit, false).await
503 }
504
505 pub async fn register_two_hop_quic(
511 &self,
512 entry: &GatewaySpec,
513 exit: &GatewaySpec,
514 ) -> Result<Registration, SessionError> {
515 self.register_two_hop_inner(entry, exit, true).await
516 }
517
518 async fn register_two_hop_inner(
519 &self,
520 entry: &GatewaySpec,
521 exit: &GatewaySpec,
522 entry_quic: bool,
523 ) -> Result<Registration, SessionError> {
524 let nodes = self.fetch_topology_cancellable().await?;
529 let entry_gw = gateway::select(
530 &nodes,
531 entry,
532 WgRole::Entry,
533 self.directory.as_ref(),
534 entry_quic,
535 None,
536 )?;
537 let exit_gw = gateway::select(
539 &nodes,
540 exit,
541 WgRole::Exit,
542 self.directory.as_ref(),
543 false,
544 Some(&entry_gw.identity),
545 )?;
546
547 let entry_bridge = if entry_quic {
550 entry_gw.quic.clone()
551 } else {
552 None
553 };
554
555 let (cached_entry, cached_exit) = match (
559 self.cached_hop(&entry_gw.identity, entry_gw.info(), WgRole::Entry),
560 self.cached_hop(&exit_gw.identity, exit_gw.info(), WgRole::Exit),
561 ) {
562 (Some(mut entry_hop), Some(exit_hop)) => {
563 entry_hop.bridge = entry_bridge;
564 return Ok(Registration {
565 entry: entry_hop,
566 exit: Some(exit_hop),
567 });
568 }
569 partial => partial,
570 };
571
572 let mut needed = Vec::new();
574 if cached_entry.is_none() {
575 needed.push(TicketType::V1WireguardEntry);
576 }
577 if cached_exit.is_none() {
578 needed.push(TicketType::V1WireguardExit);
579 }
580 self.ensure_ticket_types(needed).await?;
581
582 let entry_lp = lp_info(&entry_gw)?;
583 let exit_lp = lp_info(&exit_gw)?;
584
585 let entry_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
588 let entry_peer =
589 LpRemotePeer::new(entry_lp.x25519).with_key_digests(entry_lp.expected_kem_key_hashes);
590 let mut entry_client = LpRegistrationClient::<TcpStream>::new_with_default_config(
591 entry_keypair,
592 entry_peer,
593 entry_lp.address,
594 entry_lp.ciphersuite,
595 entry_lp.lp_protocol_version,
596 );
597 tokio::select! {
601 biased;
602 _ = self.cancel.cancelled() => return Err(SessionError::Cancelled),
603 r = entry_client.perform_handshake() => r.map_err(|source| SessionError::Registration {
604 address: entry_lp.address,
605 source,
606 })?,
607 }
608
609 let mut rng = rand09::rngs::StdRng::from_os_rng();
610
611 let exit_hop = match cached_exit {
613 Some(hop) => hop,
614 None => {
615 let exit_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
616 let exit_peer = LpRemotePeer::new(exit_lp.x25519)
617 .with_key_digests(exit_lp.expected_kem_key_hashes);
618 let mut nested = NestedLpSession::new(
619 exit_lp.address,
620 exit_keypair,
621 exit_peer,
622 exit_lp.ciphersuite,
623 exit_lp.lp_protocol_version,
624 );
625 let exit_wg = x25519::KeyPair::new(&mut rand::thread_rng());
626 let exit_cfg = nested
627 .handshake_and_register_dvpn::<TcpStream, _>(
628 &mut entry_client,
629 &mut rng,
630 &exit_wg,
631 &exit_gw.identity,
632 self.provider.as_ref(),
633 None,
634 TicketType::V1WireguardExit,
635 )
636 .await
637 .map_err(|source| SessionError::Registration {
638 address: exit_lp.address,
639 source,
640 })?;
641 self.finalize_hop(
642 &exit_gw.identity,
643 exit_gw.info(),
644 WgRole::Exit,
645 x25519::PrivateKey::from_secret(exit_wg.private_key().to_bytes()),
646 exit_cfg,
647 )
648 }
649 };
650
651 let mut entry_hop = match cached_entry {
653 Some(hop) => hop,
654 None => {
655 let entry_wg = x25519::KeyPair::new(&mut rand::thread_rng());
656 let entry_cfg = entry_client
657 .register_dvpn(
658 &mut rng,
659 &entry_wg,
660 &entry_gw.identity,
661 self.provider.as_ref(),
662 None,
663 TicketType::V1WireguardEntry,
664 )
665 .await
666 .map_err(|source| SessionError::Registration {
667 address: entry_lp.address,
668 source,
669 })?;
670 self.finalize_hop(
671 &entry_gw.identity,
672 entry_gw.info(),
673 WgRole::Entry,
674 x25519::PrivateKey::from_secret(entry_wg.private_key().to_bytes()),
675 entry_cfg,
676 )
677 }
678 };
679 entry_hop.bridge = entry_bridge;
680
681 Ok(Registration {
682 entry: entry_hop,
683 exit: Some(exit_hop),
684 })
685 }
686
687 async fn register_hop(
689 &self,
690 selected: &SelectedGateway,
691 ticket_type: TicketType,
692 ) -> Result<HopConfig, SessionError> {
693 let lp = lp_info(selected)?;
694 let keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
695 let peer = LpRemotePeer::new(lp.x25519).with_key_digests(lp.expected_kem_key_hashes);
696 let mut client = LpRegistrationClient::<TcpStream>::new_with_default_config(
697 keypair,
698 peer,
699 lp.address,
700 lp.ciphersuite,
701 lp.lp_protocol_version,
702 );
703
704 tokio::select! {
708 biased;
709 _ = self.cancel.cancelled() => return Err(SessionError::Cancelled),
710 r = client.perform_handshake() => r.map_err(|source| SessionError::Registration {
711 address: lp.address,
712 source,
713 })?,
714 }
715
716 let mut rng = rand09::rngs::StdRng::from_os_rng();
717 let wg = x25519::KeyPair::new(&mut rand::thread_rng());
718 let cfg = client
719 .register_dvpn(
720 &mut rng,
721 &wg,
722 &selected.identity,
723 self.provider.as_ref(),
724 None,
725 ticket_type,
726 )
727 .await
728 .map_err(|source| SessionError::Registration {
729 address: lp.address,
730 source,
731 })?;
732
733 let role = match ticket_type {
734 TicketType::V1WireguardExit => WgRole::Exit,
735 _ => WgRole::Entry,
736 };
737 Ok(self.finalize_hop(
738 &selected.identity,
739 selected.info(),
740 role,
741 x25519::PrivateKey::from_secret(wg.private_key().to_bytes()),
742 cfg,
743 ))
744 }
745
746 pub async fn shutdown(mut self) {
749 self.cancel.cancel();
750 if let Some(owned) = self.owned.take() {
751 let _ = owned.task.await;
752 }
753 }
754}
755
756impl Drop for Session {
757 fn drop(&mut self) {
758 self.cancel.cancel();
761 }
762}
763
764fn derive_client_id(mnemonic: &bip39::Mnemonic) -> Zeroizing<Vec<u8>> {
768 let entropy = Zeroizing::new(mnemonic.to_entropy());
769 let mut hasher = Sha256::new();
770 hasher.update(b"nym-sdk-session::client-id::v1");
771 hasher.update(entropy.as_slice());
772 Zeroizing::new(hasher.finalize().to_vec())
773}
774
775fn lp_info(
777 selected: &SelectedGateway,
778) -> Result<nym_registration_common::NymNodeLPInformation, SessionError> {
779 selected
780 .node
781 .node
782 .lp_data
783 .clone()
784 .ok_or_else(|| SessionError::MalformedGateway {
785 identity: selected.identity.to_base58_string(),
786 reason: "gateway advertises no LP data".to_string(),
787 })
788}
789
790#[cfg(test)]
791#[path = "session_tests.rs"]
792mod tests;