Rate-limit initial seed peer connections (#2943)
* Rate-limit initial seed peer connections * Revert "Rate-limit initial seed peer connections" This reverts commit f779a1eb9e1ffd5497e96dfe8ae1be4340d5d24d. * 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:
parent
3e03d48799
commit
8d01750459
|
|
@ -13,7 +13,11 @@ use futures::{
|
||||||
TryFutureExt,
|
TryFutureExt,
|
||||||
};
|
};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use tokio::{net::TcpListener, sync::broadcast, time::Instant};
|
use tokio::{
|
||||||
|
net::TcpListener,
|
||||||
|
sync::broadcast,
|
||||||
|
time::{sleep, Instant},
|
||||||
|
};
|
||||||
use tower::{
|
use tower::{
|
||||||
buffer::Buffer, discover::Change, layer::Layer, load::peak_ewma::PeakEwmaDiscover,
|
buffer::Buffer, discover::Change, layer::Layer, load::peak_ewma::PeakEwmaDiscover,
|
||||||
util::BoxService, Service, ServiceExt,
|
util::BoxService, Service, ServiceExt,
|
||||||
|
|
@ -248,7 +252,10 @@ where
|
||||||
|
|
||||||
// # Security
|
// # 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
|
// # Correctness
|
||||||
//
|
//
|
||||||
|
|
@ -258,17 +265,24 @@ where
|
||||||
// single `FuturesUnordered` to completion, and handshakes have a short timeout.
|
// single `FuturesUnordered` to completion, and handshakes have a short timeout.
|
||||||
let mut handshakes: FuturesUnordered<_> = initial_peers
|
let mut handshakes: FuturesUnordered<_> = initial_peers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|addr| {
|
.enumerate()
|
||||||
|
.map(|(i, addr)| {
|
||||||
let connection_tracker = active_outbound_connections.track_connection();
|
let connection_tracker = active_outbound_connections.track_connection();
|
||||||
let req = OutboundConnectorRequest {
|
let req = OutboundConnectorRequest {
|
||||||
addr,
|
addr,
|
||||||
connection_tracker,
|
connection_tracker,
|
||||||
};
|
};
|
||||||
|
|
||||||
outbound_connector
|
let outbound_connector = outbound_connector.clone();
|
||||||
.clone()
|
async move {
|
||||||
.oneshot(req)
|
// Rate-limit the connection, sleeping for an interval according
|
||||||
.map_err(move |e| (addr, e))
|
// 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();
|
.collect();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@ use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
net::{Ipv4Addr, SocketAddr},
|
net::{Ipv4Addr, SocketAddr},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{mpsc, oneshot},
|
channel::{mpsc, oneshot},
|
||||||
FutureExt,
|
FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
use tower::{discover::Change, service_fn, Service};
|
use tower::{discover::Change, service_fn, Service};
|
||||||
use tracing::Span;
|
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 zebra_test::net::random_known_port;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
init,
|
constants, init,
|
||||||
meta_addr::MetaAddr,
|
meta_addr::MetaAddr,
|
||||||
peer::{self, ErrorSlot, OutboundConnectorRequest},
|
peer::{self, ErrorSlot, OutboundConnectorRequest},
|
||||||
peer_set::{
|
peer_set::{
|
||||||
initialize::{crawl_and_dial, PeerChange},
|
initialize::{add_initial_peers, crawl_and_dial, PeerChange},
|
||||||
set::MorePeers,
|
set::MorePeers,
|
||||||
ActiveConnectionCounter, CandidateSet,
|
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`.
|
/// Open a local listener on `listen_addr` for `network`.
|
||||||
/// Asserts that the local listener address works as expected.
|
/// Asserts that the local listener address works as expected.
|
||||||
async fn local_listener_port_with(listen_addr: SocketAddr, network: Network) {
|
async fn local_listener_port_with(listen_addr: SocketAddr, network: Network) {
|
||||||
|
|
@ -763,3 +800,48 @@ where
|
||||||
|
|
||||||
(config, peerset_rx)
|
(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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue