Skip to content

Commit 6340b59

Browse files
committed
fix(p2p): cap concurrent incoming connections, globally and per peer
Bounds total incoming connections and additionally caps how many may come from a single participant, so one peer can't starve the node via a connection flood.
1 parent 00c51b9 commit 6340b59

4 files changed

Lines changed: 93 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ near-token = "0.3.4"
199199
near-workspaces = { version = "0.23.0" }
200200
num_enum = { version = "0.7.6", features = ["complex-expressions"] }
201201
pairing = { version = "0.23.0" }
202+
parking_lot = { version = "0.12.5" }
202203
pprof = { version = "0.15.0", features = [
203204
"cpp",
204205
"flamegraph",

crates/node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ near-store = { workspace = true }
6565
near-time = { workspace = true }
6666
node-types = { workspace = true }
6767
num_enum = { workspace = true }
68+
parking_lot = { workspace = true }
6869
pprof = { workspace = true }
6970
prometheus = { workspace = true }
7071
rand = { workspace = true }

crates/node/src/p2p.rs

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
6969
const 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.
7377
pub 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)]
88133
struct 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)]
10651141
mod 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

Comments
 (0)