Skip to content

Commit

Permalink
feat!: Refuse connections from recent disconnects
Browse files Browse the repository at this point in the history
In order to reduce the potential connection flood, rapid re-connection
attempts after a disconnect are ignored.

If node A decides to (gracefully) terminate the connection to some node
B, then node A blocks any connection attempts from node B for some
time. The cool-down time can be configured through the command-line
interface's argument “reconnect_cooldown”. The argument is given in
seconds. The default is 30 seconds.

BREAKING CHANGE: Introduce variant `RecentlyDisconnected` to public enum
`ConnectionRefusedReason`.
  • Loading branch information
jan-ferdinand committed Feb 13, 2025
1 parent e8cf9ac commit 6846789
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 6 deletions.
9 changes: 9 additions & 0 deletions src/config_models/cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,11 @@ pub struct Args {
/// Exposing the data directory leaks some privacy. Disable to prevent.
#[clap(long)]
pub disable_cookie_hint: bool,

/// The duration (in seconds) during which new connection attempts from peers
/// are ignored after a connection to them was closed.
#[clap(long, default_value = "30", value_parser = duration_from_seconds_str)]
pub reconnect_cooldown: Duration,
}

impl Default for Args {
Expand All @@ -269,6 +274,10 @@ fn fraction_validator(s: &str) -> Result<f64, String> {
}
}

fn duration_from_seconds_str(s: &str) -> Result<Duration, std::num::ParseIntError> {
Ok(Duration::from_secs(s.parse()?))
}

impl Args {
#[cfg(test)]
pub(crate) fn default_with_network(network: Network) -> Self {
Expand Down
10 changes: 10 additions & 0 deletions src/config_models/data_directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::models::state::archival_state::ARCHIVAL_BLOCK_MMR_DIRECTORY_NAME;
use crate::models::state::archival_state::BLOCK_INDEX_DB_NAME;
use crate::models::state::archival_state::MUTATOR_SET_DIRECTORY_NAME;
use crate::models::state::networking_state::BANNED_IPS_DB_NAME;
use crate::models::state::networking_state::DISCONNECTED_PEERS_DB_NAME;
use crate::models::state::shared::BLOCK_FILENAME_EXTENSION;
use crate::models::state::shared::BLOCK_FILENAME_PREFIX;
use crate::models::state::shared::DIR_NAME_FOR_BLOCKS;
Expand Down Expand Up @@ -104,6 +105,15 @@ impl DataDirectory {
self.database_dir_path().join(Path::new(BANNED_IPS_DB_NAME))
}

/// The directory path for the database containing disconnect times of past
/// peers.
///
/// This directory lives within `DataDirectory::database_dir_path()`.
pub fn peer_disconnect_database_dir_path(&self) -> PathBuf {
self.database_dir_path()
.join(Path::new(DISCONNECTED_PEERS_DB_NAME))
}

///////////////////////////////////////////////////////////////////////////
///
/// utxo-transfer path
Expand Down
15 changes: 15 additions & 0 deletions src/connect_to_peers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fmt::Debug;
use std::net::SocketAddr;
use std::time::SystemTime;

use anyhow::bail;
use anyhow::Result;
Expand Down Expand Up @@ -107,6 +108,20 @@ async fn check_if_connection_is_allowed(
return InternalConnectionStatus::Refused(ConnectionRefusedReason::BadStanding);
}

if let Some(time) = global_state
.net
.last_disconnect_time_of_peer(*peer_address)
.await
{
if SystemTime::now()
.duration_since(time)
.is_ok_and(|d| d < cli_arguments.reconnect_cooldown)
{
let reason = ConnectionRefusedReason::RecentlyDisconnected;
return InternalConnectionStatus::Refused(reason);
}
}

// Disallow connection if max number of peers has been reached or
// exceeded. There is another test in `answer_peer_inner` that precedes
// this one; however this test is still necessary to resolve potential
Expand Down
3 changes: 3 additions & 0 deletions src/models/database.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::fmt;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::time::SystemTime;

use serde::Deserialize;
use serde::Serialize;
Expand Down Expand Up @@ -159,6 +161,7 @@ impl BlockIndexValue {
#[derive(Clone)]
pub struct PeerDatabases {
pub peer_standings: NeptuneLevelDb<IpAddr, PeerStanding>,
pub disconnect_times: NeptuneLevelDb<SocketAddr, SystemTime>,
}

impl fmt::Debug for PeerDatabases {
Expand Down
2 changes: 2 additions & 0 deletions src/models/peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,14 @@ pub enum InternalConnectionStatus {
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConnectionRefusedReason {
AlreadyConnected,
BadStanding,
IncompatibleVersion,
MaxPeerNumberExceeded,
SelfConnect,
RecentlyDisconnected,
}

/// The reason the connection was terminated.
Expand Down
24 changes: 22 additions & 2 deletions src/models/state/networking_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::models::peer::peer_info::PeerInfo;
use crate::models::peer::PeerStanding;

pub const BANNED_IPS_DB_NAME: &str = "banned_ips";
pub(crate) const DISCONNECTED_PEERS_DB_NAME: &str = "disconnected_peers";

type PeerMap = HashMap<SocketAddr, PeerInfo>;

Expand Down Expand Up @@ -110,7 +111,7 @@ impl NetworkingState {
}
}

/// Create databases for peer standings
/// Create databases for peer standings and peer disconnect times
pub async fn initialize_peer_databases(data_dir: &DataDirectory) -> Result<PeerDatabases> {
let database_dir_path = data_dir.database_dir_path();
DataDirectory::create_dir_if_not_exists(&database_dir_path).await?;
Expand All @@ -120,8 +121,16 @@ impl NetworkingState {
&create_db_if_missing(),
)
.await?;
let disconnect_times = NeptuneLevelDb::new(
&data_dir.peer_disconnect_database_dir_path(),
&create_db_if_missing(),
)
.await?;

Ok(PeerDatabases { peer_standings })
Ok(PeerDatabases {
peer_standings,
disconnect_times,
})
}

/// Return a list of peer sanctions stored in the database.
Expand Down Expand Up @@ -188,4 +197,15 @@ impl NetworkingState {
.await
}
}

pub(crate) async fn register_peer_disconnect(&mut self, peer: SocketAddr, time: SystemTime) {
self.peer_databases.disconnect_times.put(peer, time).await;
}

pub(crate) async fn last_disconnect_time_of_peer(
&self,
peer: SocketAddr,
) -> Option<SystemTime> {
self.peer_databases.disconnect_times.get(peer).await
}
}
21 changes: 17 additions & 4 deletions src/peer_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::main_loop::MAX_NUM_DIGESTS_IN_BATCH_REQUEST;
use crate::models::blockchain::block::block_height::BlockHeight;
use crate::models::blockchain::block::Block;
use crate::models::blockchain::transaction::Transaction;
use crate::models::channel::InternalDisconnectReason;
use crate::models::channel::MainToPeerTask;
use crate::models::channel::PeerTaskToMain;
use crate::models::channel::PeerTaskToMainTransaction;
Expand Down Expand Up @@ -1563,12 +1564,24 @@ impl PeerLoopHandler {
peer.send(PeerMessage::PeerListRequest).await?;
Ok(KEEP_CONNECTION_ALIVE)
}
MainToPeerTask::Disconnect(target_socket_addr, _reason) => {
MainToPeerTask::Disconnect(peer_address, reason) => {
log_slow_scope!(fn_name!() + "::MainToPeerTask::Disconnect");

// Disconnect from this peer if its address matches that which the main
// task requested to disconnect from.
Ok(target_socket_addr == self.peer_address)
// Only disconnect from the peer the main task requested a disconnect for.
if peer_address != self.peer_address {
return Ok(false);
}

if reason == InternalDisconnectReason::OutOfConnectionCapacity {
self.global_state_lock
.lock_guard_mut()
.await
.net
.register_peer_disconnect(peer_address, SystemTime::now())
.await;
}

Ok(true)
}
// Disconnect from this peer, no matter what.
MainToPeerTask::DisconnectAll() => Ok(true),
Expand Down

0 comments on commit 6846789

Please sign in to comment.