Skip to main content

zebra_network/peer_set/
candidate_set.rs

1//! Candidate peer selection for outbound connections.
2//!
3//! The crawler in [`crawl_and_dial`][crate::peer_set::initialize] manages
4//! outbound peer connection attempts using the services in this module.
5//! Successful connections become peers in the
6//! [`PeerSet`](super::set::PeerSet).
7//!
8//! Candidate selection divides the set of all possible outbound peers into
9//! disjoint subsets, using the [`PeerAddrState`](crate::PeerAddrState):
10//!
11//! 1. [`Responded`] peers, which we have had an outbound connection to.
12//! 2. [`NeverAttemptedGossiped`] peers, which we learned about from other peers
13//!    but have never connected to. This includes gossiped peers, DNS seeder peers,
14//!    cached peers, canonical addresses from the [`Version`] messages of inbound
15//!    and outbound connections, and remote IP addresses of inbound connections.
16//! 3. [`Failed`] peers, which failed a connection attempt, or had an error
17//!    during an outbound connection.
18//! 4. [`AttemptPending`] peers, which we've recently queued for a connection.
19//!
20//! Never attempted peers are always available for connection.
21//!
22//! If a peer's attempted, responded, or failure time is recent
23//! (within the liveness limit), we avoid reconnecting to it.
24//! Otherwise, we assume that it has disconnected or hung,
25//! and attempt reconnection.
26//!
27//! ```ascii,no_run
28//!                         ┌──────────────────┐
29//!                         │   Config / DNS   │
30//!             ┌───────────│       Seed       │───────────┐
31//!             │           │    Addresses     │           │
32//!             │           └──────────────────┘           │
33//!             │                    │ untrusted_last_seen │
34//!             │                    │     is unknown      │
35//!             ▼                    │                     ▼
36//!    ┌──────────────────┐          │          ┌──────────────────┐
37//!    │    Handshake     │          │          │     Peer Set     │
38//!    │    Canonical     │──────────┼──────────│     Gossiped     │
39//!    │    Addresses     │          │          │    Addresses     │
40//!    └──────────────────┘          │          └──────────────────┘
41//!     untrusted_last_seen          │                provides
42//!         set to now               │           untrusted_last_seen
43//!                                  ▼
44//!                                  Λ   if attempted, responded, or failed:
45//!                                 ╱ ╲         ignore gossiped info
46//!                                ▕   ▏    otherwise, if never attempted:
47//!                                 ╲ ╱    skip updates to existing fields
48//!                                  V
49//!  ┌───────────────────────────────┼───────────────────────────────┐
50//!  │ AddressBook                   │                               │
51//!  │ disjoint `PeerAddrState`s     ▼                               │
52//!  │ ┌─────────────┐  ┌─────────────────────────┐  ┌─────────────┐ │
53//!  │ │ `Responded` │  │`NeverAttemptedGossiped` │  │  `Failed`   │ │
54//! ┌┼▶│    Peers    │  │          Peers          │  │   Peers     │◀┼┐
55//! ││ └─────────────┘  └─────────────────────────┘  └─────────────┘ ││
56//! ││        │                      │                      │        ││
57//! ││ #1 oldest_first        #2 newest_first        #3 oldest_first ││
58//! ││        ├──────────────────────┴──────────────────────┘        ││
59//! ││        ▼                                                      ││
60//! ││        Λ                                                      ││
61//! ││       ╱ ╲              filter by                              ││
62//! ││      ▕   ▏   is_ready_for_connection_attempt                  ││
63//! ││       ╲ ╱     to remove recent `Responded`,                   ││
64//! ││        V  `AttemptPending`, and `Failed` peers                ││
65//! ││        │                                                      ││
66//! ││        │    try outbound connection,                          ││
67//! ││        ▼  update last_attempt to now()                        ││
68//! ││┌────────────────┐                                             ││
69//! │││`AttemptPending`│                                             ││
70//! │││     Peers      │                                             ││
71//! ││└────────────────┘                                             ││
72//! │└────────┼──────────────────────────────────────────────────────┘│
73//! │         ▼                                                       │
74//! │         Λ                                                       │
75//! │        ╱ ╲                                                      │
76//! │       ▕   ▏─────────────────────────────────────────────────────┘
77//! │        ╲ ╱   connection failed, update last_failure to now()
78//! │         V
79//! │         │
80//! │         │ connection succeeded
81//! │         ▼
82//! │  ┌────────────┐
83//! │  │    send    │
84//! │  │peer::Client│
85//! │  │to Discover │
86//! │  └────────────┘
87//! │         │
88//! │         ▼
89//! │┌───────────────────────────────────────┐
90//! ││ when connection succeeds, and every   │
91//! ││  time we receive a peer heartbeat:    │
92//! └│  * update state to `Responded`        │
93//!  │  * update last_response to now()      │
94//!  └───────────────────────────────────────┘
95//! ```
96//!
97//! [`Responded`]: crate::PeerAddrState::Responded
98//! [`Version`]: crate::protocol::external::types::Version
99//! [`NeverAttemptedGossiped`]: crate::PeerAddrState::NeverAttemptedGossiped
100//! [`Failed`]: crate::PeerAddrState::Failed
101//! [`AttemptPending`]: crate::PeerAddrState::AttemptPending
102//
103// TODO:
104//   * show all possible transitions between Attempt/Responded/Failed,
105//     except Failed -> Responded is invalid, must go through Attempt
106
107use futures::FutureExt;
108use tower::{Service, ServiceExt};
109
110use crate::{
111    address_book_updater::{AddressBookRequest, AddressBookResponse, AddressBookService},
112    constants,
113    types::MetaAddr,
114    BoxError, Request, Response,
115};
116
117mod crawl;
118mod rate_limit;
119
120use crawl::CrawlFanout;
121pub(crate) use crawl::{crawl_once, CrawlService};
122pub(crate) use rate_limit::{RateLimitBySkipping, RateLimitOnYield};
123
124#[cfg(test)]
125mod tests;
126
127/// The service that chooses the next reconnection candidate.
128///
129/// # Security
130///
131/// Rate-limited so new outbound connections are started at least
132/// [`MIN_OUTBOUND_PEER_CONNECTION_INTERVAL`][constants::MIN_OUTBOUND_PEER_CONNECTION_INTERVAL]
133/// apart. The rate limit is only charged when a candidate is actually
134/// yielded: an empty address book returns `None` immediately.
135///
136/// Clones share the same rate limit.
137pub(crate) type NextPeerService = RateLimitOnYield<AddressBookService, AddressBookResponse>;
138
139/// Builds the candidate selection services used by the crawler:
140/// a [`NextPeerService`] and a [`CrawlService`].
141///
142/// Uses `address_book_service` to choose candidates and store crawled
143/// addresses, and `peer_service` to crawl the network for more addresses.
144pub(crate) fn crawler_services<S>(
145    address_book_service: AddressBookService,
146    peer_service: S,
147) -> (NextPeerService, CrawlService<S>)
148where
149    S: Service<Request, Response = Response, Error = BoxError> + Clone + Send + 'static,
150    S::Future: Send + 'static,
151{
152    let next_peer_service = RateLimitOnYield::new(
153        address_book_service.clone(),
154        constants::MIN_OUTBOUND_PEER_CONNECTION_INTERVAL,
155        |response| matches!(response, AddressBookResponse::NextReconnectPeer(Some(_))),
156    );
157
158    let crawl_service = CrawlFanout::service(peer_service, address_book_service);
159
160    (next_peer_service, crawl_service)
161}
162
163/// Returns the next candidate for a connection attempt, if any are available.
164///
165/// Returns peers in reconnection order, based on
166/// [`AddressBook::reconnection_peers`](crate::AddressBook::reconnection_peers).
167///
168/// Skips peers that have recently been active, attempted, or failed.
169///
170/// ## Correctness
171///
172/// `AttemptPending` peers will become [`Responded`] if they respond, or
173/// become `Failed` if they time out or provide a bad response.
174///
175/// Live [`Responded`] peers will stay live if they keep responding, or
176/// become a reconnection candidate if they stop responding.
177///
178/// ## Security
179///
180/// Zebra resists distributed denial of service attacks by making sure that
181/// new peer connections are initiated at least
182/// [`MIN_OUTBOUND_PEER_CONNECTION_INTERVAL`][constants::MIN_OUTBOUND_PEER_CONNECTION_INTERVAL]
183/// apart. If a peer was recently provided, then this future will sleep
184/// until the rate-limit has passed.
185///
186/// [`Responded`]: crate::PeerAddrState::Responded
187pub(crate) async fn next_reconnect_peer(
188    next_peer_service: &mut NextPeerService,
189) -> Option<MetaAddr> {
190    // Atomically choose the next peer and mark it as `AttemptPending`,
191    // in a single address book updater request.
192    //
193    // Security: new outbound peer connections are rate-limited by the
194    // [`RateLimitOnYield`] middleware, which only sleeps before yielding
195    // an address: when there is no peer, `None` is returned immediately.
196    let response = match next_peer_service.ready().await {
197        Ok(next_peer_service) => {
198            next_peer_service
199                .call(AddressBookRequest::NextReconnectPeer)
200                .await
201        }
202        Err(error) => Err(error),
203    };
204
205    match response {
206        Ok(AddressBookResponse::NextReconnectPeer(next_peer)) => next_peer,
207        Ok(_) => unreachable!("NextReconnectPeer requests always return NextReconnectPeer"),
208        Err(error) => {
209            debug!(
210                ?error,
211                "error requesting next reconnection peer, is Zebra shutting down?"
212            );
213            None
214        }
215    }
216}
217
218/// Returns the number of candidate peers that are currently ready for a
219/// connection attempt, as selected by [`next_reconnect_peer`].
220///
221/// The returned count is a snapshot: candidates can become ready or be
222/// attempted by other tasks immediately afterwards.
223pub(crate) async fn ready_peer_count(address_book_service: &AddressBookService) -> usize {
224    let response = address_book_service
225        .clone()
226        .oneshot(AddressBookRequest::ReadyPeerCount)
227        .boxed()
228        .await;
229
230    match response {
231        Ok(AddressBookResponse::ReadyPeerCount(ready_peer_count)) => ready_peer_count,
232        Ok(_) => unreachable!("ReadyPeerCount requests always return ReadyPeerCount"),
233        Err(error) => {
234            debug!(
235                ?error,
236                "error requesting ready peer count, is Zebra shutting down?"
237            );
238            0
239        }
240    }
241}