diff --git a/Cargo.lock b/Cargo.lock index a786d0dbf71..848508c2806 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2157,6 +2157,7 @@ dependencies = [ "futures-timer", "getrandom 0.2.8", "instant", + "libp2p-allow-block-list", "libp2p-autonat", "libp2p-connection-limits", "libp2p-core", @@ -2193,6 +2194,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "libp2p-allow-block-list" +version = "0.1.0" +dependencies = [ + "async-std", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "libp2p-swarm-derive", + "libp2p-swarm-test", + "void", +] + [[package]] name = "libp2p-autonat" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index dcfb8b1b230..fcb6ccc0d41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "examples/rendezvous", "identity", "interop-tests", + "misc/allow-block-list", "misc/connection-limits", "misc/keygen", "misc/metrics", diff --git a/libp2p/CHANGELOG.md b/libp2p/CHANGELOG.md index 946b0dfdd0e..adab7b52e13 100644 --- a/libp2p/CHANGELOG.md +++ b/libp2p/CHANGELOG.md @@ -8,8 +8,12 @@ To properly communicate this to users, we want them to add the dependency directly which makes the `alpha` version visible. See [PR 3580]. +- Introduce `libp2p::allow_block_list` module and deprecate `libp2p::Swarm::ban_peer_id`. + See [PR 3590]. + [PR 3386]: https://github.com/libp2p/rust-libp2p/pull/3386 [PR 3580]: https://github.com/libp2p/rust-libp2p/pull/3580 +[PR 3590]: https://github.com/libp2p/rust-libp2p/pull/3590 # 0.51.1 diff --git a/libp2p/Cargo.toml b/libp2p/Cargo.toml index 753257a7c5d..33176cfaf95 100644 --- a/libp2p/Cargo.toml +++ b/libp2p/Cargo.toml @@ -96,6 +96,7 @@ futures-timer = "3.0.2" # Explicit dependency to be used in `wasm-bindgen` featu getrandom = "0.2.3" # Explicit dependency to be used in `wasm-bindgen` feature instant = "0.1.11" # Explicit dependency to be used in `wasm-bindgen` feature +libp2p-allow-block-list = { version = "0.1.0", path = "../misc/allow-block-list" } libp2p-autonat = { version = "0.10.0", path = "../protocols/autonat", optional = true } libp2p-connection-limits = { version = "0.1.0", path = "../misc/connection-limits" } libp2p-core = { version = "0.39.0", path = "../core" } diff --git a/libp2p/src/lib.rs b/libp2p/src/lib.rs index 322decc1fb4..f453b5c28fd 100644 --- a/libp2p/src/lib.rs +++ b/libp2p/src/lib.rs @@ -40,6 +40,8 @@ pub use libp2p_core::multihash; #[doc(inline)] pub use multiaddr; +#[doc(inline)] +pub use libp2p_allow_block_list as allow_block_list; #[cfg(feature = "autonat")] #[doc(inline)] pub use libp2p_autonat as autonat; diff --git a/misc/allow-block-list/CHANGELOG.md b/misc/allow-block-list/CHANGELOG.md new file mode 100644 index 00000000000..d1e4ec5a44e --- /dev/null +++ b/misc/allow-block-list/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 - unreleased + +- Initial release. diff --git a/misc/allow-block-list/Cargo.toml b/misc/allow-block-list/Cargo.toml new file mode 100644 index 00000000000..a175876179f --- /dev/null +++ b/misc/allow-block-list/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "libp2p-allow-block-list" +edition = "2021" +rust-version = "1.62.0" +description = "Allow/block list connection management for libp2p." +version = "0.1.0" +license = "MIT" +repository = "https://github.com/libp2p/rust-libp2p" +keywords = ["peer-to-peer", "libp2p", "networking"] +categories = ["network-programming", "asynchronous"] + +[dependencies] +libp2p-core = { version = "0.39.0", path = "../../core" } +libp2p-swarm = { version = "0.42.0", path = "../../swarm" } +libp2p-identity = { version = "0.1.0", path = "../../identity", features = ["peerid"] } +void = "1" + +[dev-dependencies] +async-std = { version = "1.12.0", features = ["attributes"] } +libp2p-swarm-derive = { path = "../../swarm-derive" } +libp2p-swarm-test = { path = "../../swarm-test" } diff --git a/misc/allow-block-list/src/lib.rs b/misc/allow-block-list/src/lib.rs new file mode 100644 index 00000000000..16c47898dbc --- /dev/null +++ b/misc/allow-block-list/src/lib.rs @@ -0,0 +1,466 @@ +// Copyright 2023 Protocol Labs. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! A libp2p module for managing allow and blocks lists to peers. +//! +//! # Allow list example +//! +//! ```rust +//! # use libp2p_swarm::Swarm; +//! # use libp2p_swarm_derive::NetworkBehaviour; +//! # use libp2p_allow_block_list as allow_block_list; +//! # use libp2p_allow_block_list::AllowedPeers; +//! # +//! #[derive(NetworkBehaviour)] +//! # #[behaviour(prelude = "libp2p_swarm::derive_prelude")] +//! struct MyBehaviour { +//! allowed_peers: allow_block_list::Behaviour, +//! } +//! +//! # fn main() { +//! let behaviour = MyBehaviour { +//! allowed_peers: allow_block_list::Behaviour::default() +//! }; +//! # } +//! ``` +//! # Block list example +//! +//! ```rust +//! # use libp2p_swarm::Swarm; +//! # use libp2p_swarm_derive::NetworkBehaviour; +//! # use libp2p_allow_block_list as allow_block_list; +//! # use libp2p_allow_block_list::BlockedPeers; +//! # +//! #[derive(NetworkBehaviour)] +//! # #[behaviour(prelude = "libp2p_swarm::derive_prelude")] +//! struct MyBehaviour { +//! blocked_peers: allow_block_list::Behaviour, +//! } +//! +//! # fn main() { +//! let behaviour = MyBehaviour { +//! blocked_peers: allow_block_list::Behaviour::default() +//! }; +//! # } +//! ``` + +use libp2p_core::{Endpoint, Multiaddr}; +use libp2p_identity::PeerId; +use libp2p_swarm::{ + dummy, CloseConnection, ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, + NetworkBehaviourAction, PollParameters, THandler, THandlerInEvent, THandlerOutEvent, +}; +use std::collections::{HashSet, VecDeque}; +use std::fmt; +use std::task::{Context, Poll, Waker}; +use void::Void; + +/// A [`NetworkBehaviour`] that can act as an allow or block list. +#[derive(Default, Debug)] +pub struct Behaviour { + state: S, + close_connections: VecDeque, + waker: Option, +} + +/// The list of explicitly allowed peers. +#[derive(Default)] +pub struct AllowedPeers { + peers: HashSet, +} + +/// The list of explicitly blocked peers. +#[derive(Default)] +pub struct BlockedPeers { + peers: HashSet, +} + +impl Behaviour { + /// Allow connections to the given peer. + pub fn allow_peer(&mut self, peer: PeerId) { + self.state.peers.insert(peer); + if let Some(waker) = self.waker.take() { + waker.wake() + } + } + + /// Disallow connections to the given peer. + /// + /// All active connections to this peer will be closed immediately. + pub fn disallow_peer(&mut self, peer: PeerId) { + self.state.peers.insert(peer); + self.close_connections.push_back(peer); + if let Some(waker) = self.waker.take() { + waker.wake() + } + } +} + +impl Behaviour { + /// Block connections to a given peer. + /// + /// All active connections to this peer will be closed immediately. + pub fn block_peer(&mut self, peer: PeerId) { + self.state.peers.insert(peer); + self.close_connections.push_back(peer); + if let Some(waker) = self.waker.take() { + waker.wake() + } + } + + /// Unblock connections to a given peer. + pub fn unblock_peer(&mut self, peer: PeerId) { + self.state.peers.insert(peer); + if let Some(waker) = self.waker.take() { + waker.wake() + } + } +} + +/// A connection to this peer is not explicitly allowed and was thus [`denied`](ConnectionDenied). +#[derive(Debug)] +pub struct NotAllowed { + peer: PeerId, +} + +impl fmt::Display for NotAllowed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "peer {} is not in the allow list", self.peer) + } +} + +impl std::error::Error for NotAllowed {} + +/// A connection to this peer was explicitly blocked and was thus [`denied`](ConnectionDenied). +#[derive(Debug)] +pub struct Blocked { + peer: PeerId, +} + +impl fmt::Display for Blocked { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "peer {} is in the block list", self.peer) + } +} + +impl std::error::Error for Blocked {} + +trait Enforce: 'static { + fn enforce(&self, peer: &PeerId) -> Result<(), ConnectionDenied>; +} + +impl Enforce for AllowedPeers { + fn enforce(&self, peer: &PeerId) -> Result<(), ConnectionDenied> { + if !self.peers.contains(peer) { + return Err(ConnectionDenied::new(NotAllowed { peer: *peer })); + } + + Ok(()) + } +} + +impl Enforce for BlockedPeers { + fn enforce(&self, peer: &PeerId) -> Result<(), ConnectionDenied> { + if self.peers.contains(peer) { + return Err(ConnectionDenied::new(Blocked { peer: *peer })); + } + + Ok(()) + } +} + +impl NetworkBehaviour for Behaviour +where + S: Enforce, +{ + type ConnectionHandler = dummy::ConnectionHandler; + type OutEvent = Void; + + fn handle_established_inbound_connection( + &mut self, + _: ConnectionId, + peer: PeerId, + _: &Multiaddr, + _: &Multiaddr, + ) -> Result, ConnectionDenied> { + self.state.enforce(&peer)?; + + Ok(dummy::ConnectionHandler) + } + + fn handle_pending_outbound_connection( + &mut self, + _: ConnectionId, + peer: Option, + _: &[Multiaddr], + _: Endpoint, + ) -> Result, ConnectionDenied> { + if let Some(peer) = peer { + self.state.enforce(&peer)?; + } + + Ok(vec![]) + } + + fn handle_established_outbound_connection( + &mut self, + _: ConnectionId, + peer: PeerId, + _: &Multiaddr, + _: Endpoint, + ) -> Result, ConnectionDenied> { + self.state.enforce(&peer)?; + + Ok(dummy::ConnectionHandler) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + match event { + FromSwarm::ConnectionClosed(_) => {} + FromSwarm::ConnectionEstablished(_) => {} + FromSwarm::AddressChange(_) => {} + FromSwarm::DialFailure(_) => {} + FromSwarm::ListenFailure(_) => {} + FromSwarm::NewListener(_) => {} + FromSwarm::NewListenAddr(_) => {} + FromSwarm::ExpiredListenAddr(_) => {} + FromSwarm::ListenerError(_) => {} + FromSwarm::ListenerClosed(_) => {} + FromSwarm::NewExternalAddr(_) => {} + FromSwarm::ExpiredExternalAddr(_) => {} + } + } + + fn on_connection_handler_event( + &mut self, + _id: PeerId, + _: ConnectionId, + event: THandlerOutEvent, + ) { + void::unreachable(event) + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + _: &mut impl PollParameters, + ) -> Poll>> { + if let Some(peer) = self.close_connections.pop_front() { + return Poll::Ready(NetworkBehaviourAction::CloseConnection { + peer_id: peer, + connection: CloseConnection::All, + }); + } + + self.waker = Some(cx.waker().clone()); + Poll::Pending + } +} + +#[cfg(test)] +mod tests { + use super::*; + use libp2p_swarm::{dial_opts::DialOpts, DialError, ListenError, Swarm, SwarmEvent}; + use libp2p_swarm_test::SwarmExt; + + #[async_std::test] + async fn cannot_dial_blocked_peer() { + let mut dialer = Swarm::new_ephemeral(|_| Behaviour::::new()); + let mut listener = Swarm::new_ephemeral(|_| Behaviour::::new()); + listener.listen().await; + + dialer + .behaviour_mut() + .list + .block_peer(*listener.local_peer_id()); + + let DialError::Denied { cause } = dial(&mut dialer, &listener).unwrap_err() else { + panic!("unexpected dial error") + }; + assert!(cause.downcast::().is_ok()); + } + + #[async_std::test] + async fn blocked_peer_cannot_dial_us() { + let mut dialer = Swarm::new_ephemeral(|_| Behaviour::::new()); + let mut listener = Swarm::new_ephemeral(|_| Behaviour::::new()); + listener.listen().await; + + listener + .behaviour_mut() + .list + .block_peer(*dialer.local_peer_id()); + dial(&mut dialer, &listener).unwrap(); + async_std::task::spawn(dialer.loop_on_next()); + + let cause = listener + .wait(|e| match e { + SwarmEvent::IncomingConnectionError { + error: ListenError::Denied { cause }, + .. + } => Some(cause), + _ => None, + }) + .await; + assert!(cause.downcast::().is_ok()); + } + + #[async_std::test] + async fn connections_get_closed_upon_blocked() { + let mut dialer = Swarm::new_ephemeral(|_| Behaviour::::new()); + let mut listener = Swarm::new_ephemeral(|_| Behaviour::::new()); + listener.listen().await; + dialer.connect(&mut listener).await; + + dialer + .behaviour_mut() + .list + .block_peer(*listener.local_peer_id()); + + let ( + [SwarmEvent::ConnectionClosed { peer_id: closed_dialer_peer, .. }], + [SwarmEvent::ConnectionClosed { peer_id: closed_listener_peer, .. }] + ) = libp2p_swarm_test::drive(&mut dialer, &mut listener).await else { + panic!("unexpected events") + }; + assert_eq!(closed_dialer_peer, *listener.local_peer_id()); + assert_eq!(closed_listener_peer, *dialer.local_peer_id()); + } + + #[async_std::test] + async fn cannot_dial_peer_unless_allowed() { + let mut dialer = Swarm::new_ephemeral(|_| Behaviour::::new()); + let mut listener = Swarm::new_ephemeral(|_| Behaviour::::new()); + listener.listen().await; + + let DialError::Denied { cause } = dial(&mut dialer, &listener).unwrap_err() else { + panic!("unexpected dial error") + }; + assert!(cause.downcast::().is_ok()); + + dialer + .behaviour_mut() + .list + .allow_peer(*listener.local_peer_id()); + assert!(dial(&mut dialer, &listener).is_ok()); + } + + #[async_std::test] + async fn not_allowed_peer_cannot_dial_us() { + let mut dialer = Swarm::new_ephemeral(|_| Behaviour::::new()); + let mut listener = Swarm::new_ephemeral(|_| Behaviour::::new()); + listener.listen().await; + + dialer + .dial( + DialOpts::unknown_peer_id() + .address( + listener + .external_addresses() + .map(|a| a.addr.clone()) + .next() + .unwrap(), + ) + .build(), + ) + .unwrap(); + + let ( + [SwarmEvent::OutgoingConnectionError { error: DialError::Denied { cause: outgoing_cause }, .. }], + [_, _, _, SwarmEvent::IncomingConnectionError { error: ListenError::Denied { cause: incoming_cause }, .. }], + ) = libp2p_swarm_test::drive(&mut dialer, &mut listener).await else { + panic!("unexpected events") + }; + assert!(outgoing_cause.downcast::().is_ok()); + assert!(incoming_cause.downcast::().is_ok()); + } + + #[async_std::test] + async fn connections_get_closed_upon_disallow() { + let mut dialer = Swarm::new_ephemeral(|_| Behaviour::::new()); + let mut listener = Swarm::new_ephemeral(|_| Behaviour::::new()); + listener.listen().await; + dialer + .behaviour_mut() + .list + .allow_peer(*listener.local_peer_id()); + listener + .behaviour_mut() + .list + .allow_peer(*dialer.local_peer_id()); + + dialer.connect(&mut listener).await; + + dialer + .behaviour_mut() + .list + .disallow_peer(*listener.local_peer_id()); + let ( + [SwarmEvent::ConnectionClosed { peer_id: closed_dialer_peer, .. }], + [SwarmEvent::ConnectionClosed { peer_id: closed_listener_peer, .. }] + ) = libp2p_swarm_test::drive(&mut dialer, &mut listener).await else { + panic!("unexpected events") + }; + assert_eq!(closed_dialer_peer, *listener.local_peer_id()); + assert_eq!(closed_listener_peer, *dialer.local_peer_id()); + } + + fn dial( + dialer: &mut Swarm>, + listener: &Swarm>, + ) -> Result<(), DialError> + where + S: Enforce, + { + dialer.dial( + DialOpts::peer_id(*listener.local_peer_id()) + .addresses( + listener + .external_addresses() + .map(|a| a.addr.clone()) + .collect(), + ) + .build(), + ) + } + + #[derive(libp2p_swarm_derive::NetworkBehaviour)] + #[behaviour(prelude = "libp2p_swarm::derive_prelude")] + struct Behaviour { + list: super::Behaviour, + keep_alive: libp2p_swarm::keep_alive::Behaviour, + } + + impl Behaviour + where + S: Default, + { + fn new() -> Self { + Self { + list: super::Behaviour { + waker: None, + close_connections: VecDeque::new(), + state: S::default(), + }, + keep_alive: libp2p_swarm::keep_alive::Behaviour, + } + } + } +} diff --git a/misc/metrics/src/swarm.rs b/misc/metrics/src/swarm.rs index 8f80c74d6ec..c913710cbce 100644 --- a/misc/metrics/src/swarm.rs +++ b/misc/metrics/src/swarm.rs @@ -223,7 +223,7 @@ impl super::Recorder record(OutgoingConnectionError::Banned), #[allow(deprecated)] libp2p_swarm::DialError::ConnectionLimit(_) => { @@ -250,6 +250,7 @@ impl super::Recorder { self.connected_to_banned_peer .get_or_create(&AddressLabels { diff --git a/protocols/kad/src/behaviour.rs b/protocols/kad/src/behaviour.rs index 9bb1cdf33e7..cd17cc1b6c4 100644 --- a/protocols/kad/src/behaviour.rs +++ b/protocols/kad/src/behaviour.rs @@ -1923,6 +1923,7 @@ where }; match error { + #[allow(deprecated)] DialError::Banned | DialError::LocalPeerId { .. } | DialError::InvalidPeerId { .. } diff --git a/swarm/CHANGELOG.md b/swarm/CHANGELOG.md index aea268e526a..bb9f93a2bea 100644 --- a/swarm/CHANGELOG.md +++ b/swarm/CHANGELOG.md @@ -1,4 +1,4 @@ -# 0.42.1 [unreleased] +# 0.42.1 - unreleased - Deprecate `ConnectionLimits` in favor of `libp2p::connection_limits`. See [PR 3386]. @@ -6,8 +6,12 @@ - Introduce `ConnectionId::new_unchecked` to allow for more sophisticated, manual tests of `NetworkBehaviour`. See [PR 3652]. +- Deprecate `Swarm::ban_peer_id` in favor of the new `libp2p::allow_block_list` module. + See [PR 3590]. + [PR 3386]: https://github.com/libp2p/rust-libp2p/pull/3386 [PR 3652]: https://github.com/libp2p/rust-libp2p/pull/3652 +[PR 3590]: https://github.com/libp2p/rust-libp2p/pull/3590 # 0.42.0 diff --git a/swarm/src/lib.rs b/swarm/src/lib.rs index 1180e7185c8..41809f97ef6 100644 --- a/swarm/src/lib.rs +++ b/swarm/src/lib.rs @@ -251,6 +251,7 @@ pub enum SwarmEvent { error: DialError, }, /// We connected to a peer, but we immediately closed the connection because that peer is banned. + #[deprecated(note = "Use `libp2p::allow_block_list` instead.", since = "0.42.1")] BannedPeer { /// Identity of the banned peer. peer_id: PeerId, @@ -556,6 +557,7 @@ where if let Some(peer_id) = peer_id { // Check if peer is banned. if self.banned_peers.contains(&peer_id) { + #[allow(deprecated)] let error = DialError::Banned; self.behaviour .on_swarm_event(FromSwarm::DialFailure(DialFailure { @@ -738,6 +740,7 @@ where /// /// Any incoming connection and any dialing attempt will immediately be rejected. /// This function has no effect if the peer is already banned. + #[deprecated(note = "Use `libp2p::allow_block_list` instead.", since = "0.42.1")] pub fn ban_peer_id(&mut self, peer_id: PeerId) { if self.banned_peers.insert(peer_id) { // Note that established connections to the now banned peer are closed but not @@ -749,6 +752,7 @@ where } /// Unbans a peer. + #[deprecated(note = "Use `libp2p::allow_block_list` instead.", since = "0.42.1")] pub fn unban_peer_id(&mut self, peer_id: PeerId) { self.banned_peers.remove(&peer_id); } @@ -809,6 +813,7 @@ where established_in, } => { if self.banned_peers.contains(&peer_id) { + #[allow(deprecated)] return Some(SwarmEvent::BannedPeer { peer_id, endpoint }); } @@ -1716,6 +1721,7 @@ where #[derive(Debug)] pub enum DialError { /// The peer is currently banned. + #[deprecated(note = "Use `libp2p::allow_block_list` instead.", since = "0.42.1")] Banned, /// The configured limit for simultaneous outgoing connections /// has been reached. @@ -1776,6 +1782,7 @@ impl fmt::Display for DialError { f, "Dial error: tried to dial local peer id at {endpoint:?}." ), + #[allow(deprecated)] DialError::Banned => write!(f, "Dial error: peer is banned."), DialError::DialPeerConditionFalse(c) => { write!(f, "Dial error: condition {c:?} for dialing peer was false.") @@ -1827,6 +1834,7 @@ impl error::Error for DialError { DialError::ConnectionLimit(err) => Some(err), DialError::LocalPeerId { .. } => None, DialError::NoAddresses => None, + #[allow(deprecated)] DialError::Banned => None, DialError::DialPeerConditionFalse(_) => None, DialError::Aborted => None, @@ -1926,6 +1934,9 @@ impl error::Error for ListenError { } } +/// A connection was denied. +/// +/// To figure out which [`NetworkBehaviour`] denied the connection, use [`ConnectionDenied::downcast`]. #[derive(Debug)] pub struct ConnectionDenied { inner: Box, @@ -2117,6 +2128,7 @@ mod tests { /// [`FromSwarm::ConnectionEstablished`], [`FromSwarm::ConnectionClosed`] /// calls should be registered. #[test] + #[allow(deprecated)] fn test_connect_disconnect_ban() { let _ = env_logger::try_init();