Skip to main content

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}