@@ -68,6 +68,10 @@ const WRITE_OPERATION_TIMEOUT: std::time::Duration = std::time::Duration::from_s
6868/// never sends a ClientHello would hang the accept task forever.
6969const TLS_ACCEPT_TIMEOUT : std:: time:: Duration = std:: time:: Duration :: from_secs ( 10 ) ;
7070
71+ /// Caps how many incoming connections may be from the same participant at once.
72+ const MAX_CONCURRENT_CONNECTIONS_PER_PARTICIPANT : usize = 4 ;
73+ const CONCURRENT_CONNECTIONS_HEADROOM : usize = 16 ;
74+
7175/// Implements MeshNetworkTransportSender for sending messages over a TLS-based
7276/// mesh network.
7377pub struct TlsMeshSender {
@@ -83,6 +87,47 @@ pub struct TlsMeshReceiver {
8387 _incoming_connections_task : AutoAbortTask < ( ) > ,
8488}
8589
90+ /// Tracks how many incoming connections are currently from each participant
91+ #[ derive( Default ) ]
92+ struct PerParticipantConnectionSlots {
93+ counts : parking_lot:: Mutex < HashMap < ParticipantId , usize > > ,
94+ }
95+
96+ impl PerParticipantConnectionSlots {
97+ fn try_reserve ( self : & Arc < Self > , peer_id : ParticipantId ) -> Option < ParticipantSlotGuard > {
98+ let mut counts = self . counts . lock ( ) ;
99+ let count = counts. entry ( peer_id) . or_insert ( 0 ) ;
100+ if * count >= MAX_CONCURRENT_CONNECTIONS_PER_PARTICIPANT {
101+ return None ;
102+ }
103+ * count += 1 ;
104+ Some ( ParticipantSlotGuard {
105+ slots : self . clone ( ) ,
106+ peer_id,
107+ } )
108+ }
109+ }
110+
111+ /// Guard for a reserved slot in [`PerParticipantConnectionSlots`].
112+ /// Releases the slot when dropped, regardless of how the connection handler
113+ /// exits.
114+ struct ParticipantSlotGuard {
115+ slots : Arc < PerParticipantConnectionSlots > ,
116+ peer_id : ParticipantId ,
117+ }
118+
119+ impl Drop for ParticipantSlotGuard {
120+ fn drop ( & mut self ) {
121+ let mut counts = self . slots . counts . lock ( ) ;
122+ if let Some ( count) = counts. get_mut ( & self . peer_id ) {
123+ * count = count. saturating_sub ( 1 ) ;
124+ if * count == 0 {
125+ counts. remove ( & self . peer_id ) ;
126+ }
127+ }
128+ }
129+ }
130+
86131/// Maps public keys to participant IDs. Used to identify incoming connections.
87132#[ derive( Default ) ]
88133struct ParticipantIdentities {
@@ -573,6 +618,14 @@ where
573618
574619 let tls_acceptor = TlsAcceptor :: from ( Arc :: new ( server_config) ) ;
575620
621+ let max_concurrent_incoming_connections = config. participants . participants . len ( )
622+ * MAX_CONCURRENT_CONNECTIONS_PER_PARTICIPANT
623+ + CONCURRENT_CONNECTIONS_HEADROOM ;
624+ let connection_limiter = Arc :: new ( tokio:: sync:: Semaphore :: new (
625+ max_concurrent_incoming_connections,
626+ ) ) ;
627+ let participant_slots = Arc :: new ( PerParticipantConnectionSlots :: default ( ) ) ;
628+
576629 let ( message_sender, message_receiver) = mpsc:: unbounded_channel ( ) ;
577630 let tcp_listener = TcpListener :: bind ( SocketAddr :: V4 ( SocketAddrV4 :: new (
578631 Ipv4Addr :: new ( 0 , 0 , 0 , 0 ) ,
@@ -597,6 +650,8 @@ where
597650 tls_acceptor. clone ( ) ,
598651 participant_identities. clone ( ) ,
599652 my_id,
653+ connection_limiter. clone ( ) ,
654+ participant_slots. clone ( ) ,
600655 ) ,
601656 ) ;
602657 } )
@@ -630,7 +685,16 @@ async fn incoming_connection_handler(
630685 tls_acceptor : TlsAcceptor ,
631686 participant_identities : Arc < ParticipantIdentities > ,
632687 my_id : ParticipantId ,
688+ connection_limiter : Arc < tokio:: sync:: Semaphore > ,
689+ participant_slots : Arc < PerParticipantConnectionSlots > ,
633690) -> anyhow:: Result < ( ) > {
691+ let Ok ( _connection_permit) = connection_limiter. try_acquire_owned ( ) else {
692+ tracing:: warn!(
693+ "dropping incoming connection: at global concurrent incoming connection limit"
694+ ) ;
695+ return Ok ( ( ) ) ;
696+ } ;
697+
634698 let tcp_stream = configure_tcp_stream ( tcp_stream) ?;
635699 let mut tls_stream = timeout ( TLS_ACCEPT_TIMEOUT , tls_acceptor. accept ( tcp_stream) )
636700 . await
@@ -639,6 +703,18 @@ async fn incoming_connection_handler(
639703 let peer_id = verify_peer_identity ( tls_stream. get_ref ( ) . 1 , & participant_identities) ?;
640704 tracking:: set_progress ( & format ! ( "Authenticated as {}" , peer_id) ) ;
641705
706+ let Some ( _participant_slot) = participant_slots. try_reserve ( peer_id) else {
707+ tracing:: warn!(
708+ peer_id = %peer_id,
709+ "dropping incoming connection: participant at MAX_CONCURRENT_CONNECTIONS_PER_PARTICIPANT ({})" ,
710+ MAX_CONCURRENT_CONNECTIONS_PER_PARTICIPANT
711+ ) ;
712+ if let Err ( err) = tls_stream. shutdown ( ) . await {
713+ tracing:: error!( err = %err, "TLS shutdown failed" ) ;
714+ }
715+ return Ok ( ( ) ) ;
716+ } ;
717+
642718 let peer = connectivities. get ( peer_id) ?;
643719 // If we have an existing connection, we require this connection attempt to
644720 // be newer than the existing connection (to avoid race conditions, c.f. github issue #1759).
@@ -1064,8 +1140,9 @@ pub mod testing {
10641140#[ expect( non_snake_case) ]
10651141mod tests {
10661142 use super :: {
1067- IncomingConnection , OutgoingConnection , ParticipantIdentities , PersistentConnection ,
1068- incoming_connection_handler,
1143+ CONCURRENT_CONNECTIONS_HEADROOM , IncomingConnection ,
1144+ MAX_CONCURRENT_CONNECTIONS_PER_PARTICIPANT , OutgoingConnection , ParticipantIdentities ,
1145+ PerParticipantConnectionSlots , PersistentConnection , incoming_connection_handler,
10691146 } ;
10701147 use crate :: config:: MpcConfig ;
10711148 use crate :: network:: conn:: { AllNodeConnectivities , ConnectionVersion } ;
@@ -1082,7 +1159,7 @@ mod tests {
10821159 use rand:: rngs:: StdRng ;
10831160 use rand:: { Rng , SeedableRng } ;
10841161 use rustls:: ClientConfig ;
1085- use std:: sync:: { Arc , Mutex } ;
1162+ use std:: sync:: Arc ;
10861163 use std:: time:: Duration ;
10871164 use tokio:: net:: { TcpListener , TcpStream } ;
10881165 use tokio:: sync:: mpsc;
@@ -1569,7 +1646,7 @@ mod tests {
15691646 let client_config = must_make_client_config ( ) ;
15701647 let my_id = ParticipantId :: from_raw ( 0 ) ;
15711648 let target_id = ParticipantId :: from_raw ( 1 ) ;
1572- let resolved_address = Arc :: new ( Mutex :: new ( addr_a. clone ( ) ) ) ;
1649+ let resolved_address = Arc :: new ( parking_lot :: Mutex :: new ( addr_a. clone ( ) ) ) ;
15731650
15741651 start_root_task_with_periodic_dump ( async move {
15751652 let connectivities =
@@ -1579,7 +1656,7 @@ mod tests {
15791656 ) ;
15801657 let resolve_address = {
15811658 let resolved_address = resolved_address. clone ( ) ;
1582- move || Some ( resolved_address. lock ( ) . unwrap ( ) . clone ( ) )
1659+ move || Some ( resolved_address. lock ( ) . clone ( ) )
15831660 } ;
15841661
15851662 let _connection = PersistentConnection :: new (
@@ -1599,7 +1676,7 @@ mod tests {
15991676 . unwrap ( ) ;
16001677
16011678 // When
1602- * resolved_address. lock ( ) . unwrap ( ) = addr_b. clone ( ) ;
1679+ * resolved_address. lock ( ) = addr_b. clone ( ) ;
16031680
16041681 // Then
16051682 timeout ( Duration :: from_secs ( 120 ) , accept_b. recv ( ) )
@@ -1626,6 +1703,9 @@ mod tests {
16261703 IncomingConnection ,
16271704 > :: new ( my_id, & [ my_id] ) ) ;
16281705
1706+ let max_concurrent_incoming_connections =
1707+ 1 * MAX_CONCURRENT_CONNECTIONS_PER_PARTICIPANT + CONCURRENT_CONNECTIONS_HEADROOM ;
1708+
16291709 // When
16301710 let result = timeout (
16311711 Duration :: from_secs ( 60 ) ,
@@ -1636,6 +1716,10 @@ mod tests {
16361716 tls_acceptor,
16371717 Arc :: new ( ParticipantIdentities :: default ( ) ) ,
16381718 my_id,
1719+ Arc :: new ( tokio:: sync:: Semaphore :: new (
1720+ max_concurrent_incoming_connections,
1721+ ) ) ,
1722+ Arc :: new ( PerParticipantConnectionSlots :: default ( ) ) ,
16391723 ) ,
16401724 )
16411725 . await
0 commit comments