1mod admission;
19mod authorize;
20mod call;
21mod confidential;
22mod dht;
23mod framing;
24mod pubsub;
25mod report;
26mod serve;
27mod stream;
28mod versions;
29
30pub use admission::{Admission, AdmissionLimits};
31pub use call::{Call, DEFAULT_CALL_TIMEOUT, MAX_CALL_TIMEOUT};
32pub use confidential::{
33 is_clear_refusal, Confidentiality, ConfidentialityError, ConfidentialityReason, Seal,
34};
35pub use pubsub::{Event, EventDedup, Publication, PublicationSeq, SignedPublication, Subscription};
36pub use report::{Report, ReportError};
37pub use serve::{handler, BoxFuture, Handler, Offer, Request, Served, StreamOffer};
38pub(crate) use stream::Reseal;
39pub use stream::{
40 stream_handler, Stream, StreamCall, StreamEvent, StreamHandler, DEFAULT_STREAM_DEADLINE,
41};
42pub use versions::{forget_v5_peer, handshake_counters};
43
44use std::collections::HashMap;
45use std::fmt;
46use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
47use std::sync::{Arc, Mutex, MutexGuard};
48use std::time::Duration;
49
50use sha2::{Digest, Sha384};
51use tokio::sync::watch;
52
53use crate::cbor::{self, Value};
54use crate::frame::{self, FrameError, Liveness, NeighbourLink, NeighbourPeer};
55use crate::handshake::{
56 self, ClientSession, Exporter, HandshakeError, Peer, RefusalCode, Station, VERSION, VERSION_5,
57};
58use crate::node_key::NodeKey;
59use crate::profile::Profile;
60use crate::record::RecordError;
61use crate::seal::Keyring;
62use crate::statement_issuer::{
63 ConnectMaterial, IssuerError, StatementIssuer, StatementSubscription,
64};
65use crate::transport::{self, DialError, Target};
66
67use framing::{read_frame, FrameWriter, HANDSHAKE_FRAME_BYTES, MAX_FRAME_BYTES};
68
69pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
71
72const CLOSE_LINGER: Duration = Duration::from_secs(1);
75
76const STATUS_GRACE_MS: i64 = 5 * 60 * 1000;
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum LinkError {
83 InvalidConfig(String),
86 Dial(String),
88 Handshake(HandshakeError),
90 HandshakeTimeout,
92 Issuer(IssuerError),
94 Io(String),
96 FrameTooLarge(usize),
98 Frame(FrameError),
101 Record(RecordError),
103 StatusExpired,
105 BindingExpired,
107 Closed,
109 Goodbye(String),
111 LivenessLost,
113 V5DowngradeRefused,
117 CallTimeout,
119 Provider {
121 responded_by: [u8; 32],
122 code: String,
123 detail: Option<String>,
124 },
125 Relay { reported_by: [u8; 32], code: String },
127 RecordNotFound,
129 UnexpectedReply(String),
131 InvalidOffer,
134 NoOrg,
136 AlreadyServed,
138 KemAdvertiseDisabled,
141 Stopped,
143 Stream {
146 code: String,
147 message: String,
148 relay: bool,
149 },
150 EndOfStream,
152 StreamClosed,
154 StreamOpenTooLarge(usize),
156 Confidentiality(ConfidentialityError),
159 SealedRefused { named: Option<[u8; 8]> },
162 ClearAnswerToSealed,
166 SealedFramesExhausted,
169}
170
171impl fmt::Display for LinkError {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 match self {
174 LinkError::Provider {
175 code,
176 detail: Some(d),
177 ..
178 } => write!(f, "the provider answered {code}: {d}"),
179 LinkError::Provider { code, .. } => write!(f, "the provider answered {code}"),
180 LinkError::Relay { code, .. } => {
181 write!(f, "the station could not relay the call: {code}")
182 }
183 LinkError::Stream { code, message, .. } if !message.is_empty() => {
184 write!(f, "stream error {code}: {message}")
185 }
186 LinkError::Stream { code, .. } => write!(f, "stream error {code}"),
187 LinkError::Handshake(e) => write!(f, "handshake: {e}"),
188 LinkError::Frame(e) => write!(f, "frame: {e}"),
189 LinkError::Record(e) => write!(f, "record: {e}"),
190 LinkError::Issuer(e) => write!(f, "{e}"),
191 LinkError::Goodbye(reason) => write!(f, "the station said goodbye: {reason}"),
192 LinkError::Confidentiality(e) => write!(f, "{e}"),
193 LinkError::SealedRefused { named: Some(id) } => write!(
194 f,
195 "the provider could not open the request; it holds key {}",
196 id.iter().map(|b| format!("{b:02x}")).collect::<String>()
197 ),
198 LinkError::SealedRefused { named: None } => {
199 f.write_str("the provider opens no sealed payload")
200 }
201 LinkError::ClearAnswerToSealed => f.write_str("a clear answer to a sealed request"),
202 LinkError::KemAdvertiseDisabled => {
203 f.write_str("a required-confidential procedure needs kem_advertise on")
204 }
205 LinkError::SealedFramesExhausted => {
206 f.write_str("this sealed stream has sealed all the frames it may")
207 }
208 other => write!(f, "{other:?}"),
209 }
210 }
211}
212
213impl std::error::Error for LinkError {}
214
215impl From<FrameError> for LinkError {
216 fn from(e: FrameError) -> Self {
217 LinkError::Frame(e)
218 }
219}
220
221impl From<RecordError> for LinkError {
222 fn from(e: RecordError) -> Self {
223 LinkError::Record(e)
224 }
225}
226
227impl From<HandshakeError> for LinkError {
228 fn from(e: HandshakeError) -> Self {
229 LinkError::Handshake(e)
230 }
231}
232
233impl From<DialError> for LinkError {
234 fn from(e: DialError) -> Self {
235 LinkError::Dial(e.to_string())
236 }
237}
238
239pub struct Config {
245 pub target: Target,
246 pub identity: Arc<NodeKey>,
247 pub issuer: StatementIssuer,
248 pub member_endorsement: Vec<u8>,
249 pub publication_seq: Option<Arc<PublicationSeq>>,
250 pub admission: Option<Arc<Admission>>,
251 pub dedup: Option<Arc<EventDedup>>,
252 pub share: Option<String>,
255 pub keyring: Option<Arc<Keyring>>,
259 pub kem_advertise: bool,
265}
266
267impl Config {
268 pub fn new(target: Target, identity: Arc<NodeKey>, issuer: StatementIssuer) -> Config {
270 Config {
271 target,
272 identity,
273 issuer,
274 member_endorsement: Vec::new(),
275 publication_seq: None,
276 admission: None,
277 dedup: None,
278 share: None,
279 keyring: None,
280 kem_advertise: false,
281 }
282 }
283}
284
285#[derive(Clone)]
287pub struct Link {
288 inner: Arc<Inner>,
289}
290
291struct Inner {
292 serial: u64,
294 connection: quinn::Connection,
295 _endpoint: quinn::Endpoint,
296 control: FrameWriter,
297 profile: Profile,
298 key: Arc<NodeKey>,
299 self_id: [u8; 32],
300 station: Station,
301 station_capabilities: u64,
302 connection_hash: [u8; 48],
303 version: i64,
305 pongs: tokio::sync::mpsc::Sender<[u8; frame::LIVENESS_NONCE_SIZE]>,
307 pong_in_flight: AtomicBool,
310 pongs_rx: tokio::sync::Mutex<tokio::sync::mpsc::Receiver<[u8; frame::LIVENESS_NONCE_SIZE]>>,
311 send_seq: tokio::sync::Mutex<u64>,
313 status_deadline: AtomicI64,
314 publication_seq: Arc<PublicationSeq>,
315 admission: Arc<Admission>,
316 dedup: Arc<EventDedup>,
317 share: String,
318 keyring: Option<Arc<Keyring>>,
319 kem_advertise: bool,
320 state: Mutex<State>,
321 done_tx: watch::Sender<bool>,
322 done_rx: watch::Receiver<bool>,
323}
324
325struct State {
326 ended: Option<LinkError>,
327 closing: bool,
330 unrouted: HashMap<String, u64>,
331 pending: HashMap<[u8; 16], call::Pending>,
332 subs: HashMap<([u8; 32], String), Vec<pubsub::SubscriberSlot>>,
333 served: HashMap<([u8; 32], String), serve::ServedEntry>,
334 streams: Vec<std::sync::Weak<stream::StreamInner>>,
335}
336
337impl Link {
338 pub async fn dial(cfg: Config) -> Result<Link, LinkError> {
346 if cfg.identity.profile() != cfg.target.profile {
347 return Err(LinkError::InvalidConfig(
348 "the identity key is of another profile than the target's".into(),
349 ));
350 }
351 if let Some(admission) = &cfg.admission {
352 admission.limits().validate()?;
353 }
354 if cfg
355 .keyring
356 .as_ref()
357 .is_some_and(|k| k.profile() != cfg.target.profile)
358 {
359 return Err(LinkError::InvalidConfig(
360 "the keyring is of another profile than the target's".into(),
361 ));
362 }
363 if cfg.kem_advertise && cfg.keyring.is_none() {
364 return Err(LinkError::InvalidConfig(
365 "kem_advertise names a key: it needs a keyring".into(),
366 ));
367 }
368 let node_id = cfg.target.expected_node_id;
369 let version = versions::dial_version(&node_id, std::time::Instant::now());
370 let linked = dial_once(&cfg, version).await;
371 let v5_refused = matches!(
372 linked,
373 Err(LinkError::Handshake(HandshakeError::Refused(
374 RefusalCode::UnsupportedVersion
375 )))
376 ) && version == VERSION_5;
377 if !v5_refused {
378 return linked;
379 }
380 if !versions::unsupported_version(&node_id, std::time::Instant::now()) {
381 return Err(LinkError::V5DowngradeRefused);
382 }
383 dial_once(&cfg, VERSION).await
384 }
385
386 pub fn handshake_version(&self) -> i64 {
388 self.inner.version
389 }
390
391 pub fn station_node_id(&self) -> [u8; 32] {
393 self.inner.station.node_id
394 }
395
396 pub fn serial(&self) -> u64 {
398 self.inner.serial
399 }
400
401 pub fn node_id(&self) -> [u8; 32] {
403 self.inner.self_id
404 }
405
406 pub fn station_capabilities(&self) -> u64 {
408 self.inner.station_capabilities
409 }
410
411 pub fn profile(&self) -> Profile {
413 self.inner.profile
414 }
415
416 pub fn error(&self) -> Option<LinkError> {
418 self.inner.lock().ended.clone()
419 }
420
421 pub async fn done(&self) -> LinkError {
423 let mut done = self.inner.done_rx.clone();
424 let _ = done.wait_for(|ended| *ended).await;
425 self.error().unwrap_or(LinkError::Closed)
426 }
427
428 pub fn unrouted(&self) -> HashMap<String, u64> {
431 self.inner.lock().unrouted.clone()
432 }
433
434 pub async fn close(&self, reason: &str) -> Result<(), LinkError> {
438 if !self.inner.mark_closing() {
439 return Ok(());
440 }
441 let goodbye = frame::goodbye_frame(reason, None)?;
442 let sent = self.inner.send_control(&goodbye).await;
443 if sent.is_ok() {
444 self.inner.control.finish().await;
445 let _ = tokio::time::timeout(CLOSE_LINGER, self.inner.connection.closed()).await;
446 }
447 self.inner.end(LinkError::Closed);
448 sent
449 }
450}
451
452async fn dial_once(cfg: &Config, version: i64) -> Result<Link, LinkError> {
455 tokio::time::timeout(HANDSHAKE_TIMEOUT, async {
456 let dialed = transport::dial_target(&cfg.target).await?;
457 let connection = dialed.connection.clone();
458 let linked = handshaken(cfg, dialed, version).await;
459 if let Err(e) = &linked {
460 versions::count_refusal(e);
461 connection.close(0u32.into(), b"handshake failed");
462 }
463 linked
464 })
465 .await
466 .map_err(|_| LinkError::HandshakeTimeout)
467 .and_then(|linked| linked)
468}
469
470async fn handshaken(
473 cfg: &Config,
474 dialed: transport::Dialed,
475 version: i64,
476) -> Result<Link, LinkError> {
477 let export = keying_exporter(dialed.connection.clone());
478 let export: &Exporter = &export;
479 let (send, mut recv) = dialed
480 .connection
481 .open_bi()
482 .await
483 .map_err(|e| LinkError::Io(format!("open the control stream: {e}")))?;
484 let control = FrameWriter::new(send);
485 control
486 .write(&handshake::opener(), HANDSHAKE_FRAME_BYTES)
487 .await?;
488 let challenge = read_frame(&mut recv, HANDSHAKE_FRAME_BYTES).await?;
489 let material = cfg.issuer.connect_material().map_err(LinkError::Issuer)?;
490 let (connect, station) = handshake::answer_challenge(
491 &challenge,
492 &client_session(cfg, &dialed.leaf, &material, version, export),
493 )?;
494 control.write(&connect, HANDSHAKE_FRAME_BYTES).await?;
495 let hello = read_frame(&mut recv, HANDSHAKE_FRAME_BYTES).await?;
496 let capabilities = handshake::read_hello(&hello, &station)?;
497 let self_id = cfg
498 .identity
499 .node_id()
500 .map_err(|e| LinkError::InvalidConfig(e.to_string()))?;
501 let statements = cfg
502 .issuer
503 .subscribe(&material.binding)
504 .map_err(LinkError::Issuer)?;
505 let inner = new_inner(
506 cfg,
507 dialed,
508 control,
509 station,
510 capabilities,
511 &challenge,
512 self_id,
513 );
514 let station_node_id = inner.station.node_id;
515 versions::completed(&station_node_id, &inner)?;
516 spawn_link_tasks(&inner, statements, recv);
517 Ok(Link { inner })
518}
519
520fn keying_exporter(
523 connection: quinn::Connection,
524) -> impl Fn(&str, &[u8], usize) -> Option<Vec<u8>> + Send + Sync {
525 move |label: &str, context: &[u8], length: usize| {
526 let mut out = vec![0; length];
527 connection
528 .export_keying_material(&mut out, label.as_bytes(), context)
529 .ok()?;
530 Some(out)
531 }
532}
533
534fn client_session<'a>(
538 cfg: &Config,
539 leaf: &'a [u8],
540 material: &'a ConnectMaterial,
541 version: i64,
542 export: &'a Exporter,
543) -> ClientSession<'a> {
544 ClientSession {
545 profile: cfg.target.profile,
546 expected_node_id: cfg.target.expected_node_id,
547 leaf,
548 identity_key: cfg.identity.public_key(),
549 connect_key: &material.key,
550 connect_binding: &material.binding,
551 connect_status: &material.status,
552 capabilities: 0,
553 now_ms: now_ms(),
554 member_endorsement: cfg.member_endorsement.clone(),
555 version,
556 export: Some(export),
557 }
558}
559
560fn new_inner(
563 cfg: &Config,
564 dialed: transport::Dialed,
565 control: FrameWriter,
566 station: Station,
567 capabilities: u64,
568 challenge: &[u8],
569 self_id: [u8; 32],
570) -> Arc<Inner> {
571 let (done_tx, done_rx) = watch::channel(false);
572 let share = cfg
573 .share
574 .clone()
575 .unwrap_or_else(|| format!("{}:{}", cfg.target.host, cfg.target.port));
576 let (pongs, pongs_rx) = tokio::sync::mpsc::channel(1);
577 static SERIALS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
578 Arc::new(Inner {
579 serial: SERIALS.fetch_add(1, Ordering::Relaxed),
580 connection: dialed.connection,
581 _endpoint: dialed.endpoint,
582 control,
583 profile: cfg.target.profile,
584 key: cfg.identity.clone(),
585 self_id,
586 status_deadline: AtomicI64::new(station.status_expires_at + STATUS_GRACE_MS),
587 station_capabilities: capabilities,
588 connection_hash: Sha384::digest(challenge).into(),
589 version: station.version,
590 pongs,
591 pong_in_flight: AtomicBool::new(false),
592 pongs_rx: tokio::sync::Mutex::new(pongs_rx),
593 station,
594 send_seq: tokio::sync::Mutex::new(0),
595 publication_seq: cfg.publication_seq.clone().unwrap_or_default(),
596 admission: cfg
597 .admission
598 .clone()
599 .unwrap_or_else(|| Arc::new(Admission::new(AdmissionLimits::default()))),
600 dedup: cfg.dedup.clone().unwrap_or_default(),
601 share,
602 keyring: cfg.keyring.clone(),
603 kem_advertise: cfg.kem_advertise,
604 state: Mutex::new(State {
605 ended: None,
606 closing: false,
607 unrouted: HashMap::new(),
608 pending: HashMap::new(),
609 subs: HashMap::new(),
610 served: HashMap::new(),
611 streams: Vec::new(),
612 }),
613 done_tx,
614 done_rx,
615 })
616}
617
618fn spawn_link_tasks(
622 inner: &Arc<Inner>,
623 statements: StatementSubscription,
624 recv: quinn::RecvStream,
625) {
626 tokio::spawn(send_statements(Arc::downgrade(inner), statements));
627 tokio::spawn(read_control(inner.clone(), recv));
628 tokio::spawn(stream::accept_streams(Arc::downgrade(inner)));
629 tokio::spawn(call::probe(Arc::downgrade(inner)));
630 tokio::spawn(watch_expiries(Arc::downgrade(inner)));
631 tokio::spawn(watch_connection(Arc::downgrade(inner)));
632}
633
634impl Inner {
635 fn lock(&self) -> MutexGuard<'_, State> {
636 self.state
637 .lock()
638 .unwrap_or_else(|poisoned| poisoned.into_inner())
639 }
640
641 fn count(&self, what: &str) {
642 *self.lock().unrouted.entry(what.to_string()).or_default() += 1;
643 }
644
645 fn mark_closing(&self) -> bool {
648 let mut state = self.lock();
649 if state.ended.is_some() {
650 return false;
651 }
652 state.closing = true;
653 true
654 }
655
656 async fn send_control(&self, v: &Value) -> Result<(), LinkError> {
659 if self.version == VERSION_5 {
660 return self.write_control(v).await;
661 }
662 let mut seq = self.send_seq.lock().await;
663 let signed = frame::sign_neighbour(
664 v,
665 &self.key,
666 &NeighbourLink {
667 connection: self.connection_hash,
668 seq: *seq,
669 },
670 )?;
671 if frame::neighbour_signed(self.profile, &frame_type_of(v)) {
672 *seq += 1;
673 }
674 self.write_control(&signed).await
675 }
676
677 async fn write_control(&self, v: &Value) -> Result<(), LinkError> {
680 let encoded =
681 cbor::encode(v).map_err(|e| LinkError::Frame(FrameError::Payload(e.to_string())))?;
682 self.control.write(&encoded, MAX_FRAME_BYTES).await
683 }
684
685 fn end(&self, err: LinkError) {
688 let mut state = self.lock();
689 let Some(err) = state.mark_ended(err) else {
690 return;
691 };
692 let pending = std::mem::take(&mut state.pending);
693 let subs = std::mem::take(&mut state.subs);
694 let served = std::mem::take(&mut state.served);
695 let streams = std::mem::take(&mut state.streams);
696 drop(state);
697 for (_, p) in pending {
698 let _ = p.outcome.send(Err(err.clone()));
699 }
700 drop(subs);
701 for s in served.into_values() {
702 s.end(err.clone());
703 }
704 for s in streams.into_iter().filter_map(|w| w.upgrade()) {
705 stream::StreamInner::end(&s, Some(err.clone()));
706 }
707 self.connection.close(0u32.into(), b"link ended");
708 if self.version == VERSION {
709 versions::v4_ended(&self.station.node_id, self.serial);
710 }
711 let _ = self.done_tx.send_replace(true);
712 }
713}
714
715impl State {
716 fn mark_ended(&mut self, err: LinkError) -> Option<LinkError> {
720 if self.ended.is_some() {
721 return None;
722 }
723 let err = if self.closing { LinkError::Closed } else { err };
724 self.ended = Some(err.clone());
725 Some(err)
726 }
727}
728
729async fn send_statements(link: std::sync::Weak<Inner>, mut statements: StatementSubscription) {
732 loop {
733 let Some(done) = link.upgrade().map(|l| l.done_rx.clone()) else {
734 return;
735 };
736 let mut done = done;
737 let statement = tokio::select! {
738 _ = done.wait_for(|ended| *ended) => return,
739 statement = statements.recv() => statement,
740 };
741 let (Some(statement), Some(inner)) = (statement, link.upgrade()) else {
742 return;
743 };
744 if let Err(e) = inner
745 .control
746 .write(&handshake::status_frame(&statement), MAX_FRAME_BYTES)
747 .await
748 {
749 inner.end(e);
750 return;
751 }
752 }
753}
754
755async fn read_control(inner: Arc<Inner>, mut recv: quinn::RecvStream) {
757 let mut recv_seq = 0u64;
758 let mut done = inner.done_rx.clone();
759 loop {
760 let payload = tokio::select! {
761 _ = done.wait_for(|ended| *ended) => return,
762 payload = read_frame(&mut recv, MAX_FRAME_BYTES) => payload,
763 };
764 let outcome = match payload {
765 Ok(payload) => received(&inner, &payload, &mut recv_seq),
766 Err(e) => Err(e),
767 };
768 if let Err(e) = outcome {
769 inner.end(e);
770 return;
771 }
772 }
773}
774
775fn received(inner: &Arc<Inner>, payload: &[u8], recv_seq: &mut u64) -> Result<(), LinkError> {
780 let v = cbor::decode(payload).map_err(|_| LinkError::Frame(FrameError::Malformed))?;
781 let frame_type = frame_type_of(&v);
782 if frame_type == "status" {
783 let expires_at = handshake::read_status(
784 payload,
785 &Peer {
786 profile: inner.profile,
787 identity_key: inner.station.identity_key.clone(),
788 binding: inner.station.tls_binding.clone(),
789 now_ms: now_ms(),
790 },
791 )?;
792 inner
793 .status_deadline
794 .store(expires_at + STATUS_GRACE_MS, Ordering::SeqCst);
795 return Ok(());
796 }
797 if let Some((kind, nonce)) = liveness_frame(inner.version, &v)? {
798 return liveness(inner, kind, nonce);
799 }
800 let opened = opened(inner, &v, &frame_type, recv_seq)?;
801 match frame_type.as_str() {
802 "event" => pubsub::evented(inner, &opened),
803 "result" | "error" => call::replied(inner, &opened),
804 "call" => serve::called(inner, &opened),
805 "goodbye" => {
806 let reason = match opened.get("reason") {
807 Some(Value::Text(r)) => r.clone(),
808 _ => String::new(),
809 };
810 return Err(LinkError::Goodbye(reason));
811 }
812 _ => inner.count(&frame_type),
813 }
814 Ok(())
815}
816
817fn opened(
820 inner: &Inner,
821 v: &Value,
822 frame_type: &str,
823 recv_seq: &mut u64,
824) -> Result<Value, LinkError> {
825 if inner.version == VERSION_5 {
826 return Ok(frame::verify_session_frame(v)?);
827 }
828 let opened = frame::verify_neighbour(
829 v,
830 &NeighbourPeer {
831 profile: inner.profile,
832 peer_key: inner.station.identity_key.clone(),
833 connection: inner.connection_hash,
834 seq: *recv_seq,
835 },
836 )?;
837 if frame::neighbour_signed(inner.profile, frame_type) {
838 *recv_seq += 1;
839 }
840 Ok(opened)
841}
842
843fn liveness_frame(
848 version: i64,
849 v: &Value,
850) -> Result<Option<(Liveness, [u8; frame::LIVENESS_NONCE_SIZE])>, LinkError> {
851 let Some(found) = frame::liveness_nonce(v) else {
852 return Ok(None);
853 };
854 if version != VERSION_5 {
855 return Err(LinkError::Frame(FrameError::Malformed));
856 }
857 frame::verify_session_frame(v)?;
858 Ok(Some(found))
859}
860
861fn liveness(
864 inner: &Arc<Inner>,
865 kind: Liveness,
866 nonce: [u8; frame::LIVENESS_NONCE_SIZE],
867) -> Result<(), LinkError> {
868 match kind {
869 Liveness::Ping if !pong_slot(&inner.pong_in_flight) => {}
873 Liveness::Ping => {
874 tokio::spawn(send_pong(inner.clone(), nonce));
875 }
876 Liveness::Pong => {
877 let _ = inner.pongs.try_send(nonce);
878 }
879 }
880 Ok(())
881}
882
883async fn send_pong(inner: Arc<Inner>, nonce: [u8; frame::LIVENESS_NONCE_SIZE]) {
886 let sent = inner
887 .send_control(&frame::liveness_pong_frame(&nonce))
888 .await;
889 inner.pong_in_flight.store(false, Ordering::Release);
890 if let Err(e) = sent {
891 inner.end(e);
892 }
893}
894
895fn pong_slot(in_flight: &AtomicBool) -> bool {
897 !in_flight.swap(true, Ordering::AcqRel)
898}
899
900async fn watch_expiries(link: std::sync::Weak<Inner>) {
903 loop {
904 let Some(inner) = link.upgrade() else { return };
905 let now = now_ms();
906 if now >= inner.station.binding_not_after {
907 inner.end(LinkError::BindingExpired);
908 return;
909 }
910 let status_deadline = inner.status_deadline.load(Ordering::SeqCst);
911 if now >= status_deadline {
912 inner.end(LinkError::StatusExpired);
913 return;
914 }
915 let wait = (status_deadline.min(inner.station.binding_not_after) - now).clamp(1, 60_000);
916 let mut done = inner.done_rx.clone();
917 drop(inner);
918 tokio::select! {
919 _ = done.wait_for(|ended| *ended) => return,
920 _ = tokio::time::sleep(Duration::from_millis(wait as u64)) => {}
921 }
922 }
923}
924
925async fn watch_connection(link: std::sync::Weak<Inner>) {
927 let Some(connection) = link.upgrade().map(|l| l.connection.clone()) else {
928 return;
929 };
930 let cause = connection.closed().await;
931 if let Some(inner) = link.upgrade() {
932 inner.end(LinkError::Io(cause.to_string()));
933 }
934}
935
936fn frame_type_of(v: &Value) -> String {
937 match v.get("frame_type") {
938 Some(Value::Text(t)) => t.clone(),
939 _ => String::new(),
940 }
941}
942
943fn now_ms() -> i64 {
944 crate::uuid_v7::now_ms() as i64
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950
951 fn with_neighbour(v: Value) -> Value {
952 let Value::Map(mut pairs) = v else {
953 panic!("a frame is a map");
954 };
955 pairs.push((Value::text("neighbour"), Value::Map(Vec::new())));
956 Value::Map(pairs)
957 }
958
959 #[test]
962 fn a_burst_of_pings_starts_one_pong_at_a_time() {
963 let in_flight = AtomicBool::new(false);
964 let taken = (0..1000).filter(|_| pong_slot(&in_flight)).count();
965 assert_eq!(taken, 1);
966 in_flight.store(false, Ordering::Release);
967 assert!(pong_slot(&in_flight), "free again once the pong is written");
968 }
969
970 #[test]
973 fn a_liveness_frame_is_read_as_every_v5_frame_and_exists_only_on_v5() {
974 let nonce = [9; frame::LIVENESS_NONCE_SIZE];
975 let ping = frame::liveness_ping_frame(&nonce);
976 let pong = frame::liveness_pong_frame(&nonce);
977 assert_eq!(
978 liveness_frame(VERSION_5, &ping),
979 Ok(Some((Liveness::Ping, nonce)))
980 );
981 assert_eq!(
982 liveness_frame(VERSION_5, &pong),
983 Ok(Some((Liveness::Pong, nonce)))
984 );
985 for signed in [with_neighbour(ping.clone()), with_neighbour(pong.clone())] {
986 assert_eq!(
987 liveness_frame(VERSION_5, &signed),
988 Err(LinkError::Frame(FrameError::Malformed))
989 );
990 }
991 assert_eq!(
992 liveness_frame(VERSION, &ping),
993 Err(LinkError::Frame(FrameError::Malformed))
994 );
995 let goodbye = frame::goodbye_frame("bye", None).unwrap();
996 assert_eq!(liveness_frame(VERSION_5, &goodbye), Ok(None));
997 }
998}