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().build(client_kernel).unwrap();
737            client_kernels.push(async move { client.await.map(|_| ()) });
738        }
739
740        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
741
742        assert!(futures::future::try_select(server, clients).await.is_ok());
743
744        assert_eq!(client_success.load(Ordering::Relaxed), peer_count);
745    }
746
747    #[rstest]
748    #[case(2, HeaderObfuscatorSettings::default())]
749    #[case(2, HeaderObfuscatorSettings::Enabled)]
750    #[case(2, HeaderObfuscatorSettings::EnabledWithKey(12345))]
751    #[case(3, HeaderObfuscatorSettings::default())]
752    #[timeout(Duration::from_secs(90))]
753    #[tokio::test(flavor = "multi_thread")]
754    async fn peer_to_peer_connect_transient(
755        #[case] peer_count: usize,
756        #[case] header_obfuscator_settings: HeaderObfuscatorSettings,
757    ) -> Result<(), Box<dyn std::error::Error>> {
758        assert!(peer_count > 1);
759        citadel_logging::setup_log();
760        TestBarrier::setup(peer_count);
761        let udp_mode = UdpMode::Enabled;
762
763        let do_deregister = peer_count == 2;
764
765        let client_success = &AtomicUsize::new(0);
766        let (server, server_addr) = server_info::<StackedRatchet>();
767
768        let client_kernels = FuturesUnordered::new();
769        let total_peers = (0..peer_count)
770            .map(|_| Uuid::new_v4())
771            .collect::<Vec<Uuid>>();
772
773        for idx in 0..peer_count {
774            let uuid = total_peers.get(idx).cloned().unwrap();
775            let peers = total_peers
776                .clone()
777                .into_iter()
778                .filter(|r| r != &uuid)
779                .map(UserIdentifier::from)
780                .collect::<Vec<UserIdentifier>>();
781
782            let mut agg = PeerConnectionSetupAggregator::default();
783
784            for peer in peers {
785                let security_settings = SessionSecuritySettings {
786                    header_obfuscator_settings,
787                    ..Default::default()
788                };
789                agg = agg
790                    .with_peer_custom(peer)
791                    .with_udp_mode(udp_mode)
792                    .ensure_registered()
793                    .with_session_security_settings(security_settings)
794                    .add();
795            }
796
797            let server_connection_settings =
798                DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
799                    .build()
800                    .unwrap();
801
802            let client_kernel = PeerConnectionKernel::new(
803                server_connection_settings,
804                agg,
805                move |results, remote| async move {
806                    log::info!(target: "citadel", "***PEER {uuid} CONNECTED***");
807                    let session_cid = remote.conn_type.get_session_cid();
808
809                    let check = move |conn: PeerConnectSuccess<_>| async move {
810                        if do_deregister {
811                            conn.remote
812                                .deregister()
813                                .await
814                                .expect("Deregistration failed");
815                            assert!(!conn
816                                .remote
817                                .inner
818                                .account_manager()
819                                .get_persistence_handler()
820                                .hyperlan_peer_exists(session_cid, conn.channel.get_peer_cid())
821                                .await
822                                .unwrap());
823                        }
824                        conn
825                    };
826
827                    let _ = handle_peer_connect_successes(
828                        results,
829                        session_cid,
830                        peer_count,
831                        udp_mode,
832                        check,
833                    )
834                    .await;
835
836                    log::info!(target: "citadel", "***PEER {uuid} finished all checks***");
837                    let _ = client_success.fetch_add(1, Ordering::Relaxed);
838                    wait_for_peers().await;
839                    remote.shutdown_kernel().await
840                },
841            );
842
843            let client = DefaultNodeBuilder::default().build(client_kernel)?;
844            client_kernels.push(async move { client.await.map(|_| ()) });
845        }
846
847        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
848
849        if let Err(err) = futures::future::try_select(server, clients).await {
850            return match err {
851                futures::future::Either::Left(res) => Err(res.0.into_string().into()),
852                futures::future::Either::Right(res) => Err(res.0.into_string().into()),
853            };
854        }
855
856        assert_eq!(client_success.load(Ordering::Relaxed), peer_count);
857        Ok(())
858    }
859
860    #[rstest]
861    #[case(2)]
862    #[case(3)]
863    #[timeout(std::time::Duration::from_secs(180))]
864    #[tokio::test(flavor = "multi_thread")]
865    async fn test_peer_to_peer_file_transfer(
866        #[case] peer_count: usize,
867    ) -> Result<(), Box<dyn std::error::Error>> {
868        assert!(peer_count > 1);
869        citadel_logging::setup_log();
870        TestBarrier::setup(peer_count);
871        let udp_mode = UdpMode::Enabled;
872
873        let sender_success = &AtomicBool::new(false);
874        let receiver_success = &AtomicBool::new(false);
875
876        let (server, server_addr) = server_info::<StackedRatchet>();
877
878        let client_kernels = FuturesUnordered::new();
879        let total_peers = (0..peer_count)
880            .map(|_| Uuid::new_v4())
881            .collect::<Vec<Uuid>>();
882
883        let sender_uuid = total_peers[0];
884
885        for idx in 0..peer_count {
886            let uuid = total_peers.get(idx).cloned().unwrap();
887            let mut peers = total_peers
888                .clone()
889                .into_iter()
890                .filter(|r| r != &uuid)
891                .map(UserIdentifier::from)
892                .collect::<Vec<UserIdentifier>>();
893            // 0: [1, 2] <-- At idx 0, we want the sender to connect to all other peers
894            // 1: [0] <-- At idx 1, we want the receiver to connect to the sender
895            // 2: [0] <-- At idx 2, we want the receiver to connect to the sender
896            // ..
897            // n: [0] <-- At idx n, we want the receiver to connect to the sender
898            if idx != 0 {
899                peers = vec![sender_uuid.into()];
900            }
901
902            let mut agg = PeerConnectionSetupAggregator::default();
903
904            for peer in peers {
905                agg = agg
906                    .with_peer_custom(peer)
907                    .ensure_registered()
908                    .with_udp_mode(udp_mode)
909                    .with_session_security_settings(SessionSecuritySettings::default())
910                    .add();
911            }
912
913            let server_connection_settings =
914                DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
915                    .build()
916                    .unwrap();
917
918            let client_kernel = PeerConnectionKernel::new(
919                server_connection_settings,
920                agg,
921                move |results, remote| async move {
922                    log::info!(target: "citadel", "***PEER {uuid} CONNECTED***");
923                    wait_for_peers().await;
924                    let session_cid = remote.conn_type.get_session_cid();
925                    let is_sender = idx == 0; // the first peer is the sender, the rest are receivers
926                    let check = move |mut conn: PeerConnectSuccess<_>| async move {
927                        if is_sender {
928                            conn.remote
929                                .send_file_with_custom_opts(
930                                    "../resources/TheBridge.pdf",
931                                    32 * 1024,
932                                    TransferType::FileTransfer,
933                                )
934                                .await
935                                .expect("Failed to send file");
936                        } else {
937                            // TODO: route file-transfer + other events to peer channel
938                            // Bounded, so a failure names the stage it stopped at.
939                            //
940                            // `case_2` times out at the rstest budget of 180s on
941                            // macOS, intermittently, and the report is the bare
942                            // "Timeout 180s expired" — which cannot say whether
943                            // the handle never arrived, or arrived and the
944                            // transfer then stalled part way. Those are different
945                            // bugs. Two other intermittent failures in this suite
946                            // were solved this session by exactly this move, and
947                            // neither yielded to reading the code.
948                            //
949                            // The budgets are generous fractions of the 180s the
950                            // test already allows, so this cannot turn a slow
951                            // machine into a failure it would not otherwise have
952                            // had — it only replaces silence with a sentence.
953                            const HANDLE_BUDGET: std::time::Duration =
954                                std::time::Duration::from_secs(60);
955                            const STATUS_BUDGET: std::time::Duration =
956                                std::time::Duration::from_secs(60);
957
958                            let mut handles = conn.incoming_object_transfer_handles.take().unwrap();
959                            let mut handle = match citadel_io::tokio::time::timeout(
960                                HANDLE_BUDGET,
961                                handles.recv(),
962                            )
963                            .await
964                            {
965                                Ok(Some(handle)) => handle,
966                                Ok(None) => panic!(
967                                    "the incoming-transfer channel closed before any handle arrived"
968                                ),
969                                Err(_) => panic!(
970                                    "no incoming file-transfer handle within {HANDLE_BUDGET:?}: the sender never started, or its request never routed here"
971                                ),
972                            };
973                            handle.accept().unwrap();
974
975                            use citadel_types::proto::ObjectTransferStatus;
976                            use futures::StreamExt;
977                            let mut path = None;
978                            while let Some(status) = match citadel_io::tokio::time::timeout(
979                                STATUS_BUDGET,
980                                handle.next(),
981                            )
982                            .await
983                            {
984                                Ok(status) => status,
985                                Err(_) => panic!(
986                                    "the transfer stalled: no status within {STATUS_BUDGET:?}, last seen {path:?}"
987                                ),
988                            } {
989                                match status {
990                                    ObjectTransferStatus::ReceptionComplete => {
991                                        let cmp =
992                                            include_bytes!("../../../../resources/TheBridge.pdf");
993                                        let streamed_data =
994                                            citadel_io::tokio::fs::read(path.clone().unwrap())
995                                                .await
996                                                .unwrap();
997                                        assert_eq!(
998                                            cmp,
999                                            streamed_data.as_slice(),
1000                                            "Original data and streamed data does not match"
1001                                        );
1002
1003                                        log::info!(target: "citadel", "Peer has finished receiving and verifying the file!");
1004                                        break;
1005                                    }
1006
1007                                    ObjectTransferStatus::ReceptionBeginning(file_path, vfm) => {
1008                                        path = Some(file_path);
1009                                        assert_eq!(vfm.name, "TheBridge.pdf")
1010                                    }
1011
1012                                    _ => {}
1013                                }
1014                            }
1015                        }
1016
1017                        conn
1018                    };
1019                    // Use a peer count of two since we only have one sender and one receiver per pair
1020                    // However, we need a way of ensuring we collect three results
1021                    let peer_count = if idx == 0 { peer_count } else { 2 };
1022                    let _ = handle_peer_connect_successes(
1023                        results,
1024                        session_cid,
1025                        peer_count,
1026                        udp_mode,
1027                        check,
1028                    )
1029                    .await;
1030
1031                    if is_sender {
1032                        sender_success.store(true, Ordering::Relaxed);
1033                    } else {
1034                        receiver_success.store(true, Ordering::Relaxed);
1035                    }
1036
1037                    log::info!(target: "citadel", "***PEER {uuid} (is_sender: {is_sender}) finished all checks***");
1038                    wait_for_peers().await;
1039                    log::info!(target: "citadel", "***PEER {uuid} (is_sender: {is_sender}) shutting down***");
1040                    remote.shutdown_kernel().await
1041                },
1042            );
1043
1044            let client = DefaultNodeBuilder::default().build(client_kernel).unwrap();
1045            client_kernels.push(async move { client.await.map(|_| ()) });
1046        }
1047
1048        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
1049
1050        if let Err(err) = futures::future::try_select(server, clients).await {
1051            return match err {
1052                futures::future::Either::Left(res) => Err(res.0.into_string().into()),
1053                futures::future::Either::Right(res) => Err(res.0.into_string().into()),
1054            };
1055        }
1056
1057        assert!(sender_success.load(Ordering::Relaxed));
1058        assert!(receiver_success.load(Ordering::Relaxed));
1059        Ok(())
1060    }
1061
1062    #[rstest]
1063    #[case(2)]
1064    #[timeout(std::time::Duration::from_secs(90))]
1065    #[tokio::test(flavor = "multi_thread")]
1066    async fn test_peer_to_peer_rekey(
1067        #[case] peer_count: usize,
1068    ) -> Result<(), Box<dyn std::error::Error>> {
1069        assert!(peer_count > 1);
1070        citadel_logging::setup_log();
1071        TestBarrier::setup(peer_count);
1072        let udp_mode = UdpMode::Enabled;
1073
1074        let client_success = &AtomicUsize::new(0);
1075        let (server, server_addr) = server_info::<StackedRatchet>();
1076
1077        let client_kernels = FuturesUnordered::new();
1078        let total_peers = (0..peer_count)
1079            .map(|_| Uuid::new_v4())
1080            .collect::<Vec<Uuid>>();
1081
1082        for idx in 0..peer_count {
1083            let uuid = total_peers.get(idx).cloned().unwrap();
1084            let peers = total_peers
1085                .clone()
1086                .into_iter()
1087                .filter(|r| r != &uuid)
1088                .map(UserIdentifier::from)
1089                .collect::<Vec<UserIdentifier>>();
1090
1091            let mut agg = PeerConnectionSetupAggregator::default();
1092
1093            for peer in peers {
1094                agg = agg
1095                    .with_peer_custom(peer)
1096                    .ensure_registered()
1097                    .with_udp_mode(udp_mode)
1098                    .with_session_security_settings(SessionSecuritySettings::default())
1099                    .add();
1100            }
1101
1102            let server_connection_settings =
1103                DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
1104                    .build()
1105                    .unwrap();
1106
1107            let client_kernel = PeerConnectionKernel::new(
1108                server_connection_settings,
1109                agg,
1110                move |results, remote| async move {
1111                    log::info!(target: "citadel", "***PEER {uuid} CONNECTED***");
1112                    let session_cid = remote.conn_type.get_session_cid();
1113
1114                    let check = move |conn: PeerConnectSuccess<_>| async move {
1115                        if idx == 0 {
1116                            for x in 1..10 {
1117                                assert_eq!(
1118                                    conn.remote.rekey().await.expect("Failed to rekey"),
1119                                    Some(x)
1120                                );
1121                            }
1122                        }
1123
1124                        conn
1125                    };
1126
1127                    let results = handle_peer_connect_successes(
1128                        results,
1129                        session_cid,
1130                        peer_count,
1131                        udp_mode,
1132                        check,
1133                    )
1134                    .await;
1135
1136                    log::info!(target: "citadel", "***PEER {uuid} finished all check (count: {})s***", results.len());
1137                    let _ = client_success.fetch_add(1, Ordering::Relaxed);
1138                    wait_for_peers().await;
1139                    remote.shutdown_kernel().await
1140                },
1141            );
1142
1143            let client = DefaultNodeBuilder::default().build(client_kernel)?;
1144            client_kernels.push(async move { client.await.map(|_| ()) });
1145        }
1146
1147        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
1148
1149        if let Err(err) = futures::future::try_select(server, clients).await {
1150            return match err {
1151                futures::future::Either::Left(res) => Err(res.0.into_string().into()),
1152                futures::future::Either::Right(res) => Err(res.0.into_string().into()),
1153            };
1154        }
1155
1156        assert_eq!(client_success.load(Ordering::Relaxed), peer_count);
1157        Ok(())
1158    }
1159
1160    #[rstest]
1161    #[case(2)]
1162    #[timeout(std::time::Duration::from_secs(90))]
1163    #[tokio::test(flavor = "multi_thread")]
1164    async fn test_peer_to_peer_disconnect(
1165        #[case] peer_count: usize,
1166    ) -> Result<(), Box<dyn std::error::Error>> {
1167        assert!(peer_count > 1);
1168        citadel_logging::setup_log();
1169        TestBarrier::setup(peer_count);
1170        let udp_mode = UdpMode::Enabled;
1171
1172        let client_success = &AtomicUsize::new(0);
1173        let (server, server_addr) = server_info::<StackedRatchet>();
1174
1175        let client_kernels = FuturesUnordered::new();
1176        let total_peers = (0..peer_count)
1177            .map(|_| Uuid::new_v4())
1178            .collect::<Vec<Uuid>>();
1179
1180        for idx in 0..peer_count {
1181            let uuid = total_peers.get(idx).cloned().unwrap();
1182            let peers = total_peers
1183                .clone()
1184                .into_iter()
1185                .filter(|r| r != &uuid)
1186                .map(UserIdentifier::from)
1187                .collect::<Vec<UserIdentifier>>();
1188
1189            let mut agg = PeerConnectionSetupAggregator::default();
1190
1191            for peer in peers {
1192                agg = agg
1193                    .with_peer_custom(peer)
1194                    .ensure_registered()
1195                    .with_udp_mode(udp_mode)
1196                    .with_session_security_settings(SessionSecuritySettings::default())
1197                    .add();
1198            }
1199
1200            let server_connection_settings =
1201                DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
1202                    .build()
1203                    .unwrap();
1204
1205            let client_kernel = PeerConnectionKernel::new(
1206                server_connection_settings,
1207                agg,
1208                move |results, remote| async move {
1209                    log::info!(target: "citadel", "***PEER {uuid} CONNECTED***");
1210                    wait_for_peers().await;
1211                    let session_cid = remote.conn_type.get_session_cid();
1212
1213                    let check = move |conn: PeerConnectSuccess<_>| async move {
1214                        conn.remote
1215                            .disconnect()
1216                            .await
1217                            .expect("Failed to p2p disconnect");
1218                        conn
1219                    };
1220                    let _ = handle_peer_connect_successes(
1221                        results,
1222                        session_cid,
1223                        peer_count,
1224                        udp_mode,
1225                        check,
1226                    )
1227                    .await;
1228                    log::info!(target: "citadel", "***PEER {uuid} finished all checks***");
1229
1230                    let _ = client_success.fetch_add(1, Ordering::Relaxed);
1231                    wait_for_peers().await;
1232                    remote.shutdown_kernel().await
1233                },
1234            );
1235
1236            let client = DefaultNodeBuilder::default().build(client_kernel)?;
1237            client_kernels.push(async move { client.await.map(|_| ()) });
1238        }
1239
1240        let clients = Box::pin(async move { client_kernels.try_collect::<()>().await.map(|_| ()) });
1241
1242        if let Err(err) = futures::future::try_select(server, clients).await {
1243            return match err {
1244                futures::future::Either::Left(res) => Err(res.0.into_string().into()),
1245                futures::future::Either::Right(res) => Err(res.0.into_string().into()),
1246            };
1247        }
1248
1249        assert_eq!(client_success.load(Ordering::Relaxed), peer_count);
1250        Ok(())
1251    }
1252
1253    #[rstest]
1254    #[case(SecrecyMode::BestEffort, Some("test-p2p-password"))]
1255    #[timeout(std::time::Duration::from_secs(240))]
1256    #[citadel_io::tokio::test(flavor = "multi_thread")]
1257    async fn test_p2p_wrong_session_password(
1258        #[case] secrecy_mode: SecrecyMode,
1259        #[case] p2p_password: Option<&'static str>,
1260        #[values(KemAlgorithm::MlKem)] kem: KemAlgorithm,
1261        #[values(EncryptionAlgorithm::AES_GCM_256)] enx: EncryptionAlgorithm,
1262    ) {
1263        citadel_logging::setup_log_no_panic_hook();
1264        TestBarrier::setup(2);
1265        let (server, server_addr) = server_info::<StackedRatchet>();
1266        let peer_0_error_received = &AtomicBool::new(false);
1267        let peer_1_error_received = &AtomicBool::new(false);
1268
1269        let uuid0 = Uuid::new_v4();
1270        let uuid1 = Uuid::new_v4();
1271        let session_security = SessionSecuritySettingsBuilder::default()
1272            .with_secrecy_mode(secrecy_mode)
1273            .with_crypto_params(kem + enx)
1274            .build()
1275            .unwrap();
1276
1277        let mut peer0_agg = PeerConnectionSetupAggregator::default()
1278            .with_peer_custom(uuid1)
1279            .ensure_registered()
1280            .with_session_security_settings(session_security);
1281
1282        if let Some(password) = p2p_password {
1283            peer0_agg = peer0_agg.with_session_password(password);
1284        }
1285
1286        let peer0_connection = peer0_agg.add();
1287
1288        let mut peer1_agg = PeerConnectionSetupAggregator::default()
1289            .with_peer_custom(uuid0)
1290            .ensure_registered()
1291            .with_session_security_settings(session_security);
1292
1293        if let Some(_password) = p2p_password {
1294            peer1_agg = peer1_agg.with_session_password("wrong password");
1295        }
1296
1297        let peer1_connection = peer1_agg.add();
1298
1299        let server_connection_settings0 =
1300            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid0)
1301                .with_udp_mode(UdpMode::Enabled)
1302                .with_session_security_settings(session_security)
1303                .build()
1304                .unwrap();
1305
1306        let server_connection_settings1 =
1307            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid1)
1308                .with_udp_mode(UdpMode::Enabled)
1309                .with_session_security_settings(session_security)
1310                .build()
1311                .unwrap();
1312
1313        let client_kernel0 = PeerConnectionKernel::new(
1314            server_connection_settings0,
1315            peer0_connection,
1316            move |mut connection, remote| async move {
1317                wait_for_peers().await;
1318                let conn = connection.recv().await.unwrap();
1319                log::trace!(target: "citadel", "Peer 0 {} received: {:?}", remote.conn_type.get_session_cid(), conn);
1320                if conn.is_ok() {
1321                    peer_0_error_received.store(true, Ordering::SeqCst);
1322                }
1323                wait_for_peers().await;
1324                remote.shutdown_kernel().await
1325            },
1326        );
1327
1328        let client_kernel1 = PeerConnectionKernel::new(
1329            server_connection_settings1,
1330            peer1_connection,
1331            move |mut connection, remote| async move {
1332                wait_for_peers().await;
1333                let conn = connection.recv().await.unwrap();
1334                log::trace!(target: "citadel", "Peer 1 {} received: {:?}", remote.conn_type.get_session_cid(), conn);
1335                if conn.is_ok() {
1336                    peer_1_error_received.store(true, Ordering::SeqCst);
1337                }
1338                wait_for_peers().await;
1339                remote.shutdown_kernel().await
1340            },
1341        );
1342
1343        let client0 = DefaultNodeBuilder::default().build(client_kernel0).unwrap();
1344        let client1 = DefaultNodeBuilder::default().build(client_kernel1).unwrap();
1345        let clients = futures::future::try_join(client0, client1);
1346
1347        let task = async move {
1348            tokio::select! {
1349                server_res = server => Err(citadel_io::error!(citadel_io::ErrorCode::PrefabServerEndedPrematurely, citadel_io::Dbg(server_res.map(|_| ())))),
1350                client_res = clients => client_res.map(|_| ())
1351            }
1352        };
1353
1354        tokio::time::timeout(Duration::from_secs(120), task)
1355            .await
1356            .unwrap()
1357            .unwrap();
1358
1359        assert!(!peer_0_error_received.load(Ordering::SeqCst));
1360        assert!(!peer_1_error_received.load(Ordering::SeqCst));
1361    }
1362
1363    async fn handle_peer_connect_successes<F, Fut, R: Ratchet>(
1364        mut conn_rx: Receiver<Result<PeerConnectSuccess<R>, NetworkError>>,
1365        session_cid: u64,
1366        peer_count: usize,
1367        udp_mode: UdpMode,
1368        checks: F,
1369    ) -> Vec<PeerConnectSuccess<R>>
1370    where
1371        F: Fn(PeerConnectSuccess<R>) -> Fut + Send + Clone + 'static,
1372        Fut: Future<Output = PeerConnectSuccess<R>> + Send,
1373    {
1374        let (finished_tx, finished_rx) = tokio::sync::oneshot::channel();
1375
1376        let task = async move {
1377            let (done_tx, mut done_rx) = tokio::sync::mpsc::unbounded_channel();
1378            let mut conns = vec![];
1379            while let Some(conn) = conn_rx.recv().await {
1380                conns.push(conn);
1381                if conns.len() == peer_count - 1 {
1382                    break;
1383                }
1384            }
1385
1386            log::info!(target: "citadel", "~~~*** Peer {session_cid} has {} connections to other peers ***~~~", conns.len());
1387
1388            for conn in conns {
1389                let conn = conn.expect("Error receiving peer connection");
1390                handle_peer_connect_success(
1391                    conn,
1392                    done_tx.clone(),
1393                    session_cid,
1394                    udp_mode,
1395                    checks.clone(),
1396                );
1397            }
1398
1399            // Now, wait for all to finish
1400            let mut ret = vec![];
1401            while let Some(done) = done_rx.recv().await {
1402                ret.push(done);
1403                if ret.len() == peer_count - 1 {
1404                    break;
1405                }
1406            }
1407
1408            finished_tx
1409                .send(ret)
1410                .expect("Error sending finished signal in handle_peer_connect_successes");
1411        };
1412
1413        drop(tokio::task::spawn(task));
1414        let ret = finished_rx
1415            .await
1416            .expect("Error receiving finished signal in handle_peer_connect_successes");
1417
1418        assert_eq!(ret.len(), peer_count - 1);
1419        ret
1420    }
1421
1422    fn handle_peer_connect_success<F, Fut, R: Ratchet>(
1423        mut conn: PeerConnectSuccess<R>,
1424        done_tx: UnboundedSender<PeerConnectSuccess<R>>,
1425        session_cid: u64,
1426        udp_mode: UdpMode,
1427        checks: F,
1428    ) where
1429        F: Fn(PeerConnectSuccess<R>) -> Fut + Send + Clone + 'static,
1430        Fut: Future<Output = PeerConnectSuccess<R>> + Send,
1431    {
1432        let task = async move {
1433            let chan = conn.udp_channel_rx.take();
1434            crate::test_common::p2p_assertions(session_cid, &conn).await;
1435            crate::test_common::udp_mode_assertions(udp_mode, chan).await;
1436            let conn = checks(conn).await;
1437            done_tx
1438                .send(conn)
1439                .expect("Error sending done signal in handle_peer_connect_success");
1440        };
1441
1442        drop(tokio::task::spawn(task));
1443    }
1444}