zebra_network/peer_set/candidate_set/crawl.rs
1//! Crawling the network for more peer addresses.
2//!
3//! The crawler asks live [`Responded`] peers for more peers, validates the
4//! responses, and sends the new addresses to the address book. Candidate
5//! selection for outbound connections lives in the
6//! [parent module](super).
7//!
8//! [`Responded`]: crate::PeerAddrState::Responded
9
10use std::{
11 cmp::min,
12 task::{Context, Poll},
13};
14
15use futures::{
16 future::BoxFuture,
17 stream::{FuturesUnordered, StreamExt},
18 FutureExt,
19};
20use tokio::time::timeout;
21use tower::{Service, ServiceExt};
22
23use zebra_chain::serialization::DateTime32;
24
25use crate::{
26 address_book_updater::{AddressBookRequest, AddressBookService},
27 constants,
28 meta_addr::MetaAddrChange,
29 peer_set::set::MorePeers,
30 types::MetaAddr,
31 BoxError, Request, Response,
32};
33
34use super::RateLimitBySkipping;
35
36/// The service that crawls the network for more peer addresses.
37///
38/// # Security
39///
40/// Rate-limited to at most one crawl per
41/// [`MIN_PEER_GET_ADDR_INTERVAL`][constants::MIN_PEER_GET_ADDR_INTERVAL],
42/// to prevent sending a burst of repeated requests for new peer addresses.
43/// While the rate limit applies, crawls are skipped.
44///
45/// Clones share the same rate limit.
46pub(crate) type CrawlService<S> = RateLimitBySkipping<CrawlFanout<S>>;
47
48/// Crawl the network for more peer addresses, limiting the fanout to
49/// `fanout_limit` if it is `Some`.
50///
51/// - Ask a few live [`Responded`] peers to send us more peers.
52/// - Process all completed peer responses, adding new peers in the
53/// [`NeverAttemptedGossiped`] state.
54///
55/// Returns `Some(MorePeers)` if the crawl was successful and the crawler
56/// should ask for more peers. Returns `None` if there are no new peers.
57///
58/// ## Correctness
59///
60/// Pass the initial peer set size as `fanout_limit` during initialization,
61/// so that Zebra does not send duplicate requests to the same peer.
62///
63/// The crawler exits when a crawl returns an error, so errors are only
64/// returned on permanent failures.
65///
66/// The handshaker sets up the peer message receiver so it also sends a
67/// [`Responded`] peer address update.
68///
69/// [`next_reconnect_peer`] puts peers into the [`AttemptPending`] state.
70///
71/// ## Security
72///
73/// This call is rate-limited to prevent sending a burst of repeated requests for new peer
74/// addresses. Each call will only crawl the network if more time
75/// than [`MIN_PEER_GET_ADDR_INTERVAL`][constants::MIN_PEER_GET_ADDR_INTERVAL] has passed since
76/// the last crawl. Otherwise, the crawl is skipped.
77///
78/// [`Responded`]: crate::PeerAddrState::Responded
79/// [`NeverAttemptedGossiped`]: crate::PeerAddrState::NeverAttemptedGossiped
80/// [`Failed`]: crate::PeerAddrState::Failed
81/// [`AttemptPending`]: crate::PeerAddrState::AttemptPending
82/// [`next_reconnect_peer`]: super::next_reconnect_peer
83pub(crate) async fn crawl_once<S>(
84 crawl_service: &mut CrawlService<S>,
85 fanout_limit: Option<usize>,
86) -> Result<Option<MorePeers>, BoxError>
87where
88 S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
89 S::Future: Send + 'static,
90{
91 // SECURITY
92 //
93 // Rate limit sending `GetAddr` messages to peers:
94 // while the crawl service is rate-limited, calls are skipped,
95 // and return `None` without contacting any peers.
96 crawl_service.ready().await?.call(fanout_limit).await
97}
98
99/// A service that crawls the network for more peer addresses.
100///
101/// It fans out `GetPeers` requests to the peer set, validates the responses,
102/// and sends the validated addresses to the address book updater.
103///
104/// The request is an optional fanout limit, and the response is
105/// `Some(MorePeers)` if the crawl received any new peers.
106#[derive(Clone)]
107pub(crate) struct CrawlFanout<S>
108where
109 S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
110 S::Future: Send + 'static,
111{
112 /// The peer set used to crawl the network for peers.
113 peer_service: S,
114
115 /// The buffered service handle to the address book updater task.
116 address_book_service: AddressBookService,
117}
118
119impl<S> CrawlFanout<S>
120where
121 S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
122 S::Future: Send + 'static,
123{
124 /// Returns a rate-limited [`CrawlService`] that crawls using `peer_service`,
125 /// and stores the addresses it finds using `address_book_service`.
126 pub(super) fn service(
127 peer_service: S,
128 address_book_service: AddressBookService,
129 ) -> CrawlService<S> {
130 RateLimitBySkipping::new(
131 Self {
132 peer_service,
133 address_book_service,
134 },
135 constants::MIN_PEER_GET_ADDR_INTERVAL,
136 )
137 }
138}
139
140impl<S> Service<Option<usize>> for CrawlFanout<S>
141where
142 S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
143 S::Future: Send + 'static,
144{
145 type Response = Option<MorePeers>;
146 type Error = BoxError;
147 type Future = BoxFuture<'static, Result<Option<MorePeers>, BoxError>>;
148
149 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
150 // Readiness of the peer service is checked before each fanout
151 // request inside `call`.
152 Poll::Ready(Ok(()))
153 }
154
155 fn call(&mut self, fanout_limit: Option<usize>) -> Self::Future {
156 let peer_service = self.peer_service.clone();
157 let address_book_service = self.address_book_service.clone();
158
159 async move {
160 // CORRECTNESS
161 //
162 // Use a timeout to avoid deadlocks when there are no connected
163 // peers, and:
164 // - we're waiting on a handshake to complete so there are peers, or
165 // - another task that handles or adds peers is waiting on this task
166 // to complete.
167 match timeout(
168 constants::PEER_GET_ADDR_TIMEOUT,
169 crawl_fanout(peer_service, address_book_service, fanout_limit),
170 )
171 .await
172 {
173 Ok(fanout_result) => fanout_result,
174 Err(_elapsed) => {
175 // crawls must only return an error for permanent failures
176 info!("timeout waiting for peer service readiness or peer responses");
177 Ok(None)
178 }
179 }
180 }
181 .boxed()
182 }
183}
184
185/// Crawl the network for more peer addresses, limiting the fanout to
186/// `fanout_limit`.
187///
188/// Opportunistically crawl the network on every call to ensure
189/// we're actively fetching peers. Continue independently of whether we
190/// actually receive any peers, but always ask the network for more.
191///
192/// Because requests are load-balanced across existing peers, we can make
193/// multiple requests concurrently, which will be randomly assigned to
194/// existing peers, but we don't make too many because crawls may be
195/// started while the peer set is already loaded.
196///
197/// # Correctness
198///
199/// This function does not have a timeout.
200/// Use the [`CrawlFanout`] service instead.
201async fn crawl_fanout<S>(
202 mut peer_service: S,
203 address_book_service: AddressBookService,
204 fanout_limit: Option<usize>,
205) -> Result<Option<MorePeers>, BoxError>
206where
207 S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
208 S::Future: Send + 'static,
209{
210 let fanout_limit = fanout_limit
211 .map(|fanout_limit| min(fanout_limit, constants::GET_ADDR_FANOUT))
212 .unwrap_or(constants::GET_ADDR_FANOUT);
213 debug!(?fanout_limit, "sending GetPeers requests");
214
215 let mut responses = FuturesUnordered::new();
216 let mut more_peers = None;
217
218 // Launch requests
219 for attempt in 0..fanout_limit {
220 if attempt > 0 {
221 // Let other tasks run, so we're more likely to choose a different peer.
222 //
223 // TODO: move fanouts into the PeerSet, so we always choose different peers (#2214)
224 tokio::task::yield_now().await;
225 }
226
227 let peer_service = peer_service.ready().await?;
228 responses.push(peer_service.call(Request::Peers));
229 }
230
231 let mut address_book_updates = FuturesUnordered::new();
232
233 // Process responses
234 while let Some(rsp) = responses.next().await {
235 match rsp {
236 Ok(Response::Peers(addrs)) => {
237 trace!(
238 addr_count = ?addrs.len(),
239 ?addrs,
240 "got response to GetPeers"
241 );
242 let addrs = validate_addrs(addrs, DateTime32::now());
243 address_book_updates.push(send_addrs(address_book_service.clone(), addrs));
244 more_peers = Some(MorePeers);
245 }
246 Err(e) => {
247 // since we do a fanout, and new updates are triggered by
248 // each demand, we can ignore errors in individual responses
249 trace!(?e, "got error in GetPeers request");
250 }
251 Ok(_) => unreachable!("Peers requests always return Peers responses"),
252 }
253 }
254
255 // Wait until all the address book updates have finished
256 while let Some(()) = address_book_updates.next().await {}
257
258 Ok(more_peers)
259}
260
261/// Add new `addrs` to the address book.
262async fn send_addrs(
263 address_book_service: AddressBookService,
264 addrs: impl IntoIterator<Item = MetaAddr>,
265) {
266 // # Security
267 //
268 // New gossiped peers are rate-limited because:
269 // - Zebra initiates requests for new gossiped peers
270 // - the fanout is limited
271 // - the number of addresses per peer is limited
272 let addrs: Vec<MetaAddrChange> = addrs
273 .into_iter()
274 .map(MetaAddr::new_gossiped_change)
275 .map(|maybe_addr| maybe_addr.expect("Received gossiped peers always have services set"))
276 .collect();
277
278 debug!(count = ?addrs.len(), "sending gossiped addresses to the address book");
279
280 // Don't bother making a request if there are no addresses left.
281 if addrs.is_empty() {
282 return;
283 }
284
285 // Extend handles duplicate addresses internally.
286 //
287 // Correctness: box the request future, so its captured types don't
288 // leak into generic callers (rustc's async Send inference struggles
289 // with boxed trait objects inside generic spawned tasks).
290 let result = address_book_service
291 .oneshot(AddressBookRequest::ExtendGossiped(addrs))
292 .boxed()
293 .await;
294
295 if let Err(error) = result {
296 debug!(
297 ?error,
298 "error sending gossiped addresses to the address book, is Zebra shutting down?"
299 );
300 }
301}
302
303/// Check new `addrs` before adding them to the address book.
304///
305/// `last_seen_limit` is the maximum permitted last seen time, typically
306/// [`Utc::now`](chrono::Utc::now).
307///
308/// If the data in an address is invalid, this function can:
309/// - modify the address data, or
310/// - delete the address.
311///
312/// # Security
313///
314/// Adjusts untrusted last seen times so they are not in the future. This stops
315/// malicious peers keeping all their addresses at the front of the connection
316/// queue. Honest peers with future clock skew also get adjusted.
317///
318/// Rejects all addresses if any calculated times overflow or underflow.
319pub(super) fn validate_addrs(
320 addrs: impl IntoIterator<Item = MetaAddr>,
321 last_seen_limit: DateTime32,
322) -> impl Iterator<Item = MetaAddr> {
323 // Note: The address book handles duplicate addresses internally,
324 // so we don't need to de-duplicate addresses here.
325
326 // TODO:
327 // We should eventually implement these checks in this function:
328 // - Zebra should ignore peers that are older than 3 weeks (part of #1865)
329 // - Zebra should count back 3 weeks from the newest peer timestamp sent
330 // by the other peer, to compensate for clock skew
331
332 let mut addrs: Vec<_> = addrs.into_iter().collect();
333
334 limit_last_seen_times(&mut addrs, last_seen_limit);
335
336 addrs.into_iter()
337}
338
339/// Ensure all reported `last_seen` times are less than or equal to `last_seen_limit`.
340///
341/// This will consider all addresses as invalid if trying to offset their
342/// `last_seen` times to be before the limit causes an underflow.
343fn limit_last_seen_times(addrs: &mut Vec<MetaAddr>, last_seen_limit: DateTime32) {
344 let last_seen_times = addrs.iter().map(|meta_addr| {
345 meta_addr
346 .untrusted_last_seen()
347 .expect("unexpected missing last seen: should be provided by deserialization")
348 });
349 let oldest_seen = last_seen_times.clone().min().unwrap_or(DateTime32::MIN);
350 let newest_seen = last_seen_times.max().unwrap_or(DateTime32::MAX);
351
352 // If any time is in the future, adjust all times, to compensate for clock skew on honest peers
353 if newest_seen > last_seen_limit {
354 let offset = newest_seen
355 .checked_duration_since(last_seen_limit)
356 .expect("unexpected underflow: just checked newest_seen is greater");
357
358 // Check for underflow
359 if oldest_seen.checked_sub(offset).is_some() {
360 // No underflow is possible, so apply offset to all addresses
361 for addr in addrs {
362 let last_seen = addr
363 .untrusted_last_seen()
364 .expect("unexpected missing last seen: should be provided by deserialization");
365 let last_seen = last_seen
366 .checked_sub(offset)
367 .expect("unexpected underflow: just checked oldest_seen");
368
369 addr.set_untrusted_last_seen(last_seen);
370 }
371 } else {
372 // An underflow will occur, so reject all gossiped peers
373 addrs.clear();
374 }
375 }
376}