Skip to content

Commit 5b36fc3

Browse files
committed
p2p: limit the number of pending inbound connections
1 parent 76df41d commit 5b36fc3

14 files changed

Lines changed: 450 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/
3535
- Adding an already existing relayable local transaction to the mempool will cause p2p to re-announce it.
3636
- Transactions are now announced in batches at irregular intervals (previously, a random delay was added
3737
before each individual transaction announcement).
38+
- Security improvements:
39+
- The number of pending inbound connections is now limited.
3840

3941
- Mempool:
4042
- Various optimizations were made.

networking/src/transport/impls/stream_adapter/wrapped_transport/wrapped_listener.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use futures::{
2121
stream::{FuturesUnordered, StreamExt},
2222
};
2323

24+
use logging::log;
25+
2426
use crate::{
2527
Result,
2628
transport::{TransportListener, TransportSocket, impls::stream_adapter::traits::StreamAdapter},
@@ -63,7 +65,10 @@ impl<S: StreamAdapter<T::Stream>, T: TransportSocket> TransportListener for Adap
6365
match handshake_res {
6466
(Ok(handshake), addr) => return Ok((handshake, addr)),
6567
(Err(err), addr) => {
66-
logging::log::warn!("Handshake with {addr} failed: {err}");
68+
// Note: failed transport handshakes are peer-triggerable. In particular,
69+
// a malicious peer can deliberately and repeatedly fail the Noise handshake,
70+
// so we keep this at debug level to avoid flooding default logs.
71+
log::debug!("Handshake with {addr} failed: {err}");
6772
continue;
6873
},
6974
}
@@ -80,7 +85,12 @@ impl<S: StreamAdapter<T::Stream>, T: TransportSocket> TransportListener for Adap
8085
self.handshakes.push(handshake_with_addr);
8186
},
8287
Err(err) => {
83-
logging::log::error!("Accept failed unexpectedly: {err}");
88+
// Note: this can also be peer-triggered, though less reliably than the
89+
// handshake error above. E.g. a malicious peer may open TCP connections
90+
// and close them immediately, attempting to flood the node's logs.
91+
// But in any case, this error is propagated to the p2p backend, which logs
92+
// it again, so there is no point in using a level higher than debug here.
93+
log::debug!("Accept failed unexpectedly: {err}");
8494
return Err(err);
8595
},
8696
}

node-lib/src/config_files/p2p.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize};
2525
use common::primitives::{semver::SemVer, user_agent::mintlayer_core_user_agent};
2626
use p2p::{
2727
ban_config::BanConfig,
28-
config::{BackendTimeoutsConfig, NodeType, P2pConfig},
28+
config::{BackendConfig, NodeType, P2pConfig},
2929
peer_manager::config::PeerManagerConfig,
3030
};
3131
use utils_networking::IpOrSocketAddress;
@@ -193,6 +193,7 @@ impl From<P2pConfigFile> for P2pConfig {
193193
peer_handshake_timeout: Default::default(),
194194
disconnection_timeout: Default::default(),
195195
socket_write_timeout: Default::default(),
196+
max_pending_inbound_connections: Default::default(),
196197
},
197198
protocol_config: Default::default(),
198199
custom_disconnection_reason_for_banning,

p2p/src/config.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ make_config_setting!(SyncStallingTimeout, Duration, Duration::from_secs(25));
3737
make_config_setting!(PeerHandshakeTimeout, Duration, Duration::from_secs(10));
3838
make_config_setting!(DisconnectionTimeout, Duration, Duration::from_secs(10));
3939
make_config_setting!(SoketWriteTimeout, Duration, Duration::from_secs(60));
40+
make_config_setting!(MaxPendingInboundConnections, usize, 100);
4041

4142
/// A node type.
4243
#[derive(Debug, Copy, Clone)]
@@ -144,12 +145,21 @@ pub struct BackendConfig {
144145
/// The outbound connection timeout value.
145146
pub outbound_connection_timeout: OutboundConnectionTimeout,
146147

147-
/// Timeout for initial peer handshake
148+
/// Timeout for initial peer handshake.
148149
pub peer_handshake_timeout: PeerHandshakeTimeout,
149150

150-
/// Timeout for disconnection
151+
/// Timeout for disconnection.
151152
pub disconnection_timeout: DisconnectionTimeout,
152153

153-
/// Timeout for the socket write call
154+
/// Timeout for the socket write call.
154155
pub socket_write_timeout: SoketWriteTimeout,
156+
157+
/// The maximum number of pending inbound connections that can exist at the same time.
158+
///
159+
/// Note: the presence of this limit is important for security, but its specific value is not -
160+
/// it just defines backend's burst tolerance for transport-accepted inbound connections that
161+
/// have not completed the P2P handshake yet. But it makes sense to keep it in the same order
162+
/// as `MAX_CONCURRENT_HANDSHAKES` used by wrapped transport layer's `AdaptedListener`, which
163+
/// limits the number of simultaneous transport-level inbound handshakes.
164+
pub max_pending_inbound_connections: MaxPendingInboundConnections,
155165
}

p2p/src/net/default_backend/backend.rs

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ use networking::{
3939
use p2p_types::socket_address::SocketAddress;
4040
use randomness::{RngExt as _, make_pseudo_rng};
4141
use utils::{
42-
atomics::SeqCstAtomicBool, eventhandler::EventsController, set_flag::SetFlag,
43-
shallow_clone::ShallowClone, tokio_spawn_in_current_tracing_span,
42+
atomics::SeqCstAtomicBool, debug_panic_or_log, eventhandler::EventsController,
43+
set_flag::SetFlag, shallow_clone::ShallowClone, tokio_spawn_in_current_tracing_span,
4444
};
4545

4646
use crate::{
@@ -108,6 +108,7 @@ struct PeerContext {
108108
}
109109

110110
/// Pending peer data (until handshake message is received)
111+
#[derive(Debug)]
111112
struct PendingPeerContext {
112113
handle: tokio::task::JoinHandle<()>,
113114

@@ -149,6 +150,9 @@ pub struct Backend<T: TransportSocket> {
149150
/// Pending connections
150151
pending_peers: HashMap<PeerId, PendingPeerContext>,
151152

153+
/// The number of inbound peers in `pending_peers`
154+
pending_inbound_peer_count: usize,
155+
152156
/// Map of streams for receiving events from peers.
153157
peer_event_stream_map: StreamMap<PeerId, ReceiverStream<PeerEvent>>,
154158

@@ -210,6 +214,7 @@ where
210214
syncing_event_sender,
211215
peers: HashMap::new(),
212216
pending_peers: HashMap::new(),
217+
pending_inbound_peer_count: 0,
213218
peer_event_stream_map: StreamMap::new(),
214219
command_queue: FuturesUnordered::new(),
215220
shutdown,
@@ -347,21 +352,51 @@ where
347352
res = self.socket.accept() => {
348353
match res {
349354
Ok((stream, address)) => {
355+
// Note: this execution path is peer-triggerable, so we use debug level logging
356+
// until after `pending_inbound_peer_count` has been checked, to prevent malicious
357+
// peers from flooding the default logs.
350358
if !self.networking_enabled {
351-
log::info!("Ignoring incoming connection from {address:?} because networking is disabled");
359+
log::debug!("Ignoring incoming connection from {address:?} because networking is disabled");
352360
} else {
353-
self.create_pending_peer(
354-
stream,
355-
PeerId::new(),
356-
ConnectionInfo::Inbound,
357-
address.into(),
358-
)?;
361+
let max_pending_inbound_conn_count = *self.p2p_config.backend_config.max_pending_inbound_connections;
362+
363+
if self.pending_inbound_peer_count >= max_pending_inbound_conn_count {
364+
log::debug!(
365+
concat!(
366+
"Ignoring incoming connection from {:?} because the maximum number ",
367+
"of pending incoming connections has been reached {}"
368+
),
369+
address,
370+
max_pending_inbound_conn_count
371+
);
372+
} else {
373+
self.create_pending_peer(
374+
stream,
375+
PeerId::new(),
376+
ConnectionInfo::Inbound,
377+
address.into(),
378+
)?;
379+
}
359380
}
360381
},
361382
Err(err) => {
362-
// Just log the error and let the node continue working
383+
// Note: this execution path is also peer-triggerable, though less reliably than the
384+
// successful path above, so we use debug-level logging here too. See also a similar
385+
// comment in `AdaptedListener::accept` in the transport layer.
386+
// TODO: malicious peers can still flood the default logs making successful connections
387+
// and dropping them immediately (though it will be less severe and more costly for the
388+
// attacker). Consider:
389+
// a) Using debug-level logging specifically for incoming connections (while keeping it
390+
// at "info" for outbound ones) in all (or most) connectivity-related messages (e.g.
391+
// "Peer disconnected", "Assigning peer id", "New peer accepted").
392+
// b) Implementing a rate-limiter for the log (e.g. using a custom tracing layer or filter),
393+
// either based on the source code location (Bitcoin does this) or on the specified
394+
// log target.
395+
// c) (In the case when some errors have been omitted or printed via debug!), printing
396+
// some kind of info/warn-level summary at regular intervals, e.g. "X incoming connections
397+
// ignored in the last Y seconds".
363398
if self.networking_enabled {
364-
log::error!("Accepting a new connection failed unexpectedly: {err}")
399+
log::debug!("Accepting a new connection failed unexpectedly: {err}")
365400
} else {
366401
log::debug!(
367402
"Ignoring failed incoming connection because networking is disabled (err = {err})",
@@ -426,7 +461,7 @@ where
426461
&format!("Peer[id={peer_id}]"),
427462
);
428463

429-
self.pending_peers.insert(
464+
self.insert_new_pending_peer(
430465
peer_id,
431466
PendingPeerContext {
432467
handle,
@@ -456,7 +491,7 @@ where
456491
bind_address,
457492
connection_info,
458493
backend_event_sender,
459-
} = match self.pending_peers.remove(&peer_id) {
494+
} = match self.remove_pending_peer(peer_id) {
460495
Some(pending_peer) => pending_peer,
461496
// Could be removed if self-connection was detected earlier
462497
None => return Ok(()),
@@ -568,7 +603,7 @@ where
568603
.map(|(peer_id, _pending)| *peer_id);
569604

570605
if let Some(peer_id) = pending_outbound_peer_id {
571-
let peer_ctx = self.pending_peers.remove(&peer_id).expect("peer must exist");
606+
let peer_ctx = self.remove_pending_peer(peer_id).expect("peer must exist");
572607

573608
log::info!(
574609
"self-connection detected on address {:?}",
@@ -649,7 +684,7 @@ where
649684
}
650685

651686
PeerEvent::ConnectionClosed => {
652-
if let Some(pending_peer) = self.pending_peers.remove(&peer_id) {
687+
if let Some(pending_peer) = self.remove_pending_peer(peer_id) {
653688
// Note: we'll get here if handshake has failed, so no need to use log levels
654689
// higher that debug, because the error should have been logged properly already.
655690
match pending_peer.connection_info {
@@ -802,6 +837,57 @@ where
802837
Err(_) => log::error!("sending syncing event from the backend failed unexpectedly"),
803838
}
804839
}
840+
841+
fn insert_new_pending_peer(&mut self, peer_id: PeerId, peer_context: PendingPeerContext) {
842+
let is_inbound = peer_context.connection_info.is_inbound();
843+
844+
if let Some(old_context) = self.pending_peers.insert(peer_id, peer_context) {
845+
debug_panic_or_log!(
846+
"Pending peer context already exists for a new peer {peer_id}: {old_context:?}"
847+
);
848+
} else if is_inbound {
849+
self.pending_inbound_peer_count += 1;
850+
851+
#[cfg(test)]
852+
self.assert_pending_inbound_peer_count_consistency();
853+
}
854+
855+
if let Some(observer) = &self.observer {
856+
observer.on_pending_peer_created(peer_id);
857+
}
858+
}
859+
860+
fn remove_pending_peer(&mut self, peer_id: PeerId) -> Option<PendingPeerContext> {
861+
let peer_context = self.pending_peers.remove(&peer_id);
862+
863+
if let Some(peer_context) = &peer_context {
864+
if peer_context.connection_info.is_inbound() {
865+
self.pending_inbound_peer_count -= 1;
866+
867+
#[cfg(test)]
868+
self.assert_pending_inbound_peer_count_consistency();
869+
}
870+
871+
if let Some(observer) = &self.observer {
872+
observer.on_pending_peer_removed(peer_id);
873+
}
874+
}
875+
876+
peer_context
877+
}
878+
879+
#[cfg(test)]
880+
fn assert_pending_inbound_peer_count_consistency(&self) {
881+
let actual_pending_inbound_peer_count = self
882+
.pending_peers
883+
.values()
884+
.filter(|context| context.connection_info.is_inbound())
885+
.count();
886+
assert_eq!(
887+
self.pending_inbound_peer_count,
888+
actual_pending_inbound_peer_count
889+
);
890+
}
805891
}
806892

807893
// Some boilerplate types and a function for blocking tasks handling

p2p/src/net/default_backend/peer/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ impl ConnectionInfo {
7373
ConnectionInfo::Outbound { .. } => ConnectionDirection::Outbound,
7474
}
7575
}
76+
77+
pub fn is_inbound(&self) -> bool {
78+
match self {
79+
ConnectionInfo::Inbound => true,
80+
ConnectionInfo::Outbound { .. } => false,
81+
}
82+
}
7683
}
7784

7885
pub struct Peer<T: TransportSocket> {

p2p/src/net/default_backend/types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,15 @@ pub trait BackendObserver {
417417

418418
/// Called after the message has been read from the socket.
419419
fn on_message_read(&self, peer_id: PeerId, msg: &Message);
420+
421+
/// Called after a new peer has been created (i.e. after the peer task has been spawned and
422+
/// `PendingPeerContext` added to the corresponding collection).
423+
fn on_pending_peer_created(&self, peer_id: PeerId);
424+
425+
/// Called when a `PendingPeerContext` has been removed from the corresponding collection
426+
/// (i.e. either the peer has completed the handshake successfully and is no longer pending,
427+
/// or it failed the handshake and got disconnected).
428+
fn on_pending_peer_removed(&self, peer_id: PeerId);
420429
}
421430

422431
#[cfg(test)]

p2p/src/peer_manager/tests/connections.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,7 @@ async fn connection_timeout_rpc_notified<T>(
10091009
peer_handshake_timeout: Default::default(),
10101010
disconnection_timeout: Default::default(),
10111011
socket_write_timeout: Default::default(),
1012+
max_pending_inbound_connections: Default::default(),
10121013
},
10131014

10141015
bind_addresses: Default::default(),

p2p/src/sync/tests/network_sync.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ async fn send_block_from_the_future_again(#[case] seed: Seed) {
434434
outbound_connection_timeout: Default::default(),
435435
disconnection_timeout: Default::default(),
436436
socket_write_timeout: Default::default(),
437+
max_pending_inbound_connections: Default::default(),
437438
},
438439

439440
bind_addresses: Default::default(),

p2p/src/tests/connection_lockup_when_socket_not_read.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use test_utils::{
3737
};
3838

3939
use crate::{
40-
config::{BackendTimeoutsConfig, P2pConfig},
40+
config::{BackendConfig, P2pConfig},
4141
message::{HeaderList, HeaderListRequest},
4242
net::{
4343
default_backend::types::{HandshakeMessage, Message, MessageTag, P2pTimestamp},
@@ -291,6 +291,7 @@ async fn timeout_when_socket_not_read(
291291

292292
outbound_connection_timeout: Default::default(),
293293
peer_handshake_timeout: Default::default(),
294+
max_pending_inbound_connections: Default::default(),
294295
},
295296

296297
bind_addresses: Default::default(),

0 commit comments

Comments
 (0)