Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions crates/floresta-wire/src/p2p_wire/ban_man.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use std::collections::HashMap;
use std::net::IpAddr;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

/// Default ban duration in seconds
const DEFAULT_BAN_DURATION: u64 = 60 * 60 * 24;

/// Centralized manager for tracking banned IP addresses.
#[derive(Debug, Default)]
pub struct BanMan {
banned: HashMap<IpAddr, u64>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is good to document somewhere here in this module that is expected for this u64 to be an absolute timestamp that such a peer is banned

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}

impl BanMan {
/// Creates a new empty Banman
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Creates a new empty Banman
/// Creates a new empty `Banman`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

pub fn new() -> Self {
Self {
banned: HashMap::new(),
}
}

/// Bans an IP address for `duration` seconds from now
///
/// If `duration` is 0, the default ban time (24 hours) will be use
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// If `duration` is 0, the default ban time (24 hours) will be use
/// If `duration` is 0, the default ban time (24 hours) will be used instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

pub fn add_ban(&mut self, ip: IpAddr, duration: u64) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);

let ban_duration = if duration == 0 {
DEFAULT_BAN_DURATION
} else {
duration
};

let ban_until = now + ban_duration;
self.banned.insert(ip, ban_until);
}

/// Returns true if the IP is banned
///
/// Expired bans are removed automatically
pub fn is_banned(&mut self, ip: IpAddr) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);

if let Some(ban_until) = self.banned.get(&ip) {
if *ban_until > now {
return true;
}
// Ban expired, clean it up
self.banned.remove(&ip);
}

false
}
}

#[cfg(test)]
mod tests {
use std::net::IpAddr;
use std::net::Ipv4Addr;

use super::*;

fn test_ip() -> IpAddr {
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
}

#[test]
fn test_banned_ip_is_detected() {
let mut ban_man = BanMan::new();
let ip = test_ip();

ban_man.add_ban(ip, 3600);
assert!(ban_man.is_banned(ip));
}

#[test]
fn test_unbanned_ip_is_not_detected() {
let mut ban_man = BanMan::new();
let ip = test_ip();

assert!(!ban_man.is_banned(ip));
}

#[test]
fn test_expired_ban_is_cleaned_up() {
let mut ban_man = BanMan::new();
let ip = test_ip();

// Insert a ban that already expired (ban_until is in the past)
ban_man.banned.insert(ip, 0);

assert!(!ban_man.is_banned(ip));
// Verify it was removed from the map
assert!(!ban_man.banned.contains_key(&ip));
}

#[test]
fn test_default_duration_when_zero() {
let mut ban_man = BanMan::new();
let ip = test_ip();

ban_man.add_ban(ip, 0);

// Should be banned for 24 hours from now
let ban_until = ban_man.banned.get(&ip).unwrap();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);

let expected = now + DEFAULT_BAN_DURATION;
// Allow 1 second tolerance for test execution time
assert!(*ban_until >= expected - 1 && *ban_until <= expected + 1);
}

#[test]
fn test_multiple_ips_banned_independently() {
let mut ban_man = BanMan::new();
let ip1 = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
let ip2 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));

ban_man.add_ban(ip1, 3600);

assert!(ban_man.is_banned(ip1));
assert!(!ban_man.is_banned(ip2));
}

#[test]
fn test_reban_updates_expiry() {
let mut ban_man = BanMan::new();
let ip = test_ip();

ban_man.add_ban(ip, 100);
let first_ban = *ban_man.banned.get(&ip).unwrap();

ban_man.add_ban(ip, 9999);
let second_ban = *ban_man.banned.get(&ip).unwrap();

assert!(second_ban > first_ban);
}
}
4 changes: 4 additions & 0 deletions crates/floresta-wire/src/p2p_wire/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ pub enum WireError {

/// Couldn't find the leaf data for a block
LeafDataNotFound,

/// Peer is banned
PeerBanned(IpAddr),
}

impl Display for WireError {
Expand Down Expand Up @@ -134,6 +137,7 @@ impl Display for WireError {
"We tried to work on a block that we don't have a proof for yet"
),
WireError::LeafDataNotFound => write!(f, "Couldn't find the leaf data for a block"),
WireError::PeerBanned(ip) => write!(f, "Peer {ip} is banned"),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/floresta-wire/src/p2p_wire/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ impl Default for UtreexoNodeConfig {
}

pub mod address_man;
pub mod ban_man;
pub mod block_proof;
pub mod error;
pub mod node;
Expand Down
5 changes: 5 additions & 0 deletions crates/floresta-wire/src/p2p_wire/node/conn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ where
return Err(WireError::NoAddressesAvailable);
};

let net_address = peer_address.get_net_address();
if self.ban_man.is_banned(net_address) {
return Err(WireError::PeerBanned(net_address));
}

debug!("Attempting connection with address={peer_address:?} kind={conn_kind:?}",);

let now = SystemTime::now()
Expand Down
3 changes: 3 additions & 0 deletions crates/floresta-wire/src/p2p_wire/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use tracing::info;

use super::address_man::AddressMan;
use super::address_man::LocalAddress;
use super::ban_man::BanMan;
use super::block_proof::Bitmap;
use super::error::WireError;
use super::node_context::NodeContext;
Expand Down Expand Up @@ -259,6 +260,7 @@ pub struct NodeCommon<Chain: ChainBackend> {
pub(crate) peer_by_service: HashMap<ServiceFlags, Vec<u32>>,
pub(crate) max_banscore: u32,
pub(crate) address_man: AddressMan,
pub(crate) ban_man: BanMan,
pub(crate) added_peers: Vec<AddedPeerInfo>,

// 3. Internal Communication
Expand Down Expand Up @@ -372,6 +374,7 @@ where
node_rx,
node_tx,
address_man,
ban_man: BanMan::new(),
last_tip_update: Instant::now(),
last_connection: Instant::now(),
last_peer_db_dump: Instant::now(),
Expand Down
2 changes: 2 additions & 0 deletions crates/floresta-wire/src/p2p_wire/node/peer_man.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,9 @@ where
}

// `handle_disconnection` will mark the address as banned when `Peer` object return
let peer_addr = peer.address;
peer.state = PeerStatus::Banned;
self.ban_man.add_ban(peer_addr, 0);
}

self.send_to_peer(peer, NodeRequest::Shutdown)?;
Expand Down