1use std::collections::HashSet;
17use std::fmt;
18use std::future::Future;
19use std::str::FromStr;
20use std::sync::Arc;
21use std::time::Duration;
22
23use tokio::time::Instant;
24
25use crate::cbor::Value;
26use crate::frame::StreamMode;
27use crate::record::{self, RecordType, Trust, Verified};
28use crate::seal::KEY_ID_SIZE;
29use crate::station_link::{self, Link, LinkError, Stream, DEFAULT_CALL_TIMEOUT};
30use crate::transport::Target;
31
32use super::{Pool, PoolError, PoolInner};
33
34const MIN_CANDIDATE_SHARE: Duration = Duration::from_secs(1);
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum Confidentiality {
45 #[default]
48 Preferred,
49 Required,
51}
52
53impl FromStr for Confidentiality {
54 type Err = PoolError;
55
56 fn from_str(s: &str) -> Result<Self, PoolError> {
59 match s {
60 "" | "preferred" => Ok(Confidentiality::Preferred),
61 "required" => Ok(Confidentiality::Required),
62 "off" => Err(PoolError::InvalidOpts(
63 "confidential off is refused for a call or an open: it is preferred or required"
64 .into(),
65 )),
66 other => Err(PoolError::InvalidOpts(format!(
67 "confidential is preferred or required, not {other:?}"
68 ))),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum ConfidentialityReason {
77 NoKemKey,
80}
81
82impl ConfidentialityReason {
83 pub fn name(self) -> &'static str {
85 match self {
86 ConfidentialityReason::NoKemKey => "no_kem_key",
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct ConfidentialityError {
96 pub reason: ConfidentialityReason,
97 pub advertised: Vec<[u8; KEY_ID_SIZE]>,
98}
99
100impl fmt::Display for ConfidentialityError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "confidentiality: {}", self.reason.name())?;
103 for id in &self.advertised {
104 f.write_str(" ")?;
105 for b in id {
106 write!(f, "{b:02x}")?;
107 }
108 }
109 Ok(())
110 }
111}
112
113#[derive(Debug, Clone, PartialEq)]
118pub struct Call {
119 pub realm: [u8; 32],
120 pub procedure: String,
121 pub provider: [u8; 32],
122 pub payload: Value,
123 pub timeout: Duration,
124 pub token: Option<Vec<u8>>,
125 pub proofs: Vec<Vec<u8>>,
126 pub confidential: Confidentiality,
127}
128
129impl Default for Call {
130 fn default() -> Self {
131 Call {
132 realm: [0; 32],
133 procedure: String::new(),
134 provider: [0; 32],
135 payload: Value::Map(Vec::new()),
136 timeout: Duration::ZERO,
137 token: None,
138 proofs: Vec::new(),
139 confidential: Confidentiality::Preferred,
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq)]
149pub struct StreamCall {
150 pub realm: [u8; 32],
151 pub procedure: String,
152 pub provider: [u8; 32],
153 pub mode: StreamMode,
154 pub payload: Value,
155 pub deadline: Duration,
156 pub token: Option<Vec<u8>>,
157 pub proofs: Vec<Vec<u8>>,
158 pub confidential: Confidentiality,
159}
160
161impl Default for StreamCall {
162 fn default() -> Self {
163 StreamCall {
164 realm: [0; 32],
165 procedure: String::new(),
166 provider: [0; 32],
167 mode: StreamMode::ServerStream,
168 payload: Value::Map(Vec::new()),
169 deadline: Duration::ZERO,
170 token: None,
171 proofs: Vec::new(),
172 confidential: Confidentiality::Preferred,
173 }
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub struct Provider {
180 pub node: [u8; 32],
181 pub station: [u8; 32],
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub(super) struct Candidate {
188 provider: Provider,
189 expires_at: u64,
190 created_at: u64,
191 kem_key_id: Option<[u8; KEY_ID_SIZE]>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Hash)]
195pub(super) struct ResolvedKey {
196 realm: [u8; 32],
197 procedure: String,
198 provider: [u8; 32],
199}
200
201impl Pool {
202 pub async fn call(&self, c: Call) -> Result<Value, PoolError> {
207 let inner = &self.inner;
208 let realm_key = inner.realm_key_for(&c.realm, &c.procedure)?;
209 let timeout = if c.timeout.is_zero() {
210 DEFAULT_CALL_TIMEOUT
211 } else {
212 c.timeout
213 };
214 let deadline = Instant::now() + timeout;
215 let key = ResolvedKey {
216 realm: c.realm,
217 procedure: c.procedure.clone(),
218 provider: c.provider,
219 };
220 let candidates = bounded(deadline, inner.candidates(&key, realm_key)).await?;
221 let candidates = callable(candidates, c.confidential)?;
222 let mut tried = Vec::new();
223 let count = candidates.len();
224 for (i, cand) in candidates.into_iter().enumerate() {
225 match inner
226 .reach(&cand, candidate_share(deadline, count - i))
227 .await
228 {
229 Ok(link) => {
230 let outcome =
231 bounded(deadline, inner.call_at(&link, &cand, &c, deadline)).await;
232 return inner.settled(key, cand, outcome);
233 }
234 Err(PoolError::Closed) => return Err(PoolError::Closed),
235 Err(e) => {
236 inner.forget(&key);
237 tried.push((cand.provider, e));
238 if Instant::now() >= deadline {
239 break;
240 }
241 }
242 }
243 }
244 Err(PoolError::NoProvider(tried))
245 }
246
247 pub async fn providers(
251 &self,
252 realm: &[u8; 32],
253 procedure: &str,
254 ) -> Result<Vec<Provider>, PoolError> {
255 let realm_key = self.inner.realm_key_for(realm, procedure)?;
256 let key = ResolvedKey {
257 realm: *realm,
258 procedure: procedure.to_string(),
259 provider: [0; 32],
260 };
261 let found = self.inner.resolve(&key, realm_key).await?;
262 Ok(found.into_iter().map(|c| c.provider).collect())
263 }
264
265 pub async fn open_stream(&self, c: StreamCall) -> Result<Stream, PoolError> {
271 let inner = &self.inner;
272 let realm_key = inner.realm_key_for(&c.realm, &c.procedure)?;
273 let deadline = Instant::now() + DEFAULT_CALL_TIMEOUT;
274 let key = ResolvedKey {
275 realm: c.realm,
276 procedure: c.procedure.clone(),
277 provider: c.provider,
278 };
279 let candidates = bounded(deadline, inner.candidates(&key, realm_key)).await?;
280 let candidates = callable(candidates, c.confidential)?;
281 let mut tried = Vec::new();
282 let count = candidates.len();
283 for (i, cand) in candidates.into_iter().enumerate() {
284 match inner
285 .reach(&cand, candidate_share(deadline, count - i))
286 .await
287 {
288 Ok(link) => {
289 let outcome = bounded(deadline, inner.open_at(&link, &cand, &c)).await;
290 return inner.settled(key, cand, outcome);
291 }
292 Err(PoolError::Closed) => return Err(PoolError::Closed),
293 Err(e) => {
294 inner.forget(&key);
295 tried.push((cand.provider, e));
296 if Instant::now() >= deadline {
297 break;
298 }
299 }
300 }
301 }
302 Err(PoolError::NoProvider(tried))
303 }
304
305 pub async fn station_target(&self, station: &[u8; 32]) -> Result<Target, PoolError> {
308 self.inner.station_target(station).await
309 }
310
311 pub async fn link_to(&self, station: &[u8; 32]) -> Result<Link, PoolError> {
316 let deadline = Instant::now() + DEFAULT_CALL_TIMEOUT;
317 self.inner.link_to(station, deadline).await
318 }
319
320 pub async fn find_record(&self, key: &[u8; 32]) -> Result<Verified, PoolError> {
322 self.inner
323 .first_answer(|l| async move { l.find_record(key).await })
324 .await
325 }
326
327 pub async fn find_records(&self, key: &[u8; 32]) -> Result<(Vec<Verified>, usize), PoolError> {
330 self.inner
331 .first_answer(|l| async move { l.find_records(key).await })
332 .await
333 }
334
335 pub async fn find_records_by_type(
338 &self,
339 t: RecordType,
340 ) -> Result<(Vec<Verified>, usize), PoolError> {
341 self.inner
342 .first_answer(|l| async move { l.find_records_by_type(t).await })
343 .await
344 }
345
346 pub async fn put_record(&self, wire: &[u8]) -> Result<(), PoolError> {
348 self.inner
349 .first_answer(|l| async move { l.put_record(wire).await })
350 .await
351 }
352}
353
354impl PoolInner {
355 fn remember(&self, key: ResolvedKey, cand: Candidate) {
356 self.lock().remember.insert(key, cand);
357 }
358
359 fn forget(&self, key: &ResolvedKey) {
360 self.lock().remember.remove(key);
361 }
362
363 async fn candidates(
366 &self,
367 key: &ResolvedKey,
368 realm_key: Option<Vec<u8>>,
369 ) -> Result<Vec<Candidate>, PoolError> {
370 let remembered = self.lock().remember.get(key).copied();
371 if let Some(cand) = remembered {
372 if cand.expires_at as i64 > now_ms() && self.linked_to(&cand.provider.station).is_some()
373 {
374 return Ok(vec![cand]);
375 }
376 }
377 self.resolve(key, realm_key).await
378 }
379
380 async fn resolve(
383 &self,
384 key: &ResolvedKey,
385 realm_key: Option<Vec<u8>>,
386 ) -> Result<Vec<Candidate>, PoolError> {
387 let slot = record::procedure_key(&key.realm, &key.procedure);
388 let (found, _) = self
389 .first_answer(|l| async move { l.find_records(&slot).await })
390 .await?;
391 let now = now_ms();
392 let trust = Trust {
393 profile: self.opts.identity.profile(),
394 realm_key,
395 };
396 let mut out: Vec<Candidate> = found
397 .iter()
398 .filter(|v| v.record().record_type == RecordType::PROCEDURE_ADVERTISEMENT)
399 .filter_map(|v| {
400 let ad = record::read_procedure_advertisement(v.record()).ok()?;
401 let wanted = ad.realm_id == key.realm
402 && ad.procedure == key.procedure
403 && (key.provider == [0; 32] || ad.advertiser_node == key.provider);
404 if !wanted || record::verify_authorization(v, &trust, now).is_err() {
405 return None;
406 }
407 Some(Candidate {
408 provider: Provider {
409 node: ad.advertiser_node,
410 station: ad.serving_station,
411 },
412 expires_at: v.record().expires_at,
413 created_at: v.record().created_at,
414 kem_key_id: ad.kem_key.map(|(_, id)| id),
415 })
416 })
417 .collect();
418 if out.is_empty() {
419 return Err(PoolError::NoProvider(Vec::new()));
420 }
421 out.sort_by_key(|c| std::cmp::Reverse(c.created_at));
422 Ok(out)
423 }
424
425 async fn reach(self: &Arc<Self>, cand: &Candidate, share: Instant) -> Result<Link, PoolError> {
428 bounded(share, self.link_to(&cand.provider.station, share)).await
429 }
430
431 fn settled<T>(
435 &self,
436 key: ResolvedKey,
437 cand: Candidate,
438 outcome: Result<T, PoolError>,
439 ) -> Result<T, PoolError> {
440 match &outcome {
441 Ok(_) | Err(PoolError::Link(LinkError::Provider { .. })) => self.remember(key, cand),
442 Err(_) => self.forget(&key),
443 }
444 outcome
445 }
446
447 async fn call_at(
450 &self,
451 link: &Link,
452 cand: &Candidate,
453 c: &Call,
454 deadline: Instant,
455 ) -> Result<Value, PoolError> {
456 let left = deadline.saturating_duration_since(Instant::now());
457 Ok(link
458 .call(station_link::Call {
459 realm: c.realm,
460 procedure: c.procedure.clone(),
461 target: cand.provider.node,
462 payload: c.payload.clone(),
463 timeout: left.max(Duration::from_millis(1)),
464 token: c.token.clone(),
465 proofs: c.proofs.clone(),
466 })
467 .await?)
468 }
469
470 async fn open_at(
472 &self,
473 link: &Link,
474 cand: &Candidate,
475 c: &StreamCall,
476 ) -> Result<Stream, PoolError> {
477 Ok(link
478 .open_stream(station_link::StreamCall {
479 realm: c.realm,
480 procedure: c.procedure.clone(),
481 target: cand.provider.node,
482 mode: c.mode,
483 payload: c.payload.clone(),
484 deadline: c.deadline,
485 token: c.token.clone(),
486 proofs: c.proofs.clone(),
487 })
488 .await?)
489 }
490
491 fn linked_to(&self, station: &[u8; 32]) -> Option<Link> {
493 self.links()
494 .into_iter()
495 .find(|l| l.station_node_id() == *station)
496 }
497
498 pub(super) async fn link_to(
499 self: &Arc<Self>,
500 station: &[u8; 32],
501 deadline: Instant,
502 ) -> Result<Link, PoolError> {
503 if let Some(link) = self.linked_to(station) {
504 return Ok(link);
505 }
506 let (existing, direct) = {
507 let state = self.lock();
508 if state.closed {
509 return Err(PoolError::Closed);
510 }
511 let existing = state
512 .members
513 .iter()
514 .find(|m| m.target.expected_node_id == *station)
515 .cloned();
516 (existing, state.members.iter().filter(|m| m.direct).count())
517 };
518 let fresh = existing.is_none();
519 let member = match existing {
520 Some(m) => m,
521 None => {
522 if direct >= self.opts.max_direct_links {
523 return Err(PoolError::DirectLinksFull);
524 }
525 let target = bounded(deadline, self.station_target(station)).await?;
526 self.start_member(target, true)
527 }
528 };
529 if let Some(link) = member.await_up(deadline, fresh).await {
530 return Ok(link);
531 }
532 if fresh {
533 self.drop_member(&member);
534 }
535 Err(PoolError::StationNotReached {
536 station: *station,
537 cause: member.last_error(),
538 })
539 }
540
541 async fn station_target(&self, station: &[u8; 32]) -> Result<Target, PoolError> {
542 let slot = record::station_endpoint_key(station);
543 let verified = self
544 .first_answer(|l| async move { l.find_record(&slot).await })
545 .await
546 .map_err(|e| match e {
547 PoolError::Link(link) => PoolError::NoStationEndpoint(Some(link)),
548 other => other,
549 })?;
550 let r = verified.record();
551 let signer = r.signed.as_ref().map(|s| s.key_id);
552 let endpoint = record::read_station_endpoint(r)
553 .map_err(|e| PoolError::NoStationEndpoint(Some(e.into())))?;
554 match (signer, endpoint.host_advertised.first()) {
555 (Some(signer), Some(host)) if signer == *station && endpoint.quic_port != 0 => {
556 Ok(Target {
557 host: host.clone(),
558 port: endpoint.quic_port,
559 profile: self.opts.identity.profile(),
560 expected_node_id: *station,
561 })
562 }
563 _ => Err(PoolError::NoStationEndpoint(None)),
564 }
565 }
566
567 pub(super) async fn first_answer<'a, T, F, Fut>(&self, ask: F) -> Result<T, PoolError>
571 where
572 F: Fn(Link) -> Fut,
573 Fut: Future<Output = Result<T, LinkError>> + 'a,
574 {
575 let links = self.links();
576 if links.is_empty() {
577 return Err(PoolError::NoLink(Vec::new()));
578 }
579 let mut errors = Vec::new();
580 for link in links {
581 match ask(link).await {
582 Err(e) if unreachable(&e) => errors.push(e),
583 answered => return answered.map_err(PoolError::Link),
584 }
585 }
586 Err(PoolError::NoLink(errors))
587 }
588}
589
590fn unreachable(e: &LinkError) -> bool {
593 matches!(
594 e,
595 LinkError::CallTimeout
596 | LinkError::Closed
597 | LinkError::LivenessLost
598 | LinkError::V5DowngradeRefused
599 | LinkError::Io(_)
600 | LinkError::Goodbye(_)
601 | LinkError::StatusExpired
602 | LinkError::BindingExpired
603 )
604}
605
606fn candidate_share(deadline: Instant, left: usize) -> Instant {
610 let now = Instant::now();
611 let remaining = deadline.saturating_duration_since(now);
612 (now + (remaining / left.max(1) as u32).max(MIN_CANDIDATE_SHARE)).min(deadline)
613}
614
615async fn bounded<T>(
617 deadline: Instant,
618 work: impl Future<Output = Result<T, PoolError>>,
619) -> Result<T, PoolError> {
620 tokio::time::timeout_at(deadline, work)
621 .await
622 .unwrap_or(Err(PoolError::Link(LinkError::CallTimeout)))
623}
624
625fn now_ms() -> i64 {
626 crate::uuid_v7::now_ms() as i64
627}
628
629fn callable(
637 candidates: Vec<Candidate>,
638 confidential: Confidentiality,
639) -> Result<Vec<Candidate>, PoolError> {
640 let advertised: Vec<[u8; KEY_ID_SIZE]> =
641 candidates.iter().filter_map(|c| c.kem_key_id).collect();
642 let keyed: HashSet<[u8; 32]> = candidates
643 .iter()
644 .filter(|c| c.kem_key_id.is_some())
645 .map(|c| c.provider.node)
646 .collect();
647 let clear: Vec<Candidate> = match confidential {
648 Confidentiality::Preferred => candidates
649 .into_iter()
650 .filter(|c| !keyed.contains(&c.provider.node))
651 .collect(),
652 Confidentiality::Required => Vec::new(),
653 };
654 if clear.is_empty() {
655 return Err(PoolError::Confidentiality(ConfidentialityError {
656 reason: ConfidentialityReason::NoKemKey,
657 advertised,
658 }));
659 }
660 Ok(clear)
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666
667 fn cand(node: u8, kem_key_id: Option<[u8; KEY_ID_SIZE]>) -> Candidate {
668 Candidate {
669 provider: Provider {
670 node: [node; 32],
671 station: [9; 32],
672 },
673 expires_at: 0,
674 created_at: 0,
675 kem_key_id,
676 }
677 }
678
679 fn refused(r: Result<Vec<Candidate>, PoolError>) -> ConfidentialityError {
680 match r {
681 Err(PoolError::Confidentiality(e)) => e,
682 other => panic!("not a confidentiality refusal: {other:?}"),
683 }
684 }
685
686 #[test]
687 fn preferred_calls_only_the_providers_that_name_no_key() {
688 let kept = callable(
689 vec![cand(1, Some([7; 8])), cand(2, None), cand(3, None)],
690 Confidentiality::Preferred,
691 )
692 .unwrap();
693 assert_eq!(kept, vec![cand(2, None), cand(3, None)]);
694 }
695
696 #[test]
697 fn a_provider_that_names_a_key_is_never_called_in_the_clear() {
698 let e = refused(callable(
699 vec![cand(1, Some([7; 8])), cand(2, Some([8; 8]))],
700 Confidentiality::Preferred,
701 ));
702 assert_eq!(e.reason, ConfidentialityReason::NoKemKey);
703 assert_eq!(e.advertised, vec![[7; 8], [8; 8]]);
704 assert_eq!(
705 e.to_string(),
706 "confidentiality: no_kem_key 0707070707070707 0808080808080808"
707 );
708 }
709
710 #[test]
711 fn a_provider_that_names_a_key_is_not_called_through_its_older_keyless_ad() {
712 let e = refused(callable(
713 vec![cand(1, Some([7; 8])), cand(1, None)],
714 Confidentiality::Preferred,
715 ));
716 assert_eq!(e.advertised, vec![[7; 8]]);
717 let kept = callable(
718 vec![cand(1, Some([7; 8])), cand(1, None), cand(2, None)],
719 Confidentiality::Preferred,
720 )
721 .unwrap();
722 assert_eq!(kept, vec![cand(2, None)]);
723 }
724
725 #[test]
726 fn required_calls_no_provider_until_this_sdk_seals() {
727 let e = refused(callable(
728 vec![cand(1, None), cand(2, Some([7; 8]))],
729 Confidentiality::Required,
730 ));
731 assert_eq!(e.reason, ConfidentialityReason::NoKemKey);
732 assert_eq!(e.advertised, vec![[7; 8]]);
733 let e = refused(callable(vec![cand(1, None)], Confidentiality::Required));
734 assert!(e.advertised.is_empty());
735 }
736
737 #[test]
738 fn confidential_is_preferred_or_required_and_off_is_refused() {
739 assert_eq!(Confidentiality::default(), Confidentiality::Preferred);
740 assert_eq!(
741 "".parse::<Confidentiality>().unwrap(),
742 Confidentiality::Preferred
743 );
744 assert_eq!(
745 "preferred".parse::<Confidentiality>().unwrap(),
746 Confidentiality::Preferred
747 );
748 assert_eq!(
749 "required".parse::<Confidentiality>().unwrap(),
750 Confidentiality::Required
751 );
752 for refused in ["off", "Required", "none", "optional"] {
753 assert!(
754 matches!(
755 refused.parse::<Confidentiality>(),
756 Err(PoolError::InvalidOpts(_))
757 ),
758 "{refused}"
759 );
760 }
761 }
762}