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}