Skip to main content

macula_rust/
station_link.rs

1//! A client's link to one macula station, as macula_station_link and
2//! macula-go's stationlink are: a QUIC connection dialed to the station its
3//! target pins, one bidirectional control stream, the wire 1.0 connection
4//! handshake on it, then status statements both ways and every frame of the
5//! session.
6//!
7//! After HELLO the link sends its own status statement at every reissue of
8//! the node's statement issuer, and ends when the station's statement is five
9//! minutes past its expiry or the station's TLS binding reaches its
10//! not_after. No frame carries a neighbour signature: the session proofs
11//! authenticated the station once. Every frame is read as wire 1.0 reads it
12//! (WIRE_1_0.md section 3): one of a type the link does not act on is
13//! dropped and counted, one with an unknown critical extension is dropped and
14//! counted as `extension_unsupported`, both with the link left up; one that
15//! does not read ends the link as malformed. A liveness probe every 30
16//! seconds ends the link after two misses in a row.
17
18mod 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::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 tokio::sync::watch;
51
52use crate::cbor::{self, Value};
53use crate::frame::{self, Envelope, FrameError, Liveness};
54use crate::handshake::{self, ClientSession, Exporter, HandshakeError, Peer, Ranges, Station};
55use crate::node_key::NodeKey;
56use crate::profile::Profile;
57use crate::record::RecordError;
58use crate::seal::Keyring;
59use crate::statement_issuer::{
60    ConnectMaterial, IssuerError, StatementIssuer, StatementSubscription,
61};
62use crate::transport::{self, DialError, Target};
63
64use framing::{read_frame, FrameWriter, HANDSHAKE_FRAME_BYTES, MAX_FRAME_BYTES};
65
66/// How long the handshake may take, as macula's 30 seconds.
67pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
68
69/// How long Close waits, after its GOODBYE, for the station to close the
70/// connection before closing it itself.
71const CLOSE_LINGER: Duration = Duration::from_secs(1);
72
73/// How long past a statement's expiry the link keeps a station whose next
74/// statement has not arrived: macula's five minutes.
75const STATUS_GRACE_MS: i64 = 5 * 60 * 1000;
76
77/// Why a link or one of its operations failed.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum LinkError {
80    /// The configuration cannot be dialed: a key of another profile than the
81    /// target's, or admission limits macula would not start with.
82    InvalidConfig(String),
83    /// The dial failed.
84    Dial(String),
85    /// The handshake refused, or was refused.
86    Handshake(HandshakeError),
87    /// The handshake did not finish within [`HANDSHAKE_TIMEOUT`].
88    HandshakeTimeout,
89    /// The issuer had no CONNECT material in force.
90    Issuer(IssuerError),
91    /// The QUIC connection or a stream failed.
92    Io(String),
93    /// A frame longer than the cap in force.
94    FrameTooLarge(usize),
95    /// A frame that could not be built, or one received that does not
96    /// verify.
97    Frame(FrameError),
98    /// A record that could not be built, or one found that does not verify.
99    Record(RecordError),
100    /// The station's status statement lapsed past the grace.
101    StatusExpired,
102    /// The station's TLS binding reached its not_after.
103    BindingExpired,
104    /// The link was closed by its owner.
105    Closed,
106    /// The station ended the link with a GOODBYE, for this reason.
107    Goodbye(String),
108    /// The station missed two liveness probes in a row.
109    LivenessLost,
110    /// No verified reply answered the call within its timeout.
111    CallTimeout,
112    /// A provider's ERROR for the call.
113    Provider {
114        responded_by: [u8; 32],
115        code: String,
116        detail: Option<String>,
117    },
118    /// A relay error the connected station reported for the call.
119    Relay { reported_by: [u8; 32], code: String },
120    /// A find_record the station answered not_found.
121    RecordNotFound,
122    /// A put_record the station refused, by the custodian's refusal name.
123    RecordRefused(String),
124    /// A `_dht.*` reply of a version this crate does not read
125    /// (`reply_version_unsupported`).
126    ReplyVersionUnsupported,
127    /// A station reply to a DHT call in a shape that call never answers with.
128    UnexpectedReply(String),
129    /// An offer without exactly one handler, or an org procedure's without
130    /// the realm key.
131    InvalidOffer,
132    /// A procedure without a namespace, which nobody can authorize.
133    NoOrg,
134    /// A procedure already served on this link.
135    AlreadyServed,
136    /// A required-confidential procedure on a link whose `kem_advertise` is
137    /// off: it would name no key and refuse every clear call.
138    KemAdvertiseDisabled,
139    /// A served procedure withdrawn by its owner.
140    Stopped,
141    /// The station refused an ADVERTISE or SUBSCRIBE (REGISTER_ACK,
142    /// WIRE_1_0.md section 3.5): what it answered, and its reason.
143    RegisterRefused {
144        kind: frame::RegisterKind,
145        reason: String,
146    },
147    /// A stream ended by a STREAM_ERROR: the peer's, the station's relay
148    /// error (`relay`), or this side's own abort.
149    Stream {
150        code: String,
151        message: String,
152        relay: bool,
153    },
154    /// A stream that ended normally.
155    EndOfStream,
156    /// A send on a stream this side has ended.
157    StreamClosed,
158    /// A STREAM_OPEN over the 1 MiB a peer reads of one.
159    StreamOpenTooLarge(usize),
160    /// A call or an open that could not be kept confidential, so it was not
161    /// made, or failed rather than be taken in the clear.
162    Confidentiality(ConfidentialityError),
163    /// The provider's sealed_refused: it could not open the request. `named`
164    /// is the key it holds now, `None` when it holds none.
165    SealedRefused { named: Option<[u8; 8]> },
166    /// A clear answer to a sealed request that nothing clear may give: not a
167    /// relay error, not a refusal from the closed set, not sealed_refused.
168    /// Refused, never taken as the answer.
169    ClearAnswerToSealed,
170    /// A provider's sealed stream that has sealed all the frames GCM's
171    /// bound allows under random nonces: another would pass it.
172    SealedFramesExhausted,
173}
174
175impl fmt::Display for LinkError {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            LinkError::Provider {
179                code,
180                detail: Some(d),
181                ..
182            } => write!(f, "the provider answered {code}: {d}"),
183            LinkError::Provider { code, .. } => write!(f, "the provider answered {code}"),
184            LinkError::Relay { code, .. } => {
185                write!(f, "the station could not relay the call: {code}")
186            }
187            LinkError::Stream { code, message, .. } if !message.is_empty() => {
188                write!(f, "stream error {code}: {message}")
189            }
190            LinkError::Stream { code, .. } => write!(f, "stream error {code}"),
191            LinkError::Handshake(e) => write!(f, "handshake: {e}"),
192            LinkError::Frame(e) => write!(f, "frame: {e}"),
193            LinkError::Record(e) => write!(f, "record: {e}"),
194            LinkError::Issuer(e) => write!(f, "{e}"),
195            LinkError::Goodbye(reason) => write!(f, "the station said goodbye: {reason}"),
196            LinkError::RegisterRefused { kind, reason } => {
197                write!(f, "the station refused the {kind:?}: {reason}")
198            }
199            LinkError::Confidentiality(e) => write!(f, "{e}"),
200            LinkError::SealedRefused { named: Some(id) } => write!(
201                f,
202                "the provider could not open the request; it holds key {}",
203                id.iter().map(|b| format!("{b:02x}")).collect::<String>()
204            ),
205            LinkError::SealedRefused { named: None } => {
206                f.write_str("the provider opens no sealed payload")
207            }
208            LinkError::ClearAnswerToSealed => f.write_str("a clear answer to a sealed request"),
209            LinkError::KemAdvertiseDisabled => {
210                f.write_str("a required-confidential procedure needs kem_advertise on")
211            }
212            LinkError::SealedFramesExhausted => {
213                f.write_str("this sealed stream has sealed all the frames it may")
214            }
215            other => write!(f, "{other:?}"),
216        }
217    }
218}
219
220impl std::error::Error for LinkError {}
221
222impl From<FrameError> for LinkError {
223    fn from(e: FrameError) -> Self {
224        LinkError::Frame(e)
225    }
226}
227
228impl From<RecordError> for LinkError {
229    fn from(e: RecordError) -> Self {
230        LinkError::Record(e)
231    }
232}
233
234impl From<HandshakeError> for LinkError {
235    fn from(e: HandshakeError) -> Self {
236        LinkError::Handshake(e)
237    }
238}
239
240impl From<DialError> for LinkError {
241    fn from(e: DialError) -> Self {
242        LinkError::Dial(e.to_string())
243    }
244}
245
246/// What a link is dialed with: the station to reach, the node's identity
247/// key, the statement issuer that holds its CONNECT key and statements, and
248/// the realm membership endorsement to present, empty for none. Links of one
249/// node share their publication seq, admission and dedup; a link given none
250/// makes its own.
251pub struct Config {
252    pub target: Target,
253    pub identity: Arc<NodeKey>,
254    pub issuer: StatementIssuer,
255    pub member_endorsement: Vec<u8>,
256    pub publication_seq: Option<Arc<PublicationSeq>>,
257    pub admission: Option<Arc<Admission>>,
258    pub dedup: Option<Arc<EventDedup>>,
259    /// This link's place in the admission: the station it dialed, host:port,
260    /// when `None`.
261    pub share: Option<String>,
262    /// This identity's KEM keys (macula 13, E2E design amendment A1): with
263    /// them the link opens sealed requests; without them it refuses them,
264    /// holding no key. Links of one identity share one.
265    pub keyring: Option<Arc<Keyring>>,
266    /// Names the keyring's current key in the advertisements of procedures
267    /// served confidentially ([`Confidentiality::Preferred`] or
268    /// [`Confidentiality::Required`]). Off by default: switch it on only once
269    /// every station runs macula 12.11 or later, which stores and routes a
270    /// keyed advertisement, and every caller runs 13. It needs a keyring.
271    pub kem_advertise: bool,
272}
273
274impl Config {
275    /// A link's configuration with none of the shared parts.
276    pub fn new(target: Target, identity: Arc<NodeKey>, issuer: StatementIssuer) -> Config {
277        Config {
278            target,
279            identity,
280            issuer,
281            member_endorsement: Vec::new(),
282            publication_seq: None,
283            admission: None,
284            dedup: None,
285            share: None,
286            keyring: None,
287            kem_advertise: false,
288        }
289    }
290}
291
292/// A handshaked link to one station. Cloning it shares the link.
293#[derive(Clone)]
294pub struct Link {
295    inner: Arc<Inner>,
296}
297
298struct Inner {
299    /// Distinct for every link a process dials.
300    serial: u64,
301    connection: quinn::Connection,
302    _endpoint: quinn::Endpoint,
303    control: FrameWriter,
304    profile: Profile,
305    key: Arc<NodeKey>,
306    self_id: [u8; 32],
307    station: Station,
308    station_capabilities: u64,
309    /// Liveness answers, for the probe.
310    pongs: tokio::sync::mpsc::Sender<[u8; frame::LIVENESS_NONCE_SIZE]>,
311    /// A liveness_pong is being written (macula-rust#9): at most one is in
312    /// flight, so a station's pings cannot pile up tasks on this link.
313    pong_in_flight: AtomicBool,
314    pongs_rx: tokio::sync::Mutex<tokio::sync::mpsc::Receiver<[u8; frame::LIVENESS_NONCE_SIZE]>>,
315    status_deadline: AtomicI64,
316    publication_seq: Arc<PublicationSeq>,
317    admission: Arc<Admission>,
318    dedup: Arc<EventDedup>,
319    share: String,
320    keyring: Option<Arc<Keyring>>,
321    kem_advertise: bool,
322    state: Mutex<State>,
323    done_tx: watch::Sender<bool>,
324    done_rx: watch::Receiver<bool>,
325}
326
327struct State {
328    ended: Option<LinkError>,
329    /// The owner is closing the link: however it then ends, it ends
330    /// [`LinkError::Closed`].
331    closing: bool,
332    unrouted: HashMap<String, u64>,
333    pending: HashMap<[u8; 16], call::Pending>,
334    subs: HashMap<([u8; 32], String), Vec<pubsub::SubscriberSlot>>,
335    served: HashMap<([u8; 32], String), serve::ServedEntry>,
336    streams: Vec<std::sync::Weak<stream::StreamInner>>,
337}
338
339impl Link {
340    /// Dials `cfg.target`, runs the handshake as a client, and returns the
341    /// link once the station's HELLO accepts it, within
342    /// [`HANDSHAKE_TIMEOUT`].
343    pub async fn dial(cfg: Config) -> Result<Link, LinkError> {
344        if cfg.identity.profile() != cfg.target.profile {
345            return Err(LinkError::InvalidConfig(
346                "the identity key is of another profile than the target's".into(),
347            ));
348        }
349        if let Some(admission) = &cfg.admission {
350            admission.limits().validate()?;
351        }
352        if cfg
353            .keyring
354            .as_ref()
355            .is_some_and(|k| k.profile() != cfg.target.profile)
356        {
357            return Err(LinkError::InvalidConfig(
358                "the keyring is of another profile than the target's".into(),
359            ));
360        }
361        if cfg.kem_advertise && cfg.keyring.is_none() {
362            return Err(LinkError::InvalidConfig(
363                "kem_advertise names a key: it needs a keyring".into(),
364            ));
365        }
366        dial_once(&cfg).await
367    }
368
369    /// The handshake version the link completed.
370    pub fn handshake_version(&self) -> i64 {
371        self.inner.station.version
372    }
373
374    /// The node_id of the station the link reached.
375    pub fn station_node_id(&self) -> [u8; 32] {
376        self.inner.station.node_id
377    }
378
379    /// A number distinct for every link this process dials.
380    pub fn serial(&self) -> u64 {
381        self.inner.serial
382    }
383
384    /// The node_id this link connected as.
385    pub fn node_id(&self) -> [u8; 32] {
386        self.inner.self_id
387    }
388
389    /// The capability bits the station's HELLO announced.
390    pub fn station_capabilities(&self) -> u64 {
391        self.inner.station_capabilities
392    }
393
394    /// The profile the link runs.
395    pub fn profile(&self) -> Profile {
396        self.inner.profile
397    }
398
399    /// Why the link ended, or `None` while it runs.
400    pub fn error(&self) -> Option<LinkError> {
401        self.inner.lock().ended.clone()
402    }
403
404    /// Waits until the link has ended, and says why.
405    pub async fn done(&self) -> LinkError {
406        let mut done = self.inner.done_rx.clone();
407        let _ = done.wait_for(|ended| *ended).await;
408        self.error().unwrap_or(LinkError::Closed)
409    }
410
411    /// The frames received that nothing on this link handles, by what they
412    /// were.
413    pub fn unrouted(&self) -> HashMap<String, u64> {
414        self.inner.lock().unrouted.clone()
415    }
416
417    /// Sends a GOODBYE with `reason`, closes the control stream's sending
418    /// side, waits up to a second for the station to close the connection,
419    /// and ends the link. Closing an ended link does nothing.
420    pub async fn close(&self, reason: &str) -> Result<(), LinkError> {
421        if !self.inner.mark_closing() {
422            return Ok(());
423        }
424        let goodbye = frame::goodbye_frame(reason, None)?;
425        let sent = self.inner.send_control(&goodbye).await;
426        if sent.is_ok() {
427            self.inner.control.finish().await;
428            let _ = tokio::time::timeout(CLOSE_LINGER, self.inner.connection.closed()).await;
429        }
430        self.inner.end(LinkError::Closed);
431        sent
432    }
433}
434
435/// One QUIC connection and one handshake, within [`HANDSHAKE_TIMEOUT`]. A
436/// failed handshake closes its connection.
437async fn dial_once(cfg: &Config) -> Result<Link, LinkError> {
438    tokio::time::timeout(HANDSHAKE_TIMEOUT, async {
439        let dialed = transport::dial_target(&cfg.target).await?;
440        let connection = dialed.connection.clone();
441        let linked = handshaken(cfg, dialed).await;
442        if let Err(e) = &linked {
443            versions::count_refusal(e);
444            connection.close(0u32.into(), b"handshake failed");
445        }
446        linked
447    })
448    .await
449    .map_err(|_| LinkError::HandshakeTimeout)
450    .and_then(|linked| linked)
451}
452
453/// Runs the client's side of the handshake on a new control stream of
454/// `dialed`: the opener with this crate's ranges, the challenge checked
455/// against them and the floor this process holds for the station, CONNECT,
456/// HELLO. A completed handshake raises the floor.
457async fn handshaken(cfg: &Config, dialed: transport::Dialed) -> Result<Link, LinkError> {
458    let export = keying_exporter(dialed.connection.clone());
459    let export: &Exporter = &export;
460    let (send, mut recv) = dialed
461        .connection
462        .open_bi()
463        .await
464        .map_err(|e| LinkError::Io(format!("open the control stream: {e}")))?;
465    let control = FrameWriter::new(send);
466    control
467        .write(
468            &handshake::opener(&Ranges::supported(cfg.target.profile)),
469            HANDSHAKE_FRAME_BYTES,
470        )
471        .await?;
472    let challenge = read_frame(&mut recv, HANDSHAKE_FRAME_BYTES).await?;
473    let material = cfg.issuer.connect_material().map_err(LinkError::Issuer)?;
474    let (connect, station) = handshake::answer_challenge(
475        &challenge,
476        &client_session(cfg, &dialed.leaf, &material, export),
477    )?;
478    control.write(&connect, HANDSHAKE_FRAME_BYTES).await?;
479    let hello = read_frame(&mut recv, HANDSHAKE_FRAME_BYTES).await?;
480    let capabilities = handshake::read_hello(&hello, &station)?;
481    versions::completed(&station.node_id, station.version);
482    let self_id = cfg
483        .identity
484        .node_id()
485        .map_err(|e| LinkError::InvalidConfig(e.to_string()))?;
486    let statements = cfg
487        .issuer
488        .subscribe(&material.binding)
489        .map_err(LinkError::Issuer)?;
490    let inner = new_inner(cfg, dialed, control, station, capabilities, self_id);
491    spawn_link_tasks(&inner, statements, recv);
492    Ok(Link { inner })
493}
494
495/// The TLS exporter of `connection`, as the v5 handshake asks for keying
496/// material.
497fn keying_exporter(
498    connection: quinn::Connection,
499) -> impl Fn(&str, &[u8], usize) -> Option<Vec<u8>> + Send + Sync {
500    move |label: &str, context: &[u8], length: usize| {
501        let mut out = vec![0; length];
502        connection
503            .export_keying_material(&mut out, label.as_bytes(), context)
504            .ok()?;
505        Some(out)
506    }
507}
508
509/// What the client brings to the handshake: the configured identity and
510/// target, the leaf it received, the issuer's CONNECT material, at the time
511/// now, this crate's ranges and the floor this process holds. It declares
512/// [`handshake::CAP_RELAY_OVERLOADED`]: a station may refuse an excess call
513/// with `overloaded`, and the call ends at once (macula#54).
514fn client_session<'a>(
515    cfg: &Config,
516    leaf: &'a [u8],
517    material: &'a ConnectMaterial,
518    export: &'a Exporter,
519) -> ClientSession<'a> {
520    ClientSession {
521        profile: cfg.target.profile,
522        expected_node_id: cfg.target.expected_node_id,
523        leaf,
524        identity_key: cfg.identity.public_key(),
525        connect_key: &material.key,
526        connect_binding: &material.binding,
527        connect_status: &material.status,
528        capabilities: handshake::CAP_RELAY_OVERLOADED,
529        now_ms: now_ms(),
530        member_endorsement: cfg.member_endorsement.clone(),
531        ranges: Ranges::supported(cfg.target.profile),
532        floor: &versions::floor,
533        export,
534    }
535}
536
537/// The state of a link whose handshake completed with `station` on the
538/// control stream `control` of `dialed`, numbered with the next serial.
539fn new_inner(
540    cfg: &Config,
541    dialed: transport::Dialed,
542    control: FrameWriter,
543    station: Station,
544    capabilities: u64,
545    self_id: [u8; 32],
546) -> Arc<Inner> {
547    let (done_tx, done_rx) = watch::channel(false);
548    let share = cfg
549        .share
550        .clone()
551        .unwrap_or_else(|| format!("{}:{}", cfg.target.host, cfg.target.port));
552    let (pongs, pongs_rx) = tokio::sync::mpsc::channel(1);
553    static SERIALS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
554    Arc::new(Inner {
555        serial: SERIALS.fetch_add(1, Ordering::Relaxed),
556        connection: dialed.connection,
557        _endpoint: dialed.endpoint,
558        control,
559        profile: cfg.target.profile,
560        key: cfg.identity.clone(),
561        self_id,
562        status_deadline: AtomicI64::new(station.status_expires_at + STATUS_GRACE_MS),
563        station_capabilities: capabilities,
564        pongs,
565        pong_in_flight: AtomicBool::new(false),
566        pongs_rx: tokio::sync::Mutex::new(pongs_rx),
567        station,
568        publication_seq: cfg.publication_seq.clone().unwrap_or_default(),
569        admission: cfg
570            .admission
571            .clone()
572            .unwrap_or_else(|| Arc::new(Admission::new(AdmissionLimits::default()))),
573        dedup: cfg.dedup.clone().unwrap_or_default(),
574        share,
575        keyring: cfg.keyring.clone(),
576        kem_advertise: cfg.kem_advertise,
577        state: Mutex::new(State {
578            ended: None,
579            closing: false,
580            unrouted: HashMap::new(),
581            pending: HashMap::new(),
582            subs: HashMap::new(),
583            served: HashMap::new(),
584            streams: Vec::new(),
585        }),
586        done_tx,
587        done_rx,
588    })
589}
590
591/// Starts the tasks a linked link runs: its statements, its control
592/// stream's reader, the streams it accepts, its liveness probe, and the
593/// watches on its expiries and its connection.
594fn spawn_link_tasks(
595    inner: &Arc<Inner>,
596    statements: StatementSubscription,
597    recv: quinn::RecvStream,
598) {
599    tokio::spawn(send_statements(Arc::downgrade(inner), statements));
600    tokio::spawn(read_control(inner.clone(), recv));
601    tokio::spawn(stream::accept_streams(Arc::downgrade(inner)));
602    tokio::spawn(call::probe(Arc::downgrade(inner)));
603    tokio::spawn(watch_expiries(Arc::downgrade(inner)));
604    tokio::spawn(watch_connection(Arc::downgrade(inner)));
605}
606
607impl Inner {
608    fn lock(&self) -> MutexGuard<'_, State> {
609        self.state
610            .lock()
611            .unwrap_or_else(|poisoned| poisoned.into_inner())
612    }
613
614    fn count(&self, what: &str) {
615        *self.lock().unrouted.entry(what.to_string()).or_default() += 1;
616    }
617
618    /// Marks the link closing, so it ends as closed: false when it has
619    /// already ended.
620    fn mark_closing(&self) -> bool {
621        let mut state = self.lock();
622        if state.ended.is_some() {
623            return false;
624        }
625        state.closing = true;
626        true
627    }
628
629    /// A frame on the control stream, as it is.
630    async fn send_control(&self, v: &Value) -> Result<(), LinkError> {
631        let encoded =
632            cbor::encode(v).map_err(|e| LinkError::Frame(FrameError::Payload(e.to_string())))?;
633        self.control.write(&encoded, MAX_FRAME_BYTES).await
634    }
635
636    /// Ends the link once, with `err`: pending calls, subscriptions, served
637    /// procedures and streams end with it, and the connection closes.
638    fn end(&self, err: LinkError) {
639        let mut state = self.lock();
640        let Some(err) = state.mark_ended(err) else {
641            return;
642        };
643        let pending = std::mem::take(&mut state.pending);
644        let subs = std::mem::take(&mut state.subs);
645        let served = std::mem::take(&mut state.served);
646        let streams = std::mem::take(&mut state.streams);
647        drop(state);
648        for (_, p) in pending {
649            let _ = p.outcome.send(Err(err.clone()));
650        }
651        drop(subs);
652        for s in served.into_values() {
653            s.end(err.clone());
654        }
655        for s in streams.into_iter().filter_map(|w| w.upgrade()) {
656            stream::StreamInner::end(&s, Some(err.clone()));
657        }
658        self.connection.close(0u32.into(), b"link ended");
659        let _ = self.done_tx.send_replace(true);
660    }
661}
662
663impl State {
664    /// Marks the link ended once, with `err`, or as closed when its owner was
665    /// closing it: the error it ended with, or `None` when it had already
666    /// ended.
667    fn mark_ended(&mut self, err: LinkError) -> Option<LinkError> {
668        if self.ended.is_some() {
669            return None;
670        }
671        let err = if self.closing { LinkError::Closed } else { err };
672        self.ended = Some(err.clone());
673        Some(err)
674    }
675}
676
677/// Sends each statement the issuer reissues as a STATUS, until the link
678/// ends. A STATUS carries no neighbour signature and takes no seq.
679async fn send_statements(link: std::sync::Weak<Inner>, mut statements: StatementSubscription) {
680    loop {
681        let Some(done) = link.upgrade().map(|l| l.done_rx.clone()) else {
682            return;
683        };
684        let mut done = done;
685        let statement = tokio::select! {
686            _ = done.wait_for(|ended| *ended) => return,
687            statement = statements.recv() => statement,
688        };
689        let (Some(statement), Some(inner)) = (statement, link.upgrade()) else {
690            return;
691        };
692        if let Err(e) = inner
693            .control
694            .write(&handshake::status_frame(&statement), MAX_FRAME_BYTES)
695            .await
696        {
697            inner.end(e);
698            return;
699        }
700    }
701}
702
703/// The frame types the link acts on from the station's control stream; any
704/// other is read past, counted.
705const CONTROL_FRAME_TYPES: &[&str] = &[
706    "status",
707    "liveness_ping",
708    "liveness_pong",
709    "event",
710    "result",
711    "error",
712    "call",
713    "goodbye",
714    "register_ack",
715];
716
717/// Reads the station's frames until the link ends.
718async fn read_control(inner: Arc<Inner>, mut recv: quinn::RecvStream) {
719    let mut done = inner.done_rx.clone();
720    loop {
721        let payload = tokio::select! {
722            _ = done.wait_for(|ended| *ended) => return,
723            payload = read_frame(&mut recv, MAX_FRAME_BYTES) => payload,
724        };
725        let outcome = match payload {
726            Ok(payload) => received(&inner, &payload),
727            Err(e) => Err(e),
728        };
729        if let Err(e) = outcome {
730            inner.end(e);
731            return;
732        }
733    }
734}
735
736/// A frame from the station's control stream as wire 1.0 reads it
737/// (WIRE_1_0.md section 3): its envelope, and the frame through its type's
738/// table when it is a control frame; `None` for a type the link does not act
739/// on, counted as `wire_unknown_frame_type`.
740fn control_frame(payload: &[u8]) -> Result<Option<Envelope>, FrameError> {
741    let v = cbor::decode(payload).map_err(|_| FrameError::Malformed)?;
742    let envelope = frame::read_envelope(&v)?;
743    if frame::unknown_frame_type(&envelope, CONTROL_FRAME_TYPES).is_some() {
744        return Ok(None);
745    }
746    if frame::is_control_frame(&envelope.frame_type) {
747        frame::read_control(&envelope.frame)?;
748    }
749    Ok(Some(envelope))
750}
751
752/// One frame from the station, read by [`control_frame`]: one of a type the
753/// link does not act on, or with an unknown critical extension, is dropped
754/// and counted with the link left up; one that does not read ends the link.
755/// Then a STATUS renews the station's statement, a liveness frame is
756/// answered or handed to the probe, and a GOODBYE ends the link.
757fn received(inner: &Arc<Inner>, payload: &[u8]) -> Result<(), LinkError> {
758    let envelope = match control_frame(payload) {
759        Ok(Some(envelope)) => envelope,
760        Ok(None) => {
761            inner.count("unknown_frame_type");
762            return Ok(());
763        }
764        Err(FrameError::ExtensionUnsupported(_)) => {
765            inner.count("extension_unsupported");
766            return Ok(());
767        }
768        Err(e) => return Err(LinkError::Frame(e)),
769    };
770    let v = envelope.frame;
771    match envelope.frame_type.as_str() {
772        "status" => {
773            let expires_at = handshake::read_status_frame(
774                &v,
775                &Peer {
776                    profile: inner.profile,
777                    identity_key: inner.station.identity_key.clone(),
778                    binding: inner.station.tls_binding.clone(),
779                    now_ms: now_ms(),
780                },
781            )?;
782            inner
783                .status_deadline
784                .store(expires_at + STATUS_GRACE_MS, Ordering::SeqCst);
785        }
786        "liveness_ping" | "liveness_pong" => {
787            if let Some((kind, nonce)) = frame::liveness_nonce(&v) {
788                return liveness(inner, kind, nonce);
789            }
790        }
791        "event" => pubsub::evented(inner, &v),
792        "result" | "error" => call::replied(inner, &v),
793        "call" => serve::called(inner, &v),
794        "register_ack" => registered(inner, &v)?,
795        "goodbye" => {
796            let reason = match v.get("reason") {
797                Some(Value::Text(r)) => r.clone(),
798                _ => String::new(),
799            };
800            return Err(LinkError::Goodbye(reason));
801        }
802        other => inner.count(other),
803    }
804    Ok(())
805}
806
807/// The station's REGISTER_ACK: a refused ADVERTISE of the served
808/// procedure's current record ends that serving, a refused SUBSCRIBE of this
809/// node ends the subscription, and every ack is counted by its kind and outcome. One of a
810/// kind this crate does not know is read past, counted.
811fn registered(inner: &Arc<Inner>, v: &Value) -> Result<(), LinkError> {
812    let Some(ack) = frame::read_register_ack(v)? else {
813        inner.count("register_ack_unknown_kind");
814        return Ok(());
815    };
816    let outcome = ack.refusal.as_deref().unwrap_or("accepted");
817    inner.count(&format!("register_ack_{:?}_{outcome}", ack.kind).to_lowercase());
818    let Some(reason) = ack.refusal.clone() else {
819        return Ok(());
820    };
821    let refused = LinkError::RegisterRefused {
822        kind: ack.kind,
823        reason,
824    };
825    match (ack.kind, &ack.answered) {
826        (frame::RegisterKind::Advertise, frame::Answered::Advertisement { record_version }) => {
827            serve::advertise_refused(
828                inner,
829                &(ack.realm, ack.name.clone()),
830                record_version,
831                refused,
832            )
833        }
834        (frame::RegisterKind::Subscribe, answered)
835            if subscribe_ack_is_ours(answered, &inner.self_id) =>
836        {
837            pubsub::subscribe_refused(inner, &(ack.realm, ack.name.clone()))
838        }
839        (frame::RegisterKind::Subscribe, _) => inner.count("register_ack_subscribe_not_ours"),
840        _ => {}
841    }
842    Ok(())
843}
844
845/// Whether a SUBSCRIBE ack answers this link's own node: one naming another
846/// subscriber is counted and ends nothing.
847fn subscribe_ack_is_ours(answered: &frame::Answered, self_id: &[u8; 32]) -> bool {
848    matches!(answered, frame::Answered::Subscription { subscriber } if subscriber == self_id)
849}
850
851/// Answers the station's liveness_ping with a liveness_pong of the same
852/// nonce, and hands a liveness_pong to the probe. Neither goes further.
853fn liveness(
854    inner: &Arc<Inner>,
855    kind: Liveness,
856    nonce: [u8; frame::LIVENESS_NONCE_SIZE],
857) -> Result<(), LinkError> {
858    match kind {
859        // A ping while a pong is still being written is not answered again:
860        // the station's probe needs one answer per interval, not one per
861        // ping, and a burst of pings starts one task, not one each.
862        Liveness::Ping if !pong_slot(&inner.pong_in_flight) => {}
863        Liveness::Ping => {
864            tokio::spawn(send_pong(inner.clone(), nonce));
865        }
866        Liveness::Pong => {
867            let _ = inner.pongs.try_send(nonce);
868        }
869    }
870    Ok(())
871}
872
873/// Writes the liveness_pong of `nonce`, gives back the pong slot, and ends
874/// the link when the write failed.
875async fn send_pong(inner: Arc<Inner>, nonce: [u8; frame::LIVENESS_NONCE_SIZE]) {
876    let sent = inner
877        .send_control(&frame::liveness_pong_frame(&nonce))
878        .await;
879    inner.pong_in_flight.store(false, Ordering::Release);
880    if let Err(e) = sent {
881        inner.end(e);
882    }
883}
884
885/// Takes the one pong slot: true when no pong was in flight, and it is now.
886fn pong_slot(in_flight: &AtomicBool) -> bool {
887    !in_flight.swap(true, Ordering::AcqRel)
888}
889
890/// Ends the link when the station's statement lapses past the grace, or its
891/// TLS binding reaches its not_after.
892async fn watch_expiries(link: std::sync::Weak<Inner>) {
893    loop {
894        let Some(inner) = link.upgrade() else { return };
895        let now = now_ms();
896        if now >= inner.station.binding_not_after {
897            inner.end(LinkError::BindingExpired);
898            return;
899        }
900        let status_deadline = inner.status_deadline.load(Ordering::SeqCst);
901        if now >= status_deadline {
902            inner.end(LinkError::StatusExpired);
903            return;
904        }
905        let wait = (status_deadline.min(inner.station.binding_not_after) - now).clamp(1, 60_000);
906        let mut done = inner.done_rx.clone();
907        drop(inner);
908        tokio::select! {
909            _ = done.wait_for(|ended| *ended) => return,
910            _ = tokio::time::sleep(Duration::from_millis(wait as u64)) => {}
911        }
912    }
913}
914
915/// Ends the link when its QUIC connection closes.
916async fn watch_connection(link: std::sync::Weak<Inner>) {
917    let Some(connection) = link.upgrade().map(|l| l.connection.clone()) else {
918        return;
919    };
920    let cause = connection.closed().await;
921    if let Some(inner) = link.upgrade() {
922        inner.end(LinkError::Io(cause.to_string()));
923    }
924}
925
926fn now_ms() -> i64 {
927    crate::uuid_v7::now_ms() as i64
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933
934    /// macula-rust#9: a burst of pings takes one pong slot until its pong is
935    /// written.
936    /// A refused SUBSCRIBE ends a subscription only when it names this
937    /// link's own node as the subscriber.
938    #[test]
939    fn a_refused_subscribe_of_another_subscriber_is_not_ours() {
940        let me = [2u8; 32];
941        let ours =
942            |subscriber| subscribe_ack_is_ours(&frame::Answered::Subscription { subscriber }, &me);
943        assert!(ours(me));
944        assert!(!ours([1u8; 32]));
945        let advert = frame::Answered::Advertisement {
946            record_version: [0; 16],
947        };
948        assert!(!subscribe_ack_is_ours(&advert, &me));
949    }
950
951    #[test]
952    fn a_burst_of_pings_starts_one_pong_at_a_time() {
953        let in_flight = AtomicBool::new(false);
954        let taken = (0..1000).filter(|_| pong_slot(&in_flight)).count();
955        assert_eq!(taken, 1);
956        in_flight.store(false, Ordering::Release);
957        assert!(pong_slot(&in_flight), "free again once the pong is written");
958    }
959
960    /// The wire 1.0 envelope vectors (macula#99): what the link does with each
961    /// frame after HELLO, on the control stream or a dedicated stream, and
962    /// with a length header alone.
963    #[test]
964    fn the_wire_1_0_envelope_vectors() {
965        let file = crate::wire_vectors::load("envelope");
966        for c in &file.cases {
967            let before = frame::wire_counters();
968            let (outcome, reason, quic_code) = envelope_verdict(c);
969            let after = frame::wire_counters();
970            assert_eq!((outcome, reason), (c.outcome(), c.reason()), "{}", c.id);
971            let code = c.expect_int("quic_code");
972            assert!(
973                code.is_none() || code == quic_code.map(i64::from),
974                "{}",
975                c.id
976            );
977            assert!(
978                counted(c.expect_text("count"), &before, &after),
979                "{}: not counted",
980                c.id
981            );
982        }
983    }
984
985    /// Whether the counter a case names went up; a case that names none
986    /// passes.
987    fn counted(name: &str, before: &frame::WireCounts, after: &frame::WireCounts) -> bool {
988        let ext = |w: &frame::WireCounts| w.ext_ignored.values().sum::<u64>();
989        match name {
990            "wire_unknown_frame_type" => after.unknown_frame_type > before.unknown_frame_type,
991            "wire_ext_ignored" => ext(after) > ext(before),
992            "wire_ext_refused" => {
993                let refused = |w: &frame::WireCounts| w.ext_refused.values().sum::<u64>();
994                refused(after) > refused(before)
995            }
996            _ => true,
997        }
998    }
999
1000    /// The link's verdict on one envelope case, in the vectors' terms.
1001    fn envelope_verdict(
1002        c: &crate::wire_vectors::Case,
1003    ) -> (&'static str, &'static str, Option<u32>) {
1004        if c.has("length_header") {
1005            let length = u32::from_be_bytes(c.hex("length_header").try_into().unwrap());
1006            return match framing::length_within(length as usize, MAX_FRAME_BYTES) {
1007                Ok(()) => ("accept", "", None),
1008                Err(_) => ("refuse", "frame_too_large", None),
1009            };
1010        }
1011        let payload = c.hex("frame");
1012        if c.text("position") == "dedicated_stream" {
1013            return match stream::unknown_on_stream(&payload) {
1014                Some(code) => ("abort_stream", "", Some(code)),
1015                None => ("accept", "", None),
1016            };
1017        }
1018        match control_frame(&payload) {
1019            Ok(Some(_)) => ("accept", "", None),
1020            Ok(None) => ("ignore", "", None),
1021            Err(FrameError::ExtensionUnsupported(_)) => ("refuse", "extension_unsupported", None),
1022            Err(_) => ("refuse", "malformed_frame", None),
1023        }
1024    }
1025}