1use std::collections::HashSet;
17use std::future::Future;
18use std::sync::Arc;
19use std::time::Duration;
20
21use tokio::time::Instant;
22
23use crate::cbor::Value;
24use crate::frame::StreamMode;
25use crate::record::{self, RecordType, Trust, Verified};
26use crate::seal::KEY_ID_SIZE;
27use crate::station_link::{
28 self, Confidentiality, ConfidentialityError, ConfidentialityReason, Link, LinkError, Report,
29 Seal, Stream, DEFAULT_CALL_TIMEOUT,
30};
31use crate::transport::Target;
32
33use super::member::Member;
34use super::{Pool, PoolError, PoolInner};
35
36const MIN_CANDIDATE_SHARE: Duration = Duration::from_secs(1);
38
39#[derive(Debug, Clone, PartialEq)]
44pub struct Call {
45 pub realm: [u8; 32],
46 pub procedure: String,
47 pub provider: [u8; 32],
48 pub payload: Value,
49 pub timeout: Duration,
50 pub token: Option<Vec<u8>>,
51 pub proofs: Vec<Vec<u8>>,
52 pub confidential: Confidentiality,
53}
54
55impl Default for Call {
56 fn default() -> Self {
57 Call {
58 realm: [0; 32],
59 procedure: String::new(),
60 provider: [0; 32],
61 payload: Value::Map(Vec::new()),
62 timeout: Duration::ZERO,
63 token: None,
64 proofs: Vec::new(),
65 confidential: Confidentiality::Preferred,
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq)]
75pub struct StreamCall {
76 pub realm: [u8; 32],
77 pub procedure: String,
78 pub provider: [u8; 32],
79 pub mode: StreamMode,
80 pub payload: Value,
81 pub deadline: Duration,
82 pub token: Option<Vec<u8>>,
83 pub proofs: Vec<Vec<u8>>,
84 pub confidential: Confidentiality,
85}
86
87impl Default for StreamCall {
88 fn default() -> Self {
89 StreamCall {
90 realm: [0; 32],
91 procedure: String::new(),
92 provider: [0; 32],
93 mode: StreamMode::ServerStream,
94 payload: Value::Map(Vec::new()),
95 deadline: Duration::ZERO,
96 token: None,
97 proofs: Vec::new(),
98 confidential: Confidentiality::Preferred,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct Provider {
106 pub node: [u8; 32],
107 pub station: [u8; 32],
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
113pub(super) struct Candidate {
114 provider: Provider,
115 expires_at: u64,
116 created_at: u64,
117 kem_key: Option<Vec<u8>>,
118}
119
120impl Candidate {
121 fn seal(&self) -> Seal {
124 match &self.kem_key {
125 Some(key) => Seal::To(key.clone()),
126 None => Seal::Clear,
127 }
128 }
129
130 fn kem_key_id(&self) -> Option<[u8; KEY_ID_SIZE]> {
131 self.kem_key.as_deref().map(crate::seal::key_id)
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Hash)]
136pub(super) struct ResolvedKey {
137 realm: [u8; 32],
138 procedure: String,
139 provider: [u8; 32],
140}
141
142impl Pool {
143 pub async fn call(&self, c: Call) -> Result<Value, PoolError> {
148 self.call_report(c).await.map(|(result, _)| result)
149 }
150
151 pub async fn call_report(&self, c: Call) -> Result<(Value, Report), PoolError> {
158 let inner = &self.inner;
159 let realm_key = inner.realm_key_for(&c.realm, &c.procedure)?;
160 let timeout = if c.timeout.is_zero() {
161 DEFAULT_CALL_TIMEOUT
162 } else {
163 c.timeout
164 };
165 let deadline = Instant::now() + timeout;
166 let key = ResolvedKey {
167 realm: c.realm,
168 procedure: c.procedure.clone(),
169 provider: c.provider,
170 };
171 let candidates = bounded(deadline, inner.candidates(&key, realm_key.clone())).await?;
172 let candidates = callable(candidates, c.confidential)?;
173 let (link, cand) = first_reached(inner, &key, candidates, deadline).await?;
174 let outcome = bounded(deadline, inner.call_at(&link, &cand, &c, deadline)).await;
175 let sent = Sent {
176 link: &link,
177 call: &c,
178 realm_key,
179 deadline,
180 };
181 let (cand, outcome) = inner.resealed(&key, cand, &sent, outcome).await;
182 inner.settled(key, cand, outcome)
183 }
184
185 pub async fn providers(
189 &self,
190 realm: &[u8; 32],
191 procedure: &str,
192 ) -> Result<Vec<Provider>, PoolError> {
193 let realm_key = self.inner.realm_key_for(realm, procedure)?;
194 let key = ResolvedKey {
195 realm: *realm,
196 procedure: procedure.to_string(),
197 provider: [0; 32],
198 };
199 let found = self.inner.resolve(&key, realm_key).await?;
200 Ok(found.into_iter().map(|c| c.provider).collect())
201 }
202
203 pub async fn open_stream(&self, c: StreamCall) -> Result<Stream, PoolError> {
209 let inner = &self.inner;
210 let realm_key = inner.realm_key_for(&c.realm, &c.procedure)?;
211 let deadline = Instant::now() + DEFAULT_CALL_TIMEOUT;
212 let key = ResolvedKey {
213 realm: c.realm,
214 procedure: c.procedure.clone(),
215 provider: c.provider,
216 };
217 let candidates = bounded(deadline, inner.candidates(&key, realm_key)).await?;
218 let candidates = callable(candidates, c.confidential)?;
219 let (link, cand) = first_reached(inner, &key, candidates, deadline).await?;
220 let outcome = bounded(deadline, inner.open_at(&link, &cand, &c)).await;
221 inner.settled(key, cand, outcome)
222 }
223
224 pub async fn station_target(&self, station: &[u8; 32]) -> Result<Target, PoolError> {
227 self.inner.station_target(station).await
228 }
229
230 pub async fn link_to(&self, station: &[u8; 32]) -> Result<Link, PoolError> {
235 let deadline = Instant::now() + DEFAULT_CALL_TIMEOUT;
236 self.inner.link_to(station, deadline).await
237 }
238
239 pub async fn find_record(&self, key: &[u8; 32]) -> Result<Verified, PoolError> {
241 self.inner
242 .first_answer(|l| async move { l.find_record(key).await })
243 .await
244 }
245
246 pub async fn find_records(&self, key: &[u8; 32]) -> Result<(Vec<Verified>, usize), PoolError> {
249 self.inner
250 .first_answer(|l| async move { l.find_records(key).await })
251 .await
252 }
253
254 pub async fn find_records_by_type(
257 &self,
258 t: RecordType,
259 ) -> Result<(Vec<Verified>, usize), PoolError> {
260 self.inner
261 .first_answer(|l| async move { l.find_records_by_type(t).await })
262 .await
263 }
264
265 pub async fn put_record(&self, wire: &[u8]) -> Result<(), PoolError> {
267 self.inner
268 .first_answer(|l| async move { l.put_record(wire).await })
269 .await
270 }
271}
272
273impl PoolInner {
274 fn remember(&self, key: ResolvedKey, cand: Candidate) {
275 self.lock().remember.insert(key, cand);
276 }
277
278 fn forget(&self, key: &ResolvedKey) {
279 self.lock().remember.remove(key);
280 }
281
282 async fn candidates(
285 &self,
286 key: &ResolvedKey,
287 realm_key: Option<Vec<u8>>,
288 ) -> Result<Vec<Candidate>, PoolError> {
289 let remembered = self.lock().remember.get(key).cloned();
290 let live = remembered.filter(|cand| {
291 cand.expires_at as i64 > now_ms() && self.linked_to(&cand.provider.station).is_some()
292 });
293 if let Some(cand) = live {
294 return Ok(vec![cand]);
295 }
296 self.resolve(key, realm_key).await
297 }
298
299 async fn resolve(
302 &self,
303 key: &ResolvedKey,
304 realm_key: Option<Vec<u8>>,
305 ) -> Result<Vec<Candidate>, PoolError> {
306 let slot = record::procedure_key(&key.realm, &key.procedure);
307 let (found, _) = self
308 .first_answer(|l| async move { l.find_records(&slot).await })
309 .await?;
310 let now = now_ms();
311 let trust = Trust {
312 profile: self.opts.identity.profile(),
313 realm_key,
314 };
315 let mut out: Vec<Candidate> = found
316 .iter()
317 .filter(|v| v.record().record_type == RecordType::PROCEDURE_ADVERTISEMENT)
318 .filter_map(|v| trusted_candidate(v, key, &trust, now))
319 .collect();
320 if out.is_empty() {
321 return Err(PoolError::NoProvider(Vec::new()));
322 }
323 out.sort_by_key(|c| std::cmp::Reverse(c.created_at));
324 Ok(out)
325 }
326
327 async fn reach(self: &Arc<Self>, cand: &Candidate, share: Instant) -> Result<Link, PoolError> {
330 bounded(share, self.link_to(&cand.provider.station, share)).await
331 }
332
333 fn settled<T>(
337 &self,
338 key: ResolvedKey,
339 cand: Candidate,
340 outcome: Result<T, PoolError>,
341 ) -> Result<T, PoolError> {
342 match &outcome {
343 Ok(_) | Err(PoolError::Link(LinkError::Provider { .. })) => self.remember(key, cand),
344 Err(_) => self.forget(&key),
345 }
346 outcome
347 }
348
349 async fn call_at(
354 &self,
355 link: &Link,
356 cand: &Candidate,
357 c: &Call,
358 deadline: Instant,
359 ) -> Result<(Value, Report), PoolError> {
360 let left = deadline.saturating_duration_since(Instant::now());
361 let result = link
362 .call(station_link::Call {
363 realm: c.realm,
364 procedure: c.procedure.clone(),
365 target: cand.provider.node,
366 payload: c.payload.clone(),
367 timeout: left.max(Duration::from_millis(1)),
368 token: c.token.clone(),
369 proofs: c.proofs.clone(),
370 seal: Some(cand.seal()),
371 })
372 .await?;
373 Ok((result, Report::of(cand.provider.node, cand.kem_key_id())))
374 }
375
376 async fn resealed(
386 &self,
387 key: &ResolvedKey,
388 cand: Candidate,
389 sent: &Sent<'_>,
390 outcome: Result<(Value, Report), PoolError>,
391 ) -> (Candidate, Result<(Value, Report), PoolError>) {
392 let named = match &outcome {
393 Err(PoolError::Link(LinkError::SealedRefused { named })) if cand.kem_key.is_some() => {
394 *named
395 }
396 _ => return (cand, outcome),
397 };
398 let own = ResolvedKey {
399 provider: cand.provider.node,
400 ..key.clone()
401 };
402 let fresh = bounded(sent.deadline, self.resolve(&own, sent.realm_key.clone())).await;
403 let next = match reseal_to(fresh, named) {
404 Ok(next) => next,
405 Err(e) => return (cand, Err(e)),
406 };
407 let resent = self.call_at(sent.link, &next, sent.call, sent.deadline);
408 let outcome = bounded(sent.deadline, resent).await;
409 (next, outcome)
410 }
411
412 async fn open_at(
414 &self,
415 link: &Link,
416 cand: &Candidate,
417 c: &StreamCall,
418 ) -> Result<Stream, PoolError> {
419 Ok(link
420 .open_stream(station_link::StreamCall {
421 realm: c.realm,
422 procedure: c.procedure.clone(),
423 target: cand.provider.node,
424 mode: c.mode,
425 payload: c.payload.clone(),
426 deadline: c.deadline,
427 token: c.token.clone(),
428 proofs: c.proofs.clone(),
429 seal: Some(cand.seal()),
430 })
431 .await?)
432 }
433
434 fn linked_to(&self, station: &[u8; 32]) -> Option<Link> {
436 self.links()
437 .into_iter()
438 .find(|l| l.station_node_id() == *station)
439 }
440
441 pub(super) async fn link_to(
442 self: &Arc<Self>,
443 station: &[u8; 32],
444 deadline: Instant,
445 ) -> Result<Link, PoolError> {
446 if let Some(link) = self.linked_to(station) {
447 return Ok(link);
448 }
449 let (existing, direct) = self.member_for(station)?;
450 let fresh = existing.is_none();
451 let member = match existing {
452 Some(m) => m,
453 None => self.start_direct(station, direct, deadline).await?,
454 };
455 if let Some(link) = member.await_up(deadline, fresh).await {
456 return Ok(link);
457 }
458 if fresh {
459 self.drop_member(&member);
460 }
461 Err(PoolError::StationNotReached {
462 station: *station,
463 cause: member.last_error(),
464 })
465 }
466
467 fn member_for(&self, station: &[u8; 32]) -> Result<(Option<Arc<Member>>, usize), PoolError> {
470 let state = self.lock();
471 if state.closed {
472 return Err(PoolError::Closed);
473 }
474 let existing = state
475 .members
476 .iter()
477 .find(|m| m.target.expected_node_id == *station)
478 .cloned();
479 Ok((existing, state.members.iter().filter(|m| m.direct).count()))
480 }
481
482 async fn start_direct(
485 self: &Arc<Self>,
486 station: &[u8; 32],
487 direct: usize,
488 deadline: Instant,
489 ) -> Result<Arc<Member>, PoolError> {
490 if direct >= self.opts.max_direct_links {
491 return Err(PoolError::DirectLinksFull);
492 }
493 let target = bounded(deadline, self.station_target(station)).await?;
494 Ok(self.start_member(target, true))
495 }
496
497 async fn station_target(&self, station: &[u8; 32]) -> Result<Target, PoolError> {
498 let slot = record::station_endpoint_key(station);
499 let verified = self
500 .first_answer(|l| async move { l.find_record(&slot).await })
501 .await
502 .map_err(|e| match e {
503 PoolError::Link(link) => PoolError::NoStationEndpoint(Some(link)),
504 other => other,
505 })?;
506 let r = verified.record();
507 let signer = r.signed.as_ref().map(|s| s.key_id);
508 let endpoint = record::read_station_endpoint(r)
509 .map_err(|e| PoolError::NoStationEndpoint(Some(e.into())))?;
510 match (signer, endpoint.host_advertised.first()) {
511 (Some(signer), Some(host)) if signer == *station && endpoint.quic_port != 0 => {
512 Ok(Target {
513 host: host.clone(),
514 port: endpoint.quic_port,
515 profile: self.opts.identity.profile(),
516 expected_node_id: *station,
517 })
518 }
519 _ => Err(PoolError::NoStationEndpoint(None)),
520 }
521 }
522
523 pub(super) async fn first_answer<'a, T, F, Fut>(&self, ask: F) -> Result<T, PoolError>
527 where
528 F: Fn(Link) -> Fut,
529 Fut: Future<Output = Result<T, LinkError>> + 'a,
530 {
531 let links = self.links();
532 if links.is_empty() {
533 return Err(PoolError::NoLink(Vec::new()));
534 }
535 let mut errors = Vec::new();
536 for link in links {
537 match ask(link).await {
538 Err(e) if unreachable(&e) => errors.push(e),
539 answered => return answered.map_err(PoolError::Link),
540 }
541 }
542 Err(PoolError::NoLink(errors))
543 }
544}
545
546struct Sent<'a> {
549 link: &'a Link,
550 call: &'a Call,
551 realm_key: Option<Vec<u8>>,
552 deadline: Instant,
553}
554
555fn reseal_to(
562 found: Result<Vec<Candidate>, PoolError>,
563 named: Option<[u8; KEY_ID_SIZE]>,
564) -> Result<Candidate, PoolError> {
565 let keyed: Vec<Candidate> = found
566 .unwrap_or_default()
567 .into_iter()
568 .filter(|c| c.kem_key.is_some())
569 .collect();
570 let advertised: Vec<[u8; KEY_ID_SIZE]> =
571 keyed.iter().filter_map(Candidate::kem_key_id).collect();
572 let chosen = match named {
573 Some(id) => keyed.into_iter().find(|c| c.kem_key_id() == Some(id)),
574 None => keyed.into_iter().next(),
575 };
576 let reason = match advertised.is_empty() {
577 true => ConfidentialityReason::NoKemKey,
578 false => ConfidentialityReason::KeyMismatch,
579 };
580 chosen.ok_or(PoolError::Confidentiality(ConfidentialityError {
581 reason,
582 advertised,
583 named,
584 }))
585}
586
587async fn first_reached(
592 inner: &Arc<PoolInner>,
593 key: &ResolvedKey,
594 candidates: Vec<Candidate>,
595 deadline: Instant,
596) -> Result<(Link, Candidate), PoolError> {
597 let mut tried = Vec::new();
598 let count = candidates.len();
599 for (i, cand) in candidates.into_iter().enumerate() {
600 match inner
601 .reach(&cand, candidate_share(deadline, count - i))
602 .await
603 {
604 Ok(link) => return Ok((link, cand)),
605 Err(PoolError::Closed) => return Err(PoolError::Closed),
606 Err(e) => {
607 inner.forget(key);
608 tried.push((cand.provider, e));
609 }
610 }
611 if Instant::now() >= deadline {
612 break;
613 }
614 }
615 Err(PoolError::NoProvider(tried))
616}
617
618fn trusted_candidate(
622 v: &Verified,
623 key: &ResolvedKey,
624 trust: &Trust,
625 now: i64,
626) -> Option<Candidate> {
627 let ad = record::read_procedure_advertisement(v.record()).ok()?;
628 let wanted = ad.realm_id == key.realm
629 && ad.procedure == key.procedure
630 && (key.provider == [0; 32] || ad.advertiser_node == key.provider);
631 if !wanted || record::verify_authorization(v, trust, now).is_err() {
632 return None;
633 }
634 Some(Candidate {
635 provider: Provider {
636 node: ad.advertiser_node,
637 station: ad.serving_station,
638 },
639 expires_at: v.record().expires_at,
640 created_at: v.record().created_at,
641 kem_key: ad.kem_key.map(|(key, _)| key),
642 })
643}
644
645fn unreachable(e: &LinkError) -> bool {
648 matches!(
649 e,
650 LinkError::CallTimeout
651 | LinkError::Closed
652 | LinkError::LivenessLost
653 | LinkError::V5DowngradeRefused
654 | LinkError::Io(_)
655 | LinkError::Goodbye(_)
656 | LinkError::StatusExpired
657 | LinkError::BindingExpired
658 )
659}
660
661fn candidate_share(deadline: Instant, left: usize) -> Instant {
665 let now = Instant::now();
666 let remaining = deadline.saturating_duration_since(now);
667 (now + (remaining / left.max(1) as u32).max(MIN_CANDIDATE_SHARE)).min(deadline)
668}
669
670async fn bounded<T>(
672 deadline: Instant,
673 work: impl Future<Output = Result<T, PoolError>>,
674) -> Result<T, PoolError> {
675 tokio::time::timeout_at(deadline, work)
676 .await
677 .unwrap_or(Err(PoolError::Link(LinkError::CallTimeout)))
678}
679
680fn now_ms() -> i64 {
681 crate::uuid_v7::now_ms() as i64
682}
683
684fn callable(
694 candidates: Vec<Candidate>,
695 confidential: Confidentiality,
696) -> Result<Vec<Candidate>, PoolError> {
697 if confidential == Confidentiality::Off {
698 return Err(PoolError::InvalidOpts(
699 "confidential off is refused for a pool call or open: it is preferred or required"
700 .into(),
701 ));
702 }
703 let advertised: Vec<[u8; KEY_ID_SIZE]> = candidates
704 .iter()
705 .filter_map(Candidate::kem_key_id)
706 .collect();
707 let keyed: HashSet<[u8; 32]> = candidates
708 .iter()
709 .filter(|c| c.kem_key.is_some())
710 .map(|c| c.provider.node)
711 .collect();
712 let kept: Vec<Candidate> = candidates
713 .into_iter()
714 .filter(|c| match (confidential, &c.kem_key) {
715 (_, Some(_)) => true,
716 (Confidentiality::Preferred, None) => !keyed.contains(&c.provider.node),
717 (_, None) => false,
718 })
719 .collect();
720 if kept.is_empty() {
721 return Err(PoolError::Confidentiality(ConfidentialityError {
722 reason: ConfidentialityReason::NoKemKey,
723 advertised,
724 named: None,
725 }));
726 }
727 Ok(kept)
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733
734 fn cand(node: u8, kem_key: Option<u8>) -> Candidate {
735 Candidate {
736 provider: Provider {
737 node: [node; 32],
738 station: [9; 32],
739 },
740 expires_at: 0,
741 created_at: 0,
742 kem_key: kem_key.map(|k| vec![k; 1568]),
743 }
744 }
745
746 fn refused(r: Result<Vec<Candidate>, PoolError>) -> ConfidentialityError {
747 match r {
748 Err(PoolError::Confidentiality(e)) => e,
749 other => panic!("not a confidentiality refusal: {other:?}"),
750 }
751 }
752
753 fn key_id_of(k: u8) -> [u8; KEY_ID_SIZE] {
754 crate::seal::key_id(&[k; 1568])
755 }
756
757 #[test]
758 fn a_refused_call_is_sealed_again_only_to_the_key_named() {
759 let found = || Ok(vec![cand(1, None), cand(1, Some(7)), cand(1, Some(8))]);
760 let next = reseal_to(found(), Some(key_id_of(8))).unwrap();
761 assert_eq!(next, cand(1, Some(8)));
762 let e = refused(reseal_to(found(), Some(key_id_of(9))).map(|c| vec![c]));
763 assert_eq!(e.reason, ConfidentialityReason::KeyMismatch);
764 assert_eq!(e.named, Some(key_id_of(9)));
765 assert_eq!(e.advertised, vec![key_id_of(7), key_id_of(8)]);
766 assert_eq!(reseal_to(found(), None).unwrap(), cand(1, Some(7)));
768 let e = refused(reseal_to(Ok(vec![cand(1, None)]), Some(key_id_of(8))).map(|c| vec![c]));
770 assert_eq!(e.reason, ConfidentialityReason::NoKemKey);
771 let e = refused(reseal_to(Err(PoolError::NoProvider(Vec::new())), None).map(|c| vec![c]));
772 assert_eq!(e.reason, ConfidentialityReason::NoKemKey);
773 }
774
775 #[test]
776 fn preferred_seals_to_a_keyed_provider_and_calls_a_keyless_one_in_the_clear() {
777 let kept = callable(
778 vec![cand(1, Some(7)), cand(2, None), cand(3, None)],
779 Confidentiality::Preferred,
780 )
781 .unwrap();
782 assert_eq!(kept, vec![cand(1, Some(7)), cand(2, None), cand(3, None)]);
783 assert_eq!(kept[0].seal(), Seal::To(vec![7; 1568]));
784 assert_eq!(kept[1].seal(), Seal::Clear);
785 }
786
787 #[test]
788 fn a_provider_that_names_a_key_is_not_called_through_its_older_keyless_ad() {
789 let kept = callable(
790 vec![cand(1, Some(7)), cand(1, None), cand(2, None)],
791 Confidentiality::Preferred,
792 )
793 .unwrap();
794 assert_eq!(kept, vec![cand(1, Some(7)), cand(2, None)]);
795 }
796
797 #[test]
798 fn required_calls_only_keyed_providers_and_refuses_when_there_are_none() {
799 let kept = callable(
800 vec![cand(1, None), cand(2, Some(7))],
801 Confidentiality::Required,
802 )
803 .unwrap();
804 assert_eq!(kept, vec![cand(2, Some(7))]);
805 let e = refused(callable(vec![cand(1, None)], Confidentiality::Required));
806 assert_eq!(e.reason, ConfidentialityReason::NoKemKey);
807 assert!(e.advertised.is_empty());
808 assert_eq!(e.to_string(), "confidentiality: no_kem_key");
809 }
810
811 #[test]
812 fn confidential_parses_and_off_is_refused_for_a_pool_call() {
813 assert_eq!(Confidentiality::default(), Confidentiality::Preferred);
814 for (text, parsed) in [
815 ("", Confidentiality::Preferred),
816 ("preferred", Confidentiality::Preferred),
817 ("required", Confidentiality::Required),
818 ("off", Confidentiality::Off),
819 ] {
820 assert_eq!(text.parse::<Confidentiality>(), Ok(parsed), "{text}");
821 }
822 for refused in ["Required", "none", "optional"] {
823 assert!(refused.parse::<Confidentiality>().is_err(), "{refused}");
824 }
825 assert!(matches!(
826 callable(vec![cand(1, Some(7))], Confidentiality::Off),
827 Err(PoolError::InvalidOpts(_))
828 ));
829 }
830}