Rate-limit initial seed peer connections (#2943)

* Rate-limit initial seed peer connections

* Revert "Rate-limit initial seed peer connections"

This reverts commit f779a1eb9e.

* Simplify logic

* Avoid cooperative async task starvation in the peer crawler and listener

If we don't yield in these loops, they can run for a long time before
tokio forces them to yield.

* Add test

* Check for task panics in initial peers test

* Remove duplicate code in rebase

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Conrado Gouvea 2021-10-27 20:46:43 -03:00 committed by GitHub
parent 3e03d48799
commit 8d01750459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 107 additions and 11 deletions

View File

@ -13,7 +13,11 @@ use futures::{
TryFutureExt,
};
use rand::seq::SliceRandom;
use tokio::{net::TcpListener, sync::broadcast, time::Instant};
use tokio::{
net::TcpListener,
sync::broadcast,
time::{sleep, Instant},
};
use tower::{
buffer::Buffer, discover::Change, layer::Layer, load::peak_ewma::PeakEwmaDiscover,
util::BoxService, Service, ServiceExt,
@ -248,7 +252,10 @@ where
// # Security
//
// TODO: rate-limit initial seed peer connections (#2326)
// Resists distributed denial of service attacks by making sure that
// new peer connections are initiated at least
// [`MIN_PEER_CONNECTION_INTERVAL`][constants::MIN_PEER_CONNECTION_INTERVAL]
// apart.
//
// # Correctness
//
@ -258,17 +265,24 @@ where
// single `FuturesUnordered` to completion, and handshakes have a short timeout.
let mut handshakes: FuturesUnordered<_> = initial_peers
.into_iter()
.map(|addr| {
.enumerate()
.map(|(i, addr)| {
let connection_tracker = active_outbound_connections.track_connection();
let req = OutboundConnectorRequest {
addr,
connection_tracker,
};
outbound_connector
.clone()
.oneshot(req)
.map_err(move |e| (addr, e))
let outbound_connector = outbound_connector.clone();
async move {
// Rate-limit the connection, sleeping for an interval according
// to its index in the list.
sleep(constants::MIN_PEER_CONNECTION_INTERVAL.saturating_mul(i as u32)).await;
outbound_connector
.oneshot(req)
.map_err(move |e| (addr, e))
.await
}
})
.collect();

View File

@ -17,13 +17,14 @@ use std::{
collections::HashSet,
net::{Ipv4Addr, SocketAddr},
sync::Arc,
time::Duration,
time::{Duration, Instant},
};
use futures::{
channel::{mpsc, oneshot},
FutureExt,
FutureExt, StreamExt,
};
use tokio::task::JoinHandle;
use tower::{discover::Change, service_fn, Service};
use tracing::Span;
@ -31,11 +32,11 @@ use zebra_chain::{chain_tip::NoChainTip, parameters::Network, serialization::Dat
use zebra_test::net::random_known_port;
use crate::{
init,
constants, init,
meta_addr::MetaAddr,
peer::{self, ErrorSlot, OutboundConnectorRequest},
peer_set::{
initialize::{crawl_and_dial, PeerChange},
initialize::{add_initial_peers, crawl_and_dial, PeerChange},
set::MorePeers,
ActiveConnectionCounter, CandidateSet,
},
@ -587,6 +588,42 @@ async fn crawler_peer_limit_default_connect_ok_stay_open() {
);
}
/// Test if the initial seed peer connections is rate-limited.
#[tokio::test]
async fn add_initial_peers_is_rate_limited() {
zebra_test::init();
// We don't need to actually connect to the peers; we only need to check
// if the connection attempts is rate-limited. Therefore, just return an error.
let outbound_connector =
service_fn(|_| async { Err("test outbound connector always returns errors".into()) });
const PEER_COUNT: usize = 10;
let before = Instant::now();
let (initial_peers_task_handle, peerset_rx) =
spawn_add_initial_peers(PEER_COUNT, outbound_connector);
let connections = peerset_rx.take(PEER_COUNT).collect::<Vec<_>>().await;
let elapsed = Instant::now() - before;
assert_eq!(connections.len(), PEER_COUNT);
// Make sure the rate limiting worked by checking if it took long enough
assert!(
elapsed > constants::MIN_PEER_CONNECTION_INTERVAL.saturating_mul((PEER_COUNT - 1) as u32),
"elapsed only {:?}",
elapsed
);
let initial_peers_result = initial_peers_task_handle.await;
assert!(
matches!(initial_peers_result, Ok(Ok(_))),
"unexpected error or panic in add_initial_peers task: {:?}",
initial_peers_result,
);
}
/// Open a local listener on `listen_addr` for `network`.
/// Asserts that the local listener address works as expected.
async fn local_listener_port_with(listen_addr: SocketAddr, network: Network) {
@ -763,3 +800,48 @@ where
(config, peerset_rx)
}
/// Initialize a task that connects to `peer_count` initial peers using the
/// given connector.
///
/// Dummy IPs are used.
///
/// Returns the task [`JoinHandle`], and the peer set receiver.
fn spawn_add_initial_peers<C>(
peer_count: usize,
outbound_connector: C,
) -> (
JoinHandle<Result<ActiveConnectionCounter, BoxError>>,
mpsc::Receiver<PeerChange>,
)
where
C: Service<
OutboundConnectorRequest,
Response = Change<SocketAddr, peer::Client>,
Error = BoxError,
> + Clone
+ Send
+ 'static,
C::Future: Send + 'static,
{
// Create a list of dummy IPs and initialize a config using them as the
// initial peers.
let mut peers = HashSet::new();
for address_number in 0..peer_count {
peers.insert(
SocketAddr::new(Ipv4Addr::new(127, 1, 1, address_number as _).into(), 1).to_string(),
);
}
let config = Config {
initial_mainnet_peers: peers,
network: Network::Mainnet,
..Config::default()
};
let (peerset_tx, peerset_rx) = mpsc::channel::<PeerChange>(peer_count + 1);
let add_fut = add_initial_peers(config, outbound_connector, peerset_tx);
let add_task_handle = tokio::spawn(add_fut);
(add_task_handle, peerset_rx)
}