1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use unb_client::{Endpoint, EndpointSet};
6use unb_runtime::WsError;
7
8use crate::connection::{ConnectError, PeerConnection};
9use crate::node::{Node, PeerLink};
10use crate::session::{CandidateFailure, CandidateOutcome, CandidateSession};
11
12#[doc(hidden)]
17pub trait EndpointDialer: Send + Sync {
18 fn supports(&self, kind: unb_client::TransportKind) -> bool;
19
20 fn dial(
21 &self,
22 endpoint: Endpoint,
23 ) -> Pin<Box<dyn Future<Output = Result<unb_runtime::Pipe, WsError>> + Send + 'static>>;
24}
25
26pub(crate) struct ReconnectCandidate {
27 pub(crate) identity: unb_core::NodeIdentity,
28 pub(crate) selected: PeerLink,
29 pub(crate) candidate_wire: Arc<unb_runtime::Wire>,
30}
31
32impl Node {
33 pub async fn connect(
38 self: &Arc<Self>,
39 endpoints: impl Into<EndpointSet>,
40 ) -> Result<PeerConnection, ConnectError> {
41 self.connect_using(endpoints.into(), None, None).await
42 }
43
44 #[cfg(feature = "hosting")]
45 pub(crate) async fn connect_expected(
46 self: &Arc<Self>,
47 endpoints: impl Into<EndpointSet>,
48 expected_peer: &str,
49 ) -> Result<PeerConnection, ConnectError> {
50 self.connect_using(endpoints.into(), None, Some(expected_peer))
51 .await
52 }
53
54 #[doc(hidden)]
60 pub async fn connect_with_dialer(
61 self: &Arc<Self>,
62 endpoints: impl Into<EndpointSet>,
63 dialer: Arc<dyn EndpointDialer>,
64 ) -> Result<PeerConnection, ConnectError> {
65 self.connect_using(endpoints.into(), Some(dialer), None)
66 .await
67 }
68
69 async fn connect_using(
70 self: &Arc<Self>,
71 set: EndpointSet,
72 dialer: Option<Arc<dyn EndpointDialer>>,
73 expected_peer: Option<&str>,
74 ) -> Result<PeerConnection, ConnectError> {
75 let key = set.cache_key();
76 let ordered = self
77 .dial_policy
78 .ordered_candidates(&key, &set)
79 .into_iter()
80 .filter(|endpoint| {
81 dialer
82 .as_ref()
83 .is_none_or(|dialer| dialer.supports(endpoint.kind))
84 })
85 .collect::<Vec<_>>();
86 if ordered.is_empty() {
87 return Err(ConnectError::NoSupportedEndpoint);
88 }
89 let mut last_error = ConnectError::NoSupportedEndpoint;
90 for endpoint in ordered {
91 match self
92 .try_candidate(&endpoint, expected_peer, dialer.as_ref())
93 .await
94 {
95 Ok((candidate, outcome)) => {
96 let identity = match outcome {
97 CandidateOutcome::Promoted(identity)
98 | CandidateOutcome::Duplicate(identity) => identity,
99 };
100 let Some(link) = self.peer(&identity.node_id).await else {
101 candidate.wire.shutdown();
102 let _ = candidate.cleaned.await;
103 last_error = ConnectError::Establishment {
104 message: format!(
105 "verified peer {:?} has no selected live session",
106 identity.node_id
107 ),
108 };
109 continue;
110 };
111 if n0_future::time::timeout(
112 crate::session::ROUTE_SYNC_TIMEOUT,
113 self.wait_for_selected_route(&identity.node_id, &link.session_id),
114 )
115 .await
116 .is_err()
117 {
118 candidate.wire.shutdown();
119 let _ = candidate.cleaned.await;
120 last_error = ConnectError::Establishment {
121 message: format!(
122 "verified peer {:?} did not publish its synchronized node route",
123 identity.node_id
124 ),
125 };
126 continue;
127 }
128 let connection = {
129 let mut connections = self
130 .connections
131 .write()
132 .unwrap_or_else(|poisoned| poisoned.into_inner());
133 if let Some(connection) = connections
134 .get(&identity.node_id)
135 .filter(|connection| !connection.is_terminal())
136 .cloned()
137 {
138 if connection.bind(
139 identity.clone(),
140 link.session_id.clone(),
141 link.wire.clone(),
142 ) {
143 connection.replace_endpoints(set.clone());
144 connection.replace_dialer(dialer.clone());
145 connection
146 } else {
147 let connection = PeerConnection::new(
148 Arc::downgrade(self),
149 identity.clone(),
150 set.clone(),
151 link.session_id.clone(),
152 link.wire.clone(),
153 dialer.clone(),
154 );
155 connections.insert(identity.node_id, connection.clone());
156 connection
157 }
158 } else {
159 let connection = PeerConnection::new(
160 Arc::downgrade(self),
161 identity.clone(),
162 set.clone(),
163 link.session_id.clone(),
164 link.wire.clone(),
165 dialer.clone(),
166 );
167 connections.insert(identity.node_id, connection.clone());
168 connection
169 }
170 };
171 self.dial_policy.record_winner(&key, endpoint.kind);
172 return Ok(connection);
173 }
174 Err(error) => last_error = error,
175 }
176 }
177 Err(last_error)
178 }
179
180 async fn wait_for_selected_route(&self, peer: &str, session_id: &str) {
181 let mut changes = self.route_changes();
182 loop {
183 let selected_session = self.peer(peer).await.map(|selected| selected.session_id);
184 let direct_route = matches!(
185 self.snapshot.load().node_core.resolve(peer),
186 unb_core::Resolution::Route(next_hop) if next_hop == peer
187 );
188 if selected_session.as_deref() == Some(session_id) && direct_route {
189 return;
190 }
191 if changes.changed().await.is_err() {
192 return;
193 }
194 }
195 }
196
197 async fn wait_for_direct_peer_route(&self, peer: &str) {
203 let mut changes = self.route_changes();
204 loop {
205 let selected = self.peer(peer).await.is_some();
206 let direct_route = matches!(
207 self.snapshot.load().node_core.resolve(peer),
208 unb_core::Resolution::Route(next_hop) if next_hop == peer
209 );
210 if selected && direct_route {
211 return;
212 }
213 if changes.changed().await.is_err() {
214 return;
215 }
216 }
217 }
218
219 async fn try_candidate(
220 self: &Arc<Self>,
221 endpoint: &Endpoint,
222 expected_peer: Option<&str>,
223 dialer: Option<&Arc<dyn EndpointDialer>>,
224 ) -> Result<(CandidateSession, CandidateOutcome), ConnectError> {
225 let deadline = self.dial_policy.attempt_timeout();
226 let candidate_dial = async {
227 match dialer {
228 Some(dialer) => dialer.dial(endpoint.clone()).await,
229 None => self.dial_policy.dial_candidate(endpoint).await,
230 }
231 };
232 let pipe = match n0_future::time::timeout(deadline, candidate_dial).await {
233 Ok(Ok(pipe)) => pipe,
234 Ok(Err(error)) => {
235 return Err(ConnectError::Dial {
236 transport: endpoint.kind,
237 message: error.to_string(),
238 })
239 }
240 Err(_) => {
241 return Err(ConnectError::DialTimedOut {
242 transport: endpoint.kind,
243 })
244 }
245 };
246 let candidate = self.establish(pipe, expected_peer.map(str::to_owned)).await;
247 let outcome = candidate.observed_outcome().await;
248 match outcome {
249 Ok(outcome) => Ok((candidate, outcome)),
250 Err(failure) => {
251 candidate.wire.shutdown();
252 let _ = candidate.cleaned.await;
253 Err(match (expected_peer, failure) {
254 (
255 Some(expected),
256 CandidateFailure::Retired {
257 reason: unb_core::RetirementReason::UnexpectedPeer,
258 identity,
259 },
260 ) => ConnectError::IdentityMismatch {
261 expected: expected.to_string(),
262 actual: identity.map(|identity| identity.node_id),
263 },
264 (_, CandidateFailure::Session(error)) => ConnectError::Establishment {
265 message: error.to_string(),
266 },
267 (_, CandidateFailure::Retired { reason, .. }) => ConnectError::Establishment {
268 message: format!("session retired during establishment: {reason:?}"),
269 },
270 (_, CandidateFailure::MissingIdentity) => ConnectError::Establishment {
271 message: "session completed without an admitted identity".into(),
272 },
273 })
274 }
275 }
276 }
277
278 pub(crate) async fn reconnect_peer(
279 self: &Arc<Self>,
280 peer: &str,
281 set: &EndpointSet,
282 dialer: Option<Arc<dyn EndpointDialer>>,
283 ) -> Result<ReconnectCandidate, ConnectError> {
284 let key = set.cache_key();
285 let ordered = self
286 .dial_policy
287 .ordered_candidates(&key, set)
288 .into_iter()
289 .filter(|endpoint| {
290 dialer
291 .as_ref()
292 .is_none_or(|dialer| dialer.supports(endpoint.kind))
293 })
294 .collect::<Vec<_>>();
295 if ordered.is_empty() {
296 return Err(ConnectError::NoSupportedEndpoint);
297 }
298 let mut last_error = ConnectError::NoSupportedEndpoint;
299 for endpoint in ordered {
300 match self
301 .try_candidate(&endpoint, Some(peer), dialer.as_ref())
302 .await
303 {
304 Ok((candidate, outcome)) => {
305 let identity = match outcome {
306 CandidateOutcome::Promoted(identity)
307 | CandidateOutcome::Duplicate(identity) => identity,
308 };
309 let Some(selected) = self.peer(&identity.node_id).await else {
310 candidate.wire.shutdown();
311 let _ = candidate.cleaned.await;
312 last_error = ConnectError::Establishment {
313 message: format!(
314 "verified peer {:?} has no selected live session",
315 identity.node_id
316 ),
317 };
318 continue;
319 };
320 if n0_future::time::timeout(
321 crate::session::ROUTE_SYNC_TIMEOUT,
322 self.wait_for_selected_route(&identity.node_id, &selected.session_id),
323 )
324 .await
325 .is_err()
326 {
327 candidate.wire.shutdown();
328 let _ = candidate.cleaned.await;
329 last_error = ConnectError::Establishment {
330 message: format!(
331 "verified peer {:?} did not publish its synchronized node route",
332 identity.node_id
333 ),
334 };
335 continue;
336 }
337 let Some(selected) = self.peer(&identity.node_id).await else {
338 candidate.wire.shutdown();
339 let _ = candidate.cleaned.await;
340 last_error = ConnectError::Establishment {
341 message: format!(
342 "verified peer {:?} lost its selected session after route synchronization",
343 identity.node_id
344 ),
345 };
346 continue;
347 };
348 self.dial_policy.record_winner(&key, endpoint.kind);
349 return Ok(ReconnectCandidate {
350 identity,
351 selected,
352 candidate_wire: candidate.wire,
353 });
354 }
355 Err(error) => last_error = error,
356 }
357 }
358 Err(last_error)
359 }
360
361 pub async fn link(self: &Arc<Self>, other: &Arc<Node>) -> Result<(), WsError> {
362 if Arc::ptr_eq(self, other) || self.identity.node_id == other.identity.node_id {
363 return Err(WsError::Connect("a node cannot link to itself".into()));
364 }
365 let (dial_side, accept_side) = unb_client::pair();
366 let left = self
367 .establish(dial_side, Some(other.identity.node_id.clone()))
368 .await;
369 let right = other
370 .establish(accept_side, Some(self.identity.node_id.clone()))
371 .await;
372 let result = match tokio::join!(
373 left.outcome(&other.identity.node_id),
374 right.outcome(&self.identity.node_id)
375 ) {
376 (Ok(CandidateOutcome::Promoted(_)), Ok(CandidateOutcome::Promoted(_))) => {
377 n0_future::time::timeout(crate::session::ROUTE_SYNC_TIMEOUT, async {
378 left.wire.routes_acked().await?;
379 right.wire.routes_acked().await?;
380 tokio::join!(
381 self.wait_for_direct_peer_route(&other.identity.node_id),
382 other.wait_for_direct_peer_route(&self.identity.node_id),
383 );
384 Ok::<(), WsError>(())
385 })
386 .await
387 .map_err(|_| {
388 WsError::Connect(
389 "linked peers did not publish their synchronized node routes".into(),
390 )
391 })?
392 }
393 (Err(error), _) | (_, Err(error)) => Err(error),
394 _ => Err(WsError::Connect("link closed during establishment".into())),
395 };
396 if result.is_err() {
397 left.wire.shutdown();
398 right.wire.shutdown();
399 let _ = tokio::join!(left.cleaned, right.cleaned);
400 }
401 result
402 }
403
404 pub async fn connect_transport_unchecked(
407 self: &Arc<Self>,
408 transport: unb_runtime::Pipe,
409 ) -> Result<(), WsError> {
410 let candidate = self.establish(transport, None).await;
411 match candidate.outcome("candidate").await {
412 Ok(CandidateOutcome::Promoted(_) | CandidateOutcome::Duplicate(_)) => {
413 let synchronized = n0_future::time::timeout(
414 crate::session::ROUTE_SYNC_TIMEOUT,
415 candidate.wire.routes_acked(),
416 )
417 .await
418 .map_err(|_| {
419 WsError::Connect(
420 "connected peer did not acknowledge its synchronized routes".into(),
421 )
422 })
423 .and_then(|result| result);
424 if let Err(error) = synchronized {
425 candidate.wire.shutdown();
426 let _ = candidate.cleaned.await;
427 return Err(error);
428 }
429 Ok(())
430 }
431 Err(error) => {
432 candidate.wire.shutdown();
433 let _ = candidate.cleaned.await;
434 Err(error)
435 }
436 }
437 }
438}