Skip to main content

citadel_sdk/prefabs/client/
peer_connection.rs

1//! Peer-to-Peer Connection Management
2//!
3//! This module provides functionality for establishing and managing peer-to-peer connections
4//! in the Citadel Protocol. It supports both direct and NAT-traversed connections with
5//! configurable security settings and file transfer capabilities.
6//!
7//! # Features
8//! - Multiple simultaneous peer connections
9//! - Configurable UDP and security settings per peer
10//! - Built-in file transfer support
11//! - Automatic peer registration handling
12//! - Session password protection
13//! - Connection state management
14//! - Flexible peer identification
15//!
16//! # Example
17//! ```rust
18//! use citadel_sdk::prelude::*;
19//! use citadel_sdk::prefabs::client::peer_connection::{PeerConnectionKernel, PeerConnectionSetupAggregator};
20//!
21//! # fn main() -> Result<(), NetworkError> {
22//! async fn connect_to_peers() -> Result<(), NetworkError> {
23//!     // Set up connections to multiple peers with different settings
24//!     let peers = PeerConnectionSetupAggregator::default()
25//!         .with_peer_custom("alice")
26//!         .with_udp_mode(UdpMode::Enabled)
27//!         .add()
28//!         .with_peer_custom("bob")
29//!         .with_session_security_settings(Default::default())
30//!         .add();
31//!
32//!     let settings = DefaultServerConnectionSettingsBuilder::transient("127.0.0.1:25021")
33//!         .build()?;
34//!
35//!     let kernel = PeerConnectionKernel::new(
36//!         settings,
37//!         peers,
38//!         |connections, _remote| async move {
39//!             println!("Attemping to connect to {} peers!", connections.len());
40//!             Ok(())
41//!         },
42//!     );
43//!
44//!     Ok(())
45//! }
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! # Important Notes
51//! - Peers must be mutually registered before connecting
52//! - UDP mode affects NAT traversal capabilities
53//! - File transfers require proper handler setup
54//! - Session passwords must match on both peers
55//!
56//! # Related Components
57//! - [`PeerConnectionSetupAggregator`]: Peer connection configuration
58//! - [`FileTransferHandleRx`]: File transfer handling
59//! - [`UserIdentifier`]: Peer identification
60//! - [`SessionSecuritySettings`]: Connection security
61//!
62//! [`PeerConnectionSetupAggregator`]: crate::prefabs::client::peer_connection::PeerConnectionSetupAggregator
63//! [`FileTransferHandleRx`]: crate::prefabs::client::peer_connection::FileTransferHandleRx
64//! [`UserIdentifier`]: crate::prelude::UserIdentifier
65//! [`SessionSecuritySettings`]: crate::prelude::SessionSecuritySettings
66
67use crate::prelude::results::PeerConnectSuccess;
68use crate::prelude::*;
69use crate::test_common::wait_for_peers;
70use citadel_io::tokio::sync::mpsc::{Receiver, UnboundedSender};
71use citadel_io::{tokio, Mutex};
72use citadel_proto::prelude::async_trait;
73use citadel_user::hypernode_account::UserIdentifierExt;
74use futures::stream::FuturesUnordered;
75use futures::TryStreamExt;
76use std::collections::HashMap;
77use std::future::Future;
78use std::marker::PhantomData;
79use std::sync::Arc;
80use uuid::Uuid;
81
82/// After establishing a connection to the central node, this kernel begins connecting to the desired
83/// peer(s)
84pub struct PeerConnectionKernel<'a, F, Fut, R: Ratchet> {
85    inner_kernel: Box<dyn NetKernel<R> + 'a>,
86    shared: Shared,
87    // by using fn() -> Fut, the future does not need to be Sync
88    _pd: PhantomData<fn() -> (F, Fut)>,
89}
90
91#[derive(Clone)]
92#[doc(hidden)]
93pub struct Shared {
94    active_peer_conns: Arc<Mutex<HashMap<PeerConnectionType, PeerContext>>>,
95}
96
97struct PeerContext {
98    #[allow(dead_code)]
99    conn_type: PeerConnectionType,
100    send_file_transfer_tx: UnboundedSender<ObjectTransferHandler>,
101}
102
103#[derive(Debug)]
104pub struct FileTransferHandleRx {
105    pub inner: citadel_io::tokio::sync::mpsc::UnboundedReceiver<ObjectTransferHandler>,
106    pub conn_type: VirtualTargetType,
107}
108
109impl FileTransferHandleRx {
110    /// Accepts all incoming file transfer handles and processes them in the background
111    pub fn accept_all(mut self) {
112        let task = tokio::task::spawn(async move {
113            let rx = &mut self.inner;
114            while let Some(mut handle) = rx.recv().await {
115                let task = tokio::task::spawn(async move {
116                    if let Err(err) = handle.exhaust_stream().await {
117                        let orientation = handle.orientation;
118                        log::warn!(target: "citadel", "Error background handling of file transfer for {orientation:?}: {err:?}");
119                    }
120                });
121
122                drop(task);
123            }
124        });
125
126        drop(task);
127    }
128}
129
130impl std::ops::Deref for FileTransferHandleRx {
131    type Target = citadel_io::tokio::sync::mpsc::UnboundedReceiver<ObjectTransferHandler>;
132
133    fn deref(&self) -> &Self::Target {
134        &self.inner
135    }
136}
137
138impl std::ops::DerefMut for FileTransferHandleRx {
139    fn deref_mut(&mut self) -> &mut Self::Target {
140        &mut self.inner
141    }
142}
143
144impl Drop for FileTransferHandleRx {
145    fn drop(&mut self) {
146        log::trace!(target: "citadel", "Dropping file transfer handle receiver {:?}", self.conn_type);
147    }
148}
149
150#[async_trait]
151impl<F, Fut, R: Ratchet> NetKernel<R> for PeerConnectionKernel<'_, F, Fut, R> {
152    fn load_remote(&mut self, server_remote: NodeRemote<R>) -> Result<(), NetworkError> {
153        self.inner_kernel.load_remote(server_remote)
154    }
155
156    async fn on_start(&self) -> Result<(), NetworkError> {
157        self.inner_kernel.on_start().await
158    }
159
160    #[allow(clippy::collapsible_else_if)]
161    async fn on_node_event_received(&self, message: NodeResult<R>) -> Result<(), NetworkError> {
162        match message {
163            NodeResult::ObjectTransferHandle(ObjectTransferHandle {
164                ticket: _,
165                handle,
166                session_cid,
167            }) => {
168                let is_revfs = matches!(
169                    handle.metadata.transfer_type,
170                    TransferType::RemoteEncryptedVirtualFilesystem { .. }
171                );
172                let active_peers = self.shared.active_peer_conns.lock();
173                let v_conn = if is_revfs {
174                    let peer_cid = if session_cid != handle.source {
175                        handle.source
176                    } else {
177                        handle.receiver
178                    };
179                    PeerConnectionType::LocalGroupPeer {
180                        session_cid,
181                        peer_cid,
182                    }
183                } else {
184                    if matches!(
185                        handle.orientation,
186                        ObjectTransferOrientation::Receiver { .. }
187                    ) {
188                        PeerConnectionType::LocalGroupPeer {
189                            session_cid,
190                            peer_cid: handle.source,
191                        }
192                    } else {
193                        PeerConnectionType::LocalGroupPeer {
194                            session_cid,
195                            peer_cid: handle.receiver,
196                        }
197                    }
198                };
199
200                if let Some(peer_ctx) = active_peers.get(&v_conn) {
201                    if let Err(err) = peer_ctx.send_file_transfer_tx.send(handle) {
202                        log::warn!(target: "citadel", "Error forwarding file transfer handle: {:?}", err.to_string());
203                    }
204                } else {
205                    log::warn!(target: "citadel", "Unable to find key for inbound file transfer handle: {:?}\n Active Peers: {:?} \n handle_source = {}, handle_receiver = {}", v_conn, active_peers.keys().cloned().collect::<Vec<_>>(), handle.source, handle.receiver);
206                }
207
208                Ok(())
209            }
210
211            // Note: NodeResult::Disconnect is for C2S connections only.
212            // P2P disconnects are handled via NodeResult::PeerEvent(PeerSignal::Disconnect).
213            // C2S disconnects don't require removing from active_peer_conns.
214            unprocessed @ NodeResult::Disconnect(..) | unprocessed => {
215                // pass any unprocessed events to the lower kernel
216                self.inner_kernel.on_node_event_received(unprocessed).await
217            }
218        }
219    }
220
221    async fn on_stop(&mut self) -> Result<(), NetworkError> {
222        self.inner_kernel.on_stop().await
223    }
224}
225
226/// Allows easy aggregation of [`UserIdentifier`]'s and custom settings for the connection
227/// request
228#[derive(Debug, Default, Clone)]
229pub struct PeerConnectionSetupAggregator {
230    inner: Vec<PeerConnectionSettings>,
231}
232
233#[derive(Debug, Clone)]
234struct PeerConnectionSettings {
235    id: UserIdentifier,
236    session_security_settings: SessionSecuritySettings,
237    udp_mode: UdpMode,
238    ensure_registered: bool,
239    peer_session_password: Option<PreSharedKey>,
240    turn: Option<TurnRelayConfig>,
241}
242
243pub struct AddedPeer {
244    list: PeerConnectionSetupAggregator,
245    id: UserIdentifier,
246    session_security_settings: Option<SessionSecuritySettings>,
247    ensure_registered: bool,
248    udp_mode: Option<UdpMode>,
249    peer_session_password: Option<PreSharedKey>,
250    turn: Option<TurnRelayConfig>,
251}
252
253impl AddedPeer {
254    /// Adds the peer
255    pub fn add(mut self) -> PeerConnectionSetupAggregator {
256        let new = PeerConnectionSettings {
257            id: self.id,
258            session_security_settings: self.session_security_settings.unwrap_or_default(),
259            udp_mode: self.udp_mode.unwrap_or_default(),
260            ensure_registered: self.ensure_registered,
261            peer_session_password: self.peer_session_password,
262            turn: self.turn,
263        };
264
265        self.list.inner.push(new);
266        self.list
267    }
268
269    /// Sets the [`UdpMode`] for this peer to peer connection
270    pub fn with_udp_mode(mut self, udp_mode: UdpMode) -> Self {
271        self.udp_mode = Some(udp_mode);
272        self
273    }
274
275    /// Supplies a TURN relay configuration for this P2P connection (the peer must supply one
276    /// too). Without it, peers whose NATs defeat hole punching stay relayed through the server.
277    pub fn with_turn_config(mut self, turn: TurnRelayConfig) -> Self {
278        self.turn = Some(turn);
279        self
280    }
281
282    /// Disables the UDP mode for the client-to-server connection. The default setting is Disabled
283    pub fn disable_udp(self) -> Self {
284        self.with_udp_mode(UdpMode::Disabled)
285    }
286
287    /// Enables the UDP mode for the client-to-server connection. The default setting is Disabled
288    pub fn enable_udp(self) -> Self {
289        self.with_udp_mode(UdpMode::Enabled)
290    }
291
292    /// Sets the [`SessionSecuritySettings`] for this peer to peer connection
293    pub fn with_session_security_settings(
294        mut self,
295        session_security_settings: SessionSecuritySettings,
296    ) -> Self {
297        self.session_security_settings = Some(session_security_settings);
298        self
299    }
300
301    /// Ensures that the target user is registered before attempting to connect
302    pub fn ensure_registered(mut self) -> Self {
303        self.ensure_registered = true;
304        self
305    }
306
307    /// Adds a pre-shared key to the peer session password list. Both connecting nodes
308    /// must have matching passwords in order to establish a connection. Default is None.
309    pub fn with_session_password<T: Into<PreSharedKey>>(mut self, password: T) -> Self {
310        self.peer_session_password = Some(password.into());
311        self
312    }
313}
314
315impl PeerConnectionSetupAggregator {
316    /// Adds a peer with default connection settings
317    /// ```
318    /// use citadel_sdk::prelude::*;
319    /// let peers = PeerConnectionSetupAggregator::default()
320    ///     .with_peer("john.doe")
321    ///     .with_peer("alice")
322    ///     .with_peer("bob");
323    /// ```
324    pub fn with_peer<T: Into<UserIdentifier>>(self, peer: T) -> PeerConnectionSetupAggregator {
325        self.with_peer_custom(peer).add()
326    }
327
328    /// Adds a peer with custom settings
329    /// ```
330    /// use citadel_sdk::prelude::*;
331    /// // Set up a p2p connection to john.doe with udp enabled,
332    /// // and, a p2p connection to alice with udp disabled and
333    /// // custom security settings
334    /// let peers = PeerConnectionSetupAggregator::default()
335    ///     .with_peer_custom("john.doe")
336    ///     .with_udp_mode(UdpMode::Enabled)
337    ///     .add()
338    ///     .with_peer_custom("alice")
339    ///     .with_udp_mode(UdpMode::Disabled)
340    ///     .with_session_security_settings(Default::default())
341    ///     .add();
342    /// ```
343    pub fn with_peer_custom<T: Into<UserIdentifier>>(self, peer: T) -> AddedPeer {
344        AddedPeer {
345            list: self,
346            id: peer.into(),
347            ensure_registered: false,
348            session_security_settings: None,
349            udp_mode: None,
350            peer_session_password: None,
351            turn: None,
352        }
353    }
354}
355
356impl From<PeerConnectionSetupAggregator> for Vec<PeerConnectionSettings> {
357    fn from(this: PeerConnectionSetupAggregator) -> Self {
358        this.inner
359    }
360}
361
362impl From<Vec<UserIdentifier>> for PeerConnectionSetupAggregator {
363    fn from(ids: Vec<UserIdentifier>) -> Self {
364        let mut this = PeerConnectionSetupAggregator::default();
365        for peer in ids {
366            this = this.with_peer(peer);
367        }
368
369        this
370    }
371}
372
373impl From<UserIdentifier> for PeerConnectionSetupAggregator {
374    fn from(this: UserIdentifier) -> Self {
375        Self::from(vec![this])
376    }
377}
378
379impl From<Uuid> for PeerConnectionSetupAggregator {
380    fn from(user: Uuid) -> Self {
381        let user_identifier: UserIdentifier = user.into();
382        user_identifier.into()
383    }
384}
385
386impl From<String> for PeerConnectionSetupAggregator {
387    fn from(this: String) -> Self {
388        let user_identifier: UserIdentifier = this.into();
389        user_identifier.into()
390    }
391}
392
393impl From<&str> for PeerConnectionSetupAggregator {
394    fn from(this: &str) -> Self {
395        let user_identifier: UserIdentifier = this.into();
396        user_identifier.into()
397    }
398}
399
400impl From<u64> for PeerConnectionSetupAggregator {
401    fn from(this: u64) -> Self {
402        let user_identifier: UserIdentifier = this.into();
403        user_identifier.into()
404    }
405}
406
407#[async_trait]
408impl<'a, F, Fut, T: Into<PeerConnectionSetupAggregator> + Send + 'a, R: Ratchet>
409    PrefabFunctions<'a, T, R> for PeerConnectionKernel<'a, F, Fut, R>
410where
411    F: FnOnce(
412            Receiver<Result<PeerConnectSuccess<R>, NetworkError>>,
413            CitadelClientServerConnection<R>,
414        ) -> Fut
415        + Send
416        + 'a,
417    Fut: Future<Output = Result<(), NetworkError>> + Send + 'a,
418{
419    type UserLevelInputFunction = F;
420    type SharedBundle = Shared;
421
422    fn get_shared_bundle(&self) -> Self::SharedBundle {
423        self.shared.clone()
424    }
425
426    #[allow(clippy::blocks_in_conditions)]
427    #[cfg_attr(
428        feature = "localhost-testing",
429        tracing::instrument(level = "trace", target = "citadel", skip_all, ret, err(Debug))
430    )]
431    async fn on_c2s_channel_received(
432        connect_success: CitadelClientServerConnection<R>,
433        peers_to_connect: T,
434        f: Self::UserLevelInputFunction,
435        shared: Shared,
436    ) -> Result<(), NetworkError> {
437        let shared = &shared;
438        let session_cid = connect_success.cid;
439        let mut peers_already_registered = vec![];
440
441        wait_for_peers().await;
442        let peers_to_connect = peers_to_connect.into().inner;
443
444        for peer in &peers_to_connect {
445            // TODO: optimize this into a single concurrent operation
446            peers_already_registered.push(
447                peer.id
448                    .search_peer(session_cid, connect_success.account_manager())
449                    .await?,
450            )
451        }
452
453        let remote = connect_success.clone();
454        let (tx, rx) = citadel_io::tokio::sync::mpsc::channel(peers_to_connect.len());
455        let requests = FuturesUnordered::new();
456
457        for (mutually_registered, peer_to_connect) in
458            peers_already_registered.into_iter().zip(peers_to_connect)
459        {
460            // Each task will be responsible for possibly registering to and connecting
461            // with the desired peer
462            let remote = remote.clone();
463            let tx = tx.clone();
464            let PeerConnectionSettings {
465                id,
466                session_security_settings,
467                udp_mode,
468                ensure_registered,
469                peer_session_password,
470                turn,
471            } = peer_to_connect;
472
473            let task = async move {
474                let inner_task = async move {
475                    let (file_transfer_tx, file_transfer_rx) =
476                        citadel_io::tokio::sync::mpsc::unbounded_channel();
477
478                    // Get the actual peer CID from the mutual registration info if available
479                    let peer_cid = if let Some(mutual_peer) = &mutually_registered {
480                        mutual_peer.cid
481                    } else {
482                        id.get_cid()
483                    };
484
485                    let handle = if let Some(_already_registered) = mutually_registered {
486                        remote.find_target(session_cid, id).await?
487                    } else {
488                        // TODO: optimize peer registration + connection in one go
489                        log::info!(target: "citadel", "{session_cid} proposing target {id:?} to central node");
490                        let handle = remote.propose_target(session_cid, id.clone()).await?;
491                        // if the peer is not yet registered to the central node, wait for it to become registered
492                        // this is useful especially for testing purposes
493                        if ensure_registered {
494                            loop {
495                                if handle.is_peer_registered().await? {
496                                    break;
497                                }
498                                citadel_io::time::sleep(std::time::Duration::from_millis(200))
499                                    .await;
500                            }
501                        }
502
503                        log::info!(target: "citadel", "{session_cid} registering to peer {id:?}");
504                        // A decline is an Ok. This read `let _reg_success = ...?`
505                        // and then logged "success -> now connecting", so a peer
506                        // that had just said no was sent a PostConnect and the
507                        // caller waited out a 60s RemoteP2pConnectTimeout —
508                        // reporting a timeout instead of the refusal the SDK
509                        // already had.
510                        let registration = handle.register_to_peer().await?;
511                        if let Some(reason) = registration.refusal_reason() {
512                            return Err(NetworkError::generic(format!(
513                                "Cannot connect to peer {id:?}: {reason}"
514                            )));
515                        }
516                        log::info!(target: "citadel", "{session_cid} registered to peer {id:?} registered || success -> now connecting");
517                        handle
518                    };
519
520                    // Register the peer connection early before attempting to connect
521                    // This prevents race conditions where file transfers arrive before connection completes
522                    let peer_conn = PeerConnectionType::LocalGroupPeer {
523                        session_cid,
524                        peer_cid,
525                    };
526                    let peer_context = PeerContext {
527                        conn_type: peer_conn,
528                        send_file_transfer_tx: file_transfer_tx.clone(),
529                    };
530                    log::debug!(target: "citadel", "Early registering peer connection: {peer_conn:?}");
531                    let _ = shared
532                        .active_peer_conns
533                        .lock()
534                        .insert(peer_conn, peer_context);
535
536                    if let Some(turn) = turn {
537                        handle.set_turn_config(Some(turn)).await?;
538                    }
539
540                    handle
541                        .connect_to_peer_custom(
542                            session_security_settings,
543                            udp_mode,
544                            peer_session_password,
545                        )
546                        .await
547                        .map(|mut success| {
548                            let actual_peer_conn = success.channel.get_peer_conn_type().unwrap();
549
550                            // If the actual peer connection type differs from our early registration,
551                            // update it
552                            if actual_peer_conn != peer_conn {
553                                log::debug!(target: "citadel", "Updating peer connection registration from {peer_conn:?} to {actual_peer_conn:?}");
554                                let mut active_peers = shared.active_peer_conns.lock();
555                                if let Some(peer_ctx) = active_peers.remove(&peer_conn) {
556                                    let _ = active_peers.insert(actual_peer_conn, peer_ctx);
557                                }
558                            }
559                            // Update the existing entry with the file transfer receiver
560                            success.incoming_object_transfer_handles = Some(FileTransferHandleRx {
561                                inner: file_transfer_rx,
562                                conn_type: actual_peer_conn.as_virtual_connection(),
563                            });
564                            success
565                        })
566                        .inspect_err(|_err| {
567                            // Clean up the early registration on connection failure
568                            let _ = shared.active_peer_conns.lock().remove(&peer_conn);
569                        })
570                };
571
572                tx.send(inner_task.await)
573                    .await
574                    .map_err(|err| NetworkError::generic(err.to_string()))
575            };
576
577            requests.push(Box::pin(task))
578        }
579
580        // TODO: What should be done if a peer conn fails? No room for error here
581        // Drop the original tx so channel closes after all tasks complete
582        drop(tx);
583
584        // Use join! not try_join! to ensure both branches complete even if one errors
585        // This prevents premature cancellation of the user callback
586        let (collection_result, user_result) =
587            citadel_io::tokio::join!(requests.try_collect::<()>(), f(rx, connect_success));
588
589        // Return first error, or success if both succeeded
590        collection_result?;
591        user_result
592    }
593
594    fn construct(kernel: Box<dyn NetKernel<R> + 'a>) -> Self {
595        Self {
596            inner_kernel: kernel,
597            shared: Shared {
598                active_peer_conns: Arc::new(Mutex::new(Default::default())),
599            },
600            _pd: Default::default(),
601        }
602    }
603}
604
605#[cfg(all(test, feature = "localhost-testing"))]
606mod tests {
607    use crate::prefabs::client::peer_connection::PeerConnectionKernel;
608    use crate::prefabs::client::DefaultServerConnectionSettingsBuilder;
609    use crate::prelude::*;
610    use crate::remote_ext::results::PeerConnectSuccess;
611    use crate::test_common::{server_info, wait_for_peers, TestBarrier};
612    use citadel_io::tokio;
613    use citadel_io::tokio::sync::mpsc::{Receiver, UnboundedSender};
614    use citadel_user::prelude::UserIdentifierExt;
615    use futures::stream::FuturesUnordered;
616    use futures::TryStreamExt;
617    use rstest::rstest;
618    use std::collections::HashMap;
619    use std::future::Future;
620    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
621    use std::time::Duration;
622    use uuid::Uuid;
623
624    lazy_static::lazy_static! {
625        pub static ref PEERS: Vec<(String, String, String)> = {
626            ["alpha", "beta", "charlie", "echo", "delta", "epsilon", "foxtrot"]
627            .iter().map(|base| (format!("{base}.username"), format!("{base}.password"), format!("{base}.full_name")))
628            .collect()
629        };
630    }
631
632    #[rstest]
633    #[case(2, UdpMode::Enabled)]
634    #[case(3, UdpMode::Disabled)]
635    #[timeout(Duration::from_secs(90))]
636    #[tokio::test(flavor = "multi_thread")]
637    async fn peer_to_peer_connect(#[case] peer_count: usize, #[case] udp_mode: UdpMode) {
638        assert!(peer_count > 1);
639        citadel_logging::setup_log();
640        TestBarrier::setup(peer_count);
641
642        let client_success = &AtomicUsize::new(0);
643        let (server, server_addr) = server_info::<StackedRatchet>();
644
645        let client_kernels = FuturesUnordered::new();
646        let total_peers = (0..peer_count)
647            .map(|idx| PEERS.get(idx).unwrap().0.clone())
648            .collect::<Vec<String>>();
649
650        for idx in 0..peer_count {
651            let (username, password, full_name) = PEERS.get(idx).unwrap();
652            let peers = total_peers
653                .clone()
654                .into_iter()
655                .filter(|r| r != username)
656                .map(UserIdentifier::Username)
657                .collect::<Vec<UserIdentifier>>();
658
659            let mut agg = PeerConnectionSetupAggregator::default();
660
661            for peer in peers {
662                agg = agg
663                    .with_peer_custom(peer)
664                    .ensure_registered()
665                    .with_udp_mode(udp_mode)
666                    .with_session_security_settings(SessionSecuritySettings::default())
667                    .add();
668            }
669
670            let server_connection_settings =
671                DefaultServerConnectionSettingsBuilder::credentialed_registration(
672                    server_addr,
673                    username,
674                    full_name,
675                    password.as_str(),
676                )
677                .build()
678                .unwrap();
679
680            let username = username.clone();
681
682            let client_kernel = PeerConnectionKernel::new(
683                server_connection_settings,
684                agg.clone(),
685                move |results, connection| async move {
686                    log::info!(target: "citadel", "***PEER {username} CONNECTED ***");
687                    let session_cid = connection.conn_type.get_session_cid();
688                    let check = move |conn: PeerConnectSuccess<_>| async move {
689                        let session_cid = conn.channel.get_session_cid();
690                        let _mutual_peers = conn
691                            .remote
692                            .remote()
693                            .get_local_group_mutual_peers(session_cid)
694                            .await
695                            .unwrap();
696                        conn
697                    };
698                    let p2p_remotes = handle_peer_connect_successes(
699                        results,
700                        session_cid,
701                        peer_count,
702                        udp_mode,
703                        check,
704                    )
705                    .await
706                    .into_iter()
707                    .map(|r| (r.channel.get_peer_cid(), r.remote))
708                    .collect::<HashMap<_, _>>();
709
710                    // By now, all the network peers have been registered to.
711                    // Test that getting the peers (not necessarily mutual)
712                    // show up
713                    let network_peers = connection.get_peers(None).await.unwrap();
714                    for user in agg.inner {
715                        let peer_cid = user.id.get_cid();
716                        assert!(network_peers.iter().any(|r| r.cid == peer_cid))
717                    }
718
719                    // test to make sure the mutuals are valid
720                    let session_cid = connection.conn_type.get_session_cid();
721                    let mutual_peers = connection
722                        .get_local_group_mutual_peers(session_cid)
723                        .await
724                        .unwrap();
725                    for (peer_cid, _) in p2p_remotes {
726                        assert!(mutual_peers.iter().any(|r| r.cid == peer_cid))
727                    }
728
729                    log::info!(target: "citadel", "***PEER {username} finished all checks***");
730                    let _ = client_success.fetch_add(1, Ordering::Relaxed);
731                    wait_for_peers().await;
732                    connection.shutdown_kernel().await
733                },
734            );
735
736            let client = DefaultNodeBuilder::default()
737                .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
738                .build(client_kernel)
739                .unwrap();
740            client_kernels.push(async move { client.await.map(|_| ()) });
741        }
742
743        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
744
745        assert!(futures::future::try_select(server, clients).await.is_ok());
746
747        assert_eq!(client_success.load(Ordering::Relaxed), peer_count);
748    }
749
750    #[rstest]
751    #[case(2, HeaderObfuscatorSettings::default())]
752    #[case(2, HeaderObfuscatorSettings::Enabled)]
753    #[case(2, HeaderObfuscatorSettings::EnabledWithKey(12345))]
754    #[case(3, HeaderObfuscatorSettings::default())]
755    #[timeout(Duration::from_secs(90))]
756    #[tokio::test(flavor = "multi_thread")]
757    async fn peer_to_peer_connect_transient(
758        #[case] peer_count: usize,
759        #[case] header_obfuscator_settings: HeaderObfuscatorSettings,
760    ) -> Result<(), Box<dyn std::error::Error>> {
761        assert!(peer_count > 1);
762        citadel_logging::setup_log();
763        TestBarrier::setup(peer_count);
764        let udp_mode = UdpMode::Enabled;
765
766        let do_deregister = peer_count == 2;
767
768        let client_success = &AtomicUsize::new(0);
769        let (server, server_addr) = server_info::<StackedRatchet>();
770
771        let client_kernels = FuturesUnordered::new();
772        let total_peers = (0..peer_count)
773            .map(|_| Uuid::new_v4())
774            .collect::<Vec<Uuid>>();
775
776        for idx in 0..peer_count {
777            let uuid = total_peers.get(idx).cloned().unwrap();
778            let peers = total_peers
779                .clone()
780                .into_iter()
781                .filter(|r| r != &uuid)
782                .map(UserIdentifier::from)
783                .collect::<Vec<UserIdentifier>>();
784
785            let mut agg = PeerConnectionSetupAggregator::default();
786
787            for peer in peers {
788                let security_settings = SessionSecuritySettings {
789                    header_obfuscator_settings,
790                    ..Default::default()
791                };
792                agg = agg
793                    .with_peer_custom(peer)
794                    .with_udp_mode(udp_mode)
795                    .ensure_registered()
796                    .with_session_security_settings(security_settings)
797                    .add();
798            }
799
800            let server_connection_settings =
801                DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
802                    .build()
803                    .unwrap();
804
805            let client_kernel = PeerConnectionKernel::new(
806                server_connection_settings,
807                agg,
808                move |results, remote| async move {
809                    log::info!(target: "citadel", "***PEER {uuid} CONNECTED***");
810                    let session_cid = remote.conn_type.get_session_cid();
811
812                    let check = move |conn: PeerConnectSuccess<_>| async move {
813                        if do_deregister {
814                            conn.remote
815                                .deregister()
816                                .await
817                                .expect("Deregistration failed");
818                            assert!(!conn
819                                .remote
820                                .inner
821                                .account_manager()
822                                .get_persistence_handler()
823                                .hyperlan_peer_exists(session_cid, conn.channel.get_peer_cid())
824                                .await
825                                .unwrap());
826                        }
827                        conn
828                    };
829
830                    let _ = handle_peer_connect_successes(
831                        results,
832                        session_cid,
833                        peer_count,
834                        udp_mode,
835                        check,
836                    )
837                    .await;
838
839                    log::info!(target: "citadel", "***PEER {uuid} finished all checks***");
840                    let _ = client_success.fetch_add(1, Ordering::Relaxed);
841                    wait_for_peers().await;
842                    remote.shutdown_kernel().await
843                },
844            );
845
846            let client = DefaultNodeBuilder::default()
847                .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
848                .build(client_kernel)?;
849            client_kernels.push(async move { client.await.map(|_| ()) });
850        }
851
852        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
853
854        if let Err(err) = futures::future::try_select(server, clients).await {
855            return match err {
856                futures::future::Either::Left(res) => Err(res.0.into_string().into()),
857                futures::future::Either::Right(res) => Err(res.0.into_string().into()),
858            };
859        }
860
861        assert_eq!(client_success.load(Ordering::Relaxed), peer_count);
862        Ok(())
863    }
864
865    #[rstest]
866    #[case(2)]
867    #[case(3)]
868    #[timeout(std::time::Duration::from_secs(180))]
869    #[tokio::test(flavor = "multi_thread")]
870    async fn test_peer_to_peer_file_transfer(
871        #[case] peer_count: usize,
872    ) -> Result<(), Box<dyn std::error::Error>> {
873        assert!(peer_count > 1);
874        citadel_logging::setup_log();
875        TestBarrier::setup(peer_count);
876        let udp_mode = UdpMode::Enabled;
877
878        let sender_success = &AtomicBool::new(false);
879        let receiver_success = &AtomicBool::new(false);
880
881        let (server, server_addr) = server_info::<StackedRatchet>();
882
883        let client_kernels = FuturesUnordered::new();
884        let total_peers = (0..peer_count)
885            .map(|_| Uuid::new_v4())
886            .collect::<Vec<Uuid>>();
887
888        let sender_uuid = total_peers[0];
889
890        for idx in 0..peer_count {
891            let uuid = total_peers.get(idx).cloned().unwrap();
892            let mut peers = total_peers
893                .clone()
894                .into_iter()
895                .filter(|r| r != &uuid)
896                .map(UserIdentifier::from)
897                .collect::<Vec<UserIdentifier>>();
898            // 0: [1, 2] <-- At idx 0, we want the sender to connect to all other peers
899            // 1: [0] <-- At idx 1, we want the receiver to connect to the sender
900            // 2: [0] <-- At idx 2, we want the receiver to connect to the sender
901            // ..
902            // n: [0] <-- At idx n, we want the receiver to connect to the sender
903            if idx != 0 {
904                peers = vec![sender_uuid.into()];
905            }
906
907            let mut agg = PeerConnectionSetupAggregator::default();
908
909            for peer in peers {
910                agg = agg
911                    .with_peer_custom(peer)
912                    .ensure_registered()
913                    .with_udp_mode(udp_mode)
914                    .with_session_security_settings(SessionSecuritySettings::default())
915                    .add();
916            }
917
918            let server_connection_settings =
919                DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
920                    .build()
921                    .unwrap();
922
923            let client_kernel = PeerConnectionKernel::new(
924                server_connection_settings,
925                agg,
926                move |results, remote| async move {
927                    log::info!(target: "citadel", "***PEER {uuid} CONNECTED***");
928                    wait_for_peers().await;
929                    let session_cid = remote.conn_type.get_session_cid();
930                    let is_sender = idx == 0; // the first peer is the sender, the rest are receivers
931                    let check = move |mut conn: PeerConnectSuccess<_>| async move {
932                        if is_sender {
933                            conn.remote
934                                .send_file_with_custom_opts(
935                                    "../resources/TheBridge.pdf",
936                                    32 * 1024,
937                                    TransferType::FileTransfer,
938                                )
939                                .await
940                                .expect("Failed to send file");
941                        } else {
942                            // TODO: route file-transfer + other events to peer channel
943                            // Bounded, so a failure names the stage it stopped at.
944                            //
945                            // `case_2` times out at the rstest budget of 180s on
946                            // macOS, intermittently, and the report is the bare
947                            // "Timeout 180s expired" — which cannot say whether
948                            // the handle never arrived, or arrived and the
949                            // transfer then stalled part way. Those are different
950                            // bugs. Two other intermittent failures in this suite
951                            // were solved this session by exactly this move, and
952                            // neither yielded to reading the code.
953                            //
954                            // The budgets are generous fractions of the 180s the
955                            // test already allows, so this cannot turn a slow
956                            // machine into a failure it would not otherwise have
957                            // had — it only replaces silence with a sentence.
958                            const HANDLE_BUDGET: std::time::Duration =
959                                std::time::Duration::from_secs(60);
960                            const STATUS_BUDGET: std::time::Duration =
961                                std::time::Duration::from_secs(60);
962
963                            let mut handles = conn.incoming_object_transfer_handles.take().unwrap();
964                            let mut handle = match citadel_io::tokio::time::timeout(
965                                HANDLE_BUDGET,
966                                handles.recv(),
967                            )
968                            .await
969                            {
970                                Ok(Some(handle)) => handle,
971                                Ok(None) => panic!(
972                                    "the incoming-transfer channel closed before any handle arrived"
973                                ),
974                                Err(_) => panic!(
975                                    "no incoming file-transfer handle within {HANDLE_BUDGET:?}: the sender never started, or its request never routed here"
976                                ),
977                            };
978                            handle.accept().unwrap();
979
980                            use citadel_types::proto::ObjectTransferStatus;
981                            use futures::StreamExt;
982                            let mut path = None;
983                            while let Some(status) = match citadel_io::tokio::time::timeout(
984                                STATUS_BUDGET,
985                                handle.next(),
986                            )
987                            .await
988                            {
989                                Ok(status) => status,
990                                Err(_) => panic!(
991                                    "the transfer stalled: no status within {STATUS_BUDGET:?}, last seen {path:?}"
992                                ),
993                            } {
994                                match status {
995                                    ObjectTransferStatus::ReceptionComplete => {
996                                        let cmp =
997                                            include_bytes!("../../../../resources/TheBridge.pdf");
998                                        let streamed_data =
999                                            citadel_io::tokio::fs::read(path.clone().unwrap())
1000                                                .await
1001                                                .unwrap();
1002                                        assert_eq!(
1003                                            cmp,
1004                                            streamed_data.as_slice(),
1005                                            "Original data and streamed data does not match"
1006                                        );
1007
1008                                        log::info!(target: "citadel", "Peer has finished receiving and verifying the file!");
1009                                        break;
1010                                    }
1011
1012                                    ObjectTransferStatus::ReceptionBeginning(file_path, vfm) => {
1013                                        path = Some(file_path);
1014                                        assert_eq!(vfm.name, "TheBridge.pdf")
1015                                    }
1016
1017                                    _ => {}
1018                                }
1019                            }
1020                        }
1021
1022                        conn
1023                    };
1024                    // Use a peer count of two since we only have one sender and one receiver per pair
1025                    // However, we need a way of ensuring we collect three results
1026                    let peer_count = if idx == 0 { peer_count } else { 2 };
1027                    let _ = handle_peer_connect_successes(
1028                        results,
1029                        session_cid,
1030                        peer_count,
1031                        udp_mode,
1032                        check,
1033                    )
1034                    .await;
1035
1036                    if is_sender {
1037                        sender_success.store(true, Ordering::Relaxed);
1038                    } else {
1039                        receiver_success.store(true, Ordering::Relaxed);
1040                    }
1041
1042                    log::info!(target: "citadel", "***PEER {uuid} (is_sender: {is_sender}) finished all checks***");
1043                    wait_for_peers().await;
1044                    log::info!(target: "citadel", "***PEER {uuid} (is_sender: {is_sender}) shutting down***");
1045                    remote.shutdown_kernel().await
1046                },
1047            );
1048
1049            let client = DefaultNodeBuilder::default()
1050                .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
1051                .build(client_kernel)
1052                .unwrap();
1053            client_kernels.push(async move { client.await.map(|_| ()) });
1054        }
1055
1056        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
1057
1058        if let Err(err) = futures::future::try_select(server, clients).await {
1059            return match err {
1060                futures::future::Either::Left(res) => Err(res.0.into_string().into()),
1061                futures::future::Either::Right(res) => Err(res.0.into_string().into()),
1062            };
1063        }
1064
1065        assert!(sender_success.load(Ordering::Relaxed));
1066        assert!(receiver_success.load(Ordering::Relaxed));
1067        Ok(())
1068    }
1069
1070    #[rstest]
1071    #[case(2)]
1072    #[timeout(std::time::Duration::from_secs(90))]
1073    #[tokio::test(flavor = "multi_thread")]
1074    async fn test_peer_to_peer_rekey(
1075        #[case] peer_count: usize,
1076    ) -> Result<(), Box<dyn std::error::Error>> {
1077        assert!(peer_count > 1);
1078        citadel_logging::setup_log();
1079        TestBarrier::setup(peer_count);
1080        let udp_mode = UdpMode::Enabled;
1081
1082        let client_success = &AtomicUsize::new(0);
1083        let (server, server_addr) = server_info::<StackedRatchet>();
1084
1085        let client_kernels = FuturesUnordered::new();
1086        let total_peers = (0..peer_count)
1087            .map(|_| Uuid::new_v4())
1088            .collect::<Vec<Uuid>>();
1089
1090        for idx in 0..peer_count {
1091            let uuid = total_peers.get(idx).cloned().unwrap();
1092            let peers = total_peers
1093                .clone()
1094                .into_iter()
1095                .filter(|r| r != &uuid)
1096                .map(UserIdentifier::from)
1097                .collect::<Vec<UserIdentifier>>();
1098
1099            let mut agg = PeerConnectionSetupAggregator::default();
1100
1101            for peer in peers {
1102                agg = agg
1103                    .with_peer_custom(peer)
1104                    .ensure_registered()
1105                    .with_udp_mode(udp_mode)
1106                    .with_session_security_settings(SessionSecuritySettings::default())
1107                    .add();
1108            }
1109
1110            let server_connection_settings =
1111                DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
1112                    .build()
1113                    .unwrap();
1114
1115            let client_kernel = PeerConnectionKernel::new(
1116                server_connection_settings,
1117                agg,
1118                move |results, remote| async move {
1119                    log::info!(target: "citadel", "***PEER {uuid} CONNECTED***");
1120                    let session_cid = remote.conn_type.get_session_cid();
1121
1122                    let check = move |conn: PeerConnectSuccess<_>| async move {
1123                        if idx == 0 {
1124                            for x in 1..10 {
1125                                assert_eq!(
1126                                    conn.remote.rekey().await.expect("Failed to rekey"),
1127                                    Some(x)
1128                                );
1129                            }
1130                        }
1131
1132                        conn
1133                    };
1134
1135                    let results = handle_peer_connect_successes(
1136                        results,
1137                        session_cid,
1138                        peer_count,
1139                        udp_mode,
1140                        check,
1141                    )
1142                    .await;
1143
1144                    log::info!(target: "citadel", "***PEER {uuid} finished all check (count: {})s***", results.len());
1145                    let _ = client_success.fetch_add(1, Ordering::Relaxed);
1146                    wait_for_peers().await;
1147                    remote.shutdown_kernel().await
1148                },
1149            );
1150
1151            let client = DefaultNodeBuilder::default()
1152                .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
1153                .build(client_kernel)?;
1154            client_kernels.push(async move { client.await.map(|_| ()) });
1155        }
1156
1157        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
1158
1159        if let Err(err) = futures::future::try_select(server, clients).await {
1160            return match err {
1161                futures::future::Either::Left(res) => Err(res.0.into_string().into()),
1162                futures::future::Either::Right(res) => Err(res.0.into_string().into()),
1163            };
1164        }
1165
1166        assert_eq!(client_success.load(Ordering::Relaxed), peer_count);
1167        Ok(())
1168    }
1169
1170    #[rstest]
1171    #[case(2)]
1172    #[timeout(std::time::Duration::from_secs(90))]
1173    #[tokio::test(flavor = "multi_thread")]
1174    async fn test_peer_to_peer_disconnect(
1175        #[case] peer_count: usize,
1176    ) -> Result<(), Box<dyn std::error::Error>> {
1177        assert!(peer_count > 1);
1178        citadel_logging::setup_log();
1179        TestBarrier::setup(peer_count);
1180        let udp_mode = UdpMode::Enabled;
1181
1182        let client_success = &AtomicUsize::new(0);
1183        let (server, server_addr) = server_info::<StackedRatchet>();
1184
1185        let client_kernels = FuturesUnordered::new();
1186        let total_peers = (0..peer_count)
1187            .map(|_| Uuid::new_v4())
1188            .collect::<Vec<Uuid>>();
1189
1190        for idx in 0..peer_count {
1191            let uuid = total_peers.get(idx).cloned().unwrap();
1192            let peers = total_peers
1193                .clone()
1194                .into_iter()
1195                .filter(|r| r != &uuid)
1196                .map(UserIdentifier::from)
1197                .collect::<Vec<UserIdentifier>>();
1198
1199            let mut agg = PeerConnectionSetupAggregator::default();
1200
1201            for peer in peers {
1202                agg = agg
1203                    .with_peer_custom(peer)
1204                    .ensure_registered()
1205                    .with_udp_mode(udp_mode)
1206                    .with_session_security_settings(SessionSecuritySettings::default())
1207                    .add();
1208            }
1209
1210            let server_connection_settings =
1211                DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
1212                    .build()
1213                    .unwrap();
1214
1215            let client_kernel = PeerConnectionKernel::new(
1216                server_connection_settings,
1217                agg,
1218                move |results, remote| async move {
1219                    log::info!(target: "citadel", "***PEER {uuid} CONNECTED***");
1220                    wait_for_peers().await;
1221                    let session_cid = remote.conn_type.get_session_cid();
1222
1223                    let check = move |conn: PeerConnectSuccess<_>| async move {
1224                        conn.remote
1225                            .disconnect()
1226                            .await
1227                            .expect("Failed to p2p disconnect");
1228                        conn
1229                    };
1230                    let _ = handle_peer_connect_successes(
1231                        results,
1232                        session_cid,
1233                        peer_count,
1234                        udp_mode,
1235                        check,
1236                    )
1237                    .await;
1238                    log::info!(target: "citadel", "***PEER {uuid} finished all checks***");
1239
1240                    let _ = client_success.fetch_add(1, Ordering::Relaxed);
1241                    wait_for_peers().await;
1242                    remote.shutdown_kernel().await
1243                },
1244            );
1245
1246            let client = DefaultNodeBuilder::default()
1247                .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
1248                .build(client_kernel)?;
1249            client_kernels.push(async move { client.await.map(|_| ()) });
1250        }
1251
1252        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
1253
1254        if let Err(err) = futures::future::try_select(server, clients).await {
1255            return match err {
1256                futures::future::Either::Left(res) => Err(res.0.into_string().into()),
1257                futures::future::Either::Right(res) => Err(res.0.into_string().into()),
1258            };
1259        }
1260
1261        assert_eq!(client_success.load(Ordering::Relaxed), peer_count);
1262        Ok(())
1263    }
1264
1265    #[rstest]
1266    #[case(SecrecyMode::BestEffort, Some("test-p2p-password"))]
1267    #[timeout(std::time::Duration::from_secs(240))]
1268    #[citadel_io::tokio::test(flavor = "multi_thread")]
1269    async fn test_p2p_wrong_session_password(
1270        #[case] secrecy_mode: SecrecyMode,
1271        #[case] p2p_password: Option<&'static str>,
1272        #[values(KemAlgorithm::MlKem)] kem: KemAlgorithm,
1273        #[values(EncryptionAlgorithm::AES_GCM_256)] enx: EncryptionAlgorithm,
1274    ) {
1275        citadel_logging::setup_log_no_panic_hook();
1276        TestBarrier::setup(2);
1277        let (server, server_addr) = server_info::<StackedRatchet>();
1278        let peer_0_error_received = &AtomicBool::new(false);
1279        let peer_1_error_received = &AtomicBool::new(false);
1280
1281        let uuid0 = Uuid::new_v4();
1282        let uuid1 = Uuid::new_v4();
1283        let session_security = SessionSecuritySettingsBuilder::default()
1284            .with_secrecy_mode(secrecy_mode)
1285            .with_crypto_params(kem + enx)
1286            .build()
1287            .unwrap();
1288
1289        let mut peer0_agg = PeerConnectionSetupAggregator::default()
1290            .with_peer_custom(uuid1)
1291            .ensure_registered()
1292            .with_session_security_settings(session_security);
1293
1294        if let Some(password) = p2p_password {
1295            peer0_agg = peer0_agg.with_session_password(password);
1296        }
1297
1298        let peer0_connection = peer0_agg.add();
1299
1300        let mut peer1_agg = PeerConnectionSetupAggregator::default()
1301            .with_peer_custom(uuid0)
1302            .ensure_registered()
1303            .with_session_security_settings(session_security);
1304
1305        if let Some(_password) = p2p_password {
1306            peer1_agg = peer1_agg.with_session_password("wrong password");
1307        }
1308
1309        let peer1_connection = peer1_agg.add();
1310
1311        let server_connection_settings0 =
1312            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid0)
1313                .with_udp_mode(UdpMode::Enabled)
1314                .with_session_security_settings(session_security)
1315                .build()
1316                .unwrap();
1317
1318        let server_connection_settings1 =
1319            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid1)
1320                .with_udp_mode(UdpMode::Enabled)
1321                .with_session_security_settings(session_security)
1322                .build()
1323                .unwrap();
1324
1325        let client_kernel0 = PeerConnectionKernel::new(
1326            server_connection_settings0,
1327            peer0_connection,
1328            move |mut connection, remote| async move {
1329                wait_for_peers().await;
1330                let conn = connection.recv().await.unwrap();
1331                log::trace!(target: "citadel", "Peer 0 {} received: {:?}", remote.conn_type.get_session_cid(), conn);
1332                if conn.is_ok() {
1333                    peer_0_error_received.store(true, Ordering::SeqCst);
1334                }
1335                wait_for_peers().await;
1336                remote.shutdown_kernel().await
1337            },
1338        );
1339
1340        let client_kernel1 = PeerConnectionKernel::new(
1341            server_connection_settings1,
1342            peer1_connection,
1343            move |mut connection, remote| async move {
1344                wait_for_peers().await;
1345                let conn = connection.recv().await.unwrap();
1346                log::trace!(target: "citadel", "Peer 1 {} received: {:?}", remote.conn_type.get_session_cid(), conn);
1347                if conn.is_ok() {
1348                    peer_1_error_received.store(true, Ordering::SeqCst);
1349                }
1350                wait_for_peers().await;
1351                remote.shutdown_kernel().await
1352            },
1353        );
1354
1355        let client0 = DefaultNodeBuilder::default()
1356            .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
1357            .build(client_kernel0)
1358            .unwrap();
1359        let client1 = DefaultNodeBuilder::default()
1360            .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
1361            .build(client_kernel1)
1362            .unwrap();
1363        let clients = futures::future::try_join(client0, client1);
1364
1365        let task = async move {
1366            tokio::select! {
1367                server_res = server => Err(citadel_io::error!(citadel_io::ErrorCode::PrefabServerEndedPrematurely, citadel_io::Dbg(server_res.map(|_| ())))),
1368                client_res = clients => client_res.map(|_| ())
1369            }
1370        };
1371
1372        tokio::time::timeout(Duration::from_secs(120), task)
1373            .await
1374            .unwrap()
1375            .unwrap();
1376
1377        assert!(!peer_0_error_received.load(Ordering::SeqCst));
1378        assert!(!peer_1_error_received.load(Ordering::SeqCst));
1379    }
1380
1381    async fn handle_peer_connect_successes<F, Fut, R: Ratchet>(
1382        mut conn_rx: Receiver<Result<PeerConnectSuccess<R>, NetworkError>>,
1383        session_cid: u64,
1384        peer_count: usize,
1385        udp_mode: UdpMode,
1386        checks: F,
1387    ) -> Vec<PeerConnectSuccess<R>>
1388    where
1389        F: Fn(PeerConnectSuccess<R>) -> Fut + Send + Clone + 'static,
1390        Fut: Future<Output = PeerConnectSuccess<R>> + Send,
1391    {
1392        let (finished_tx, finished_rx) = tokio::sync::oneshot::channel();
1393
1394        let task = async move {
1395            let (done_tx, mut done_rx) = tokio::sync::mpsc::unbounded_channel();
1396            let mut conns = vec![];
1397            while let Some(conn) = conn_rx.recv().await {
1398                conns.push(conn);
1399                if conns.len() == peer_count - 1 {
1400                    break;
1401                }
1402            }
1403
1404            log::info!(target: "citadel", "~~~*** Peer {session_cid} has {} connections to other peers ***~~~", conns.len());
1405
1406            for conn in conns {
1407                let conn = conn.expect("Error receiving peer connection");
1408                handle_peer_connect_success(
1409                    conn,
1410                    done_tx.clone(),
1411                    session_cid,
1412                    udp_mode,
1413                    checks.clone(),
1414                );
1415            }
1416
1417            // Now, wait for all to finish
1418            let mut ret = vec![];
1419            while let Some(done) = done_rx.recv().await {
1420                ret.push(done);
1421                if ret.len() == peer_count - 1 {
1422                    break;
1423                }
1424            }
1425
1426            finished_tx
1427                .send(ret)
1428                .expect("Error sending finished signal in handle_peer_connect_successes");
1429        };
1430
1431        drop(tokio::task::spawn(task));
1432        let ret = finished_rx
1433            .await
1434            .expect("Error receiving finished signal in handle_peer_connect_successes");
1435
1436        assert_eq!(ret.len(), peer_count - 1);
1437        ret
1438    }
1439
1440    fn handle_peer_connect_success<F, Fut, R: Ratchet>(
1441        mut conn: PeerConnectSuccess<R>,
1442        done_tx: UnboundedSender<PeerConnectSuccess<R>>,
1443        session_cid: u64,
1444        udp_mode: UdpMode,
1445        checks: F,
1446    ) where
1447        F: Fn(PeerConnectSuccess<R>) -> Fut + Send + Clone + 'static,
1448        Fut: Future<Output = PeerConnectSuccess<R>> + Send,
1449    {
1450        let task = async move {
1451            if udp_mode == UdpMode::Enabled {
1452                // A UDP channel exists only on a P2P path, which attaches in the background.
1453                let path = conn.channel.ensure_direct().await;
1454                assert!(path.is_ok(), "no direct path: {path:?}");
1455            }
1456            let chan = conn.udp_channel_rx.take();
1457            crate::test_common::p2p_assertions(session_cid, &conn).await;
1458            crate::test_common::udp_mode_assertions(udp_mode, chan).await;
1459            let conn = checks(conn).await;
1460            done_tx
1461                .send(conn)
1462                .expect("Error sending done signal in handle_peer_connect_success");
1463        };
1464
1465        drop(tokio::task::spawn(task));
1466    }
1467}