moqtap_client/draft14/connection.rs
1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft14::endpoint::{Endpoint, EndpointError};
4use crate::draft14::event::{ClientEvent, Direction, StreamKind};
5use crate::draft14::observer::ConnectionObserver;
6use crate::draft14::session::request_id::Role;
7use crate::draft14::session::setup;
8use crate::forwarding_preference::ObjectForwardingPreference;
9use crate::malformed_tracks::MalformedTrackCondition;
10use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
11use crate::transport::{RecvStream, SendStream, Transport, TransportError};
12use moqtap_codec::dispatch::{
13 AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
14};
15use moqtap_codec::draft14::data_stream::{FetchObject, SubgroupObject, SubgroupObjectReader};
16use moqtap_codec::draft14::message::ControlMessage;
17use moqtap_codec::error::CodecError;
18use moqtap_codec::kvp::KeyValuePair;
19use moqtap_codec::types::*;
20use moqtap_codec::varint::VarInt;
21use moqtap_codec::version::DraftVersion;
22
23/// MoQT ALPN identifier (used by raw QUIC transport).
24pub const MOQT_ALPN: &[u8] = b"moq-00";
25
26/// Errors from the connection layer.
27#[derive(Debug, thiserror::Error)]
28pub enum ConnectionError {
29 /// Endpoint state machine error.
30 #[error("endpoint error: {0}")]
31 Endpoint(#[from] EndpointError),
32 /// Wire codec error.
33 #[error("codec error: {0}")]
34 Codec(#[from] CodecError),
35 /// Transport-level error.
36 #[error("transport error: {0}")]
37 Transport(#[from] TransportError),
38 /// Variable-length integer decoding error.
39 #[error("varint error: {0}")]
40 VarInt(#[from] moqtap_codec::varint::VarIntError),
41 /// Control stream was not opened.
42 #[error("control stream not open")]
43 NoControlStream,
44 /// Stream ended before a complete message was read.
45 #[error("unexpected end of stream")]
46 UnexpectedEnd,
47 /// Stream was finished by the peer.
48 #[error("stream finished")]
49 StreamFinished,
50 /// Invalid server address string.
51 #[error("invalid server address: {0}")]
52 InvalidAddress(String),
53 /// TLS configuration error.
54 #[error("TLS config error: {0}")]
55 TlsConfig(String),
56 /// Data stream used out of order (e.g. object before header).
57 #[error("data stream state error: {0}")]
58 DataStreamState(&'static str),
59 /// A control message this build decoded for draft-14 and then could not
60 /// narrow to draft-14's own message type.
61 ///
62 /// Unreachable, and that is not the same as harmless. `read_control`
63 /// decodes with this connection's own draft, so the `AnyControlMessage` it
64 /// hands back can only carry this draft's variant — but the narrowing arm
65 /// is compiled in every configuration anyway, under
66 /// `#[allow(unreachable_patterns)]` rather than a `cfg` naming the other
67 /// drafts, because such a list has to be edited in every per-draft
68 /// module whenever a draft is added and a copy that omits one leaves the
69 /// match non-exhaustive.
70 ///
71 /// Spelled as `CodecError::UnknownMessageType(0)` it would not stay inert:
72 /// every draft's
73 /// [`codec_session_error_code`](Connection::codec_session_error_code)
74 /// answers that variant `Some(PROTOCOL_VIOLATION)`. So the day the
75 /// narrowing did fail, this build's own defect would reach a caller as *the
76 /// peer sent a control message type this draft does not assign, and the
77 /// session must be closed with a Protocol Violation* — carrying `0x00` as
78 /// the codepoint that proved it. A conformance report reading that
79 /// publishes a named, well-evidenced accusation against a relay for
80 /// something no relay did.
81 ///
82 /// A variant of its own is what stops that.
83 /// [`draft_specific_cause`](Connection::draft_specific_cause) answers it
84 /// [`LocalRefusal`], the facade turns that into [`ErrorCause::Facade`], and
85 /// nothing downstream can read a rule out of a cause that says nothing
86 /// reached the wire. What is pinned is the consequence rather than the
87 /// unreachability: nothing pins the arm's reachability, which is exactly
88 /// why the consequence must not be an accusation.
89 ///
90 /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
91 /// [`ErrorCause::Facade`]: crate::dispatch::ErrorCause::Facade
92 #[error(
93 "a control message decoded for draft-14 did not narrow to draft-14: a defect in this build, and evidence about nothing the peer did"
94 )]
95 ControlMessageNarrowing,
96}
97
98impl From<crate::transport::DialError> for ConnectionError {
99 /// Maps a dial failure onto the variants this error already has, so a
100 /// caller matches `InvalidAddress` or `TlsConfig`.
101 ///
102 /// # `LocalSocket` joins `InvalidAddress`, and that is the answer being kept
103 ///
104 /// A socket this machine would not open has a variant of its own on
105 /// [`DialError`](crate::transport::DialError), and it still arrives here.
106 /// Not laziness about the churn — `InvalidAddress` is one of the
107 /// variants the facade reads as
108 /// [`ErrorCause::Facade`](crate::dispatch::ErrorCause::Facade), which
109 /// `is_local` answers **true** for, and a failed bind is this side's by
110 /// definition. Routing it to `Transport` would read better in prose and
111 /// would publish this machine's missing IPv6 stack as the relay's doing.
112 ///
113 /// The phase is not lost, only unread on this path. A caller measuring
114 /// which stage of a dial died reads
115 /// [`DialError::phase`](crate::transport::DialError::phase) off the dial
116 /// itself; a caller who arrived at this type named a `host:port` and asked
117 /// for a connection, not for a measurement, and a public variant here for
118 /// a distinction nothing on this path reads is churn with no reader, which
119 /// is why this impl stays flat.
120 fn from(e: crate::transport::DialError) -> Self {
121 match e {
122 // Two variants, one arm, deliberately — see above.
123 crate::transport::DialError::InvalidAddress(s)
124 | crate::transport::DialError::LocalSocket(s) => ConnectionError::InvalidAddress(s),
125 crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
126 crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
127 }
128 }
129}
130
131/// Transport type for the connection.
132#[derive(Debug, Clone)]
133pub enum TransportType {
134 /// Raw QUIC via quinn. The `addr` field should be `host:port`.
135 Quic,
136 /// WebTransport via wtransport. The `url` field is the WebTransport URL.
137 WebTransport {
138 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
139 url: String,
140 },
141}
142
143/// Configuration for a MoQT client connection.
144///
145/// Both `draft` and `transport` are required — there is no `Default` impl.
146pub struct ClientConfig {
147 /// The MoQT draft version to use (primary, determines codec/framing).
148 pub draft: DraftVersion,
149 /// Additional draft versions to offer in CLIENT_SETUP.
150 /// The primary `draft` is always included first.
151 pub additional_versions: Vec<DraftVersion>,
152 /// The transport type (QUIC or WebTransport).
153 pub transport: TransportType,
154 /// Whether to skip TLS certificate verification (for testing).
155 pub skip_cert_verification: bool,
156 /// Custom CA certificates to trust (DER-encoded).
157 pub ca_certs: Vec<Vec<u8>>,
158 /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
159 pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
160}
161
162impl ClientConfig {
163 /// Returns the MoQT version varints for the CLIENT_SETUP message.
164 /// Primary draft first, then any additional versions.
165 pub fn supported_versions(&self) -> Vec<VarInt> {
166 let mut versions = vec![self.draft.version_varint()];
167 for v in &self.additional_versions {
168 let varint = v.version_varint();
169 if !versions.contains(&varint) {
170 versions.push(varint);
171 }
172 }
173 versions
174 }
175
176 /// Returns the ALPN protocol identifiers for the transport.
177 pub fn alpn(&self) -> Vec<Vec<u8>> {
178 match &self.transport {
179 TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
180 TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
181 }
182 }
183}
184
185/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
186pub struct FramedSendStream {
187 inner: SendStream,
188 draft: DraftVersion,
189 /// Stateful subgroup object encoder. Initialized by
190 /// [`FramedSendStream::write_subgroup_header`] and used by
191 /// [`FramedSendStream::write_subgroup_object`] to track the delta-encoded
192 /// object ID state.
193 subgroup_io: Option<SubgroupObjectReader>,
194}
195
196impl FramedSendStream {
197 /// Create a new framed send stream for the given draft version.
198 pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
199 Self { inner, draft, subgroup_io: None }
200 }
201
202 /// Get the transport-level stream ID.
203 pub fn stream_id(&self) -> u64 {
204 self.inner.stream_id()
205 }
206
207 /// Write a control message to the stream with type+length framing.
208 /// Returns the raw bytes that were written (for event capture).
209 pub async fn write_control(
210 &mut self,
211 msg: &AnyControlMessage,
212 ) -> Result<Vec<u8>, ConnectionError> {
213 let mut buf = Vec::new();
214 msg.encode(&mut buf)?;
215 self.inner.write_all(&buf).await?;
216 Ok(buf)
217 }
218
219 /// Write a subgroup stream header. Also initializes the internal
220 /// delta-encoding state used by [`FramedSendStream::write_subgroup_object`].
221 ///
222 /// The header is refused, and nothing is written, if its fields disagree
223 /// with its own stream type. That check has to happen here rather than at
224 /// the first object: the type is what every object after it is framed
225 /// against, so a header that went out saying the wrong thing cannot be
226 /// taken back.
227 pub async fn write_subgroup_header(
228 &mut self,
229 header: &AnySubgroupHeader,
230 ) -> Result<(), ConnectionError> {
231 let mut buf = Vec::new();
232 header.encode_stream_checked(&mut buf)?;
233 self.inner.write_all(&buf).await?;
234 // Clippy would rather see these two arms as an `if let`, and rustc rejects
235 // that in a single-draft build, where the pattern is irrefutable. Only a
236 // `match` satisfies both.
237 #[allow(clippy::single_match)]
238 match header {
239 AnySubgroupHeader::Draft14(ref d14) => {
240 self.subgroup_io = Some(SubgroupObjectReader::new(d14));
241 }
242 // Only this draft's header seeds the object reader. With draft 14 the only enabled
243 // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
244 // one unreachable. Compiled in every configuration with the lint allowed, rather than
245 // gated on a `cfg` naming the other drafts: such a list has to be edited in
246 // every draft module whenever a draft is added, and a copy that omits one leaves this
247 // match non-exhaustive.
248 #[allow(unreachable_patterns)]
249 _ => {}
250 }
251 Ok(())
252 }
253
254 /// Write a fetch response header.
255 pub async fn write_fetch_header(
256 &mut self,
257 header: &AnyFetchHeader,
258 ) -> Result<(), ConnectionError> {
259 let mut buf = Vec::new();
260 header.encode_stream(&mut buf);
261 self.inner.write_all(&buf).await?;
262 Ok(())
263 }
264
265 /// Append a draft-14 subgroup object to the stream. Uses the stateful
266 /// reader installed by [`FramedSendStream::write_subgroup_header`] to
267 /// produce the correct delta-encoded object ID. Returns an error if
268 /// called before a subgroup header was written.
269 pub async fn write_subgroup_object(
270 &mut self,
271 object: &SubgroupObject,
272 ) -> Result<(), ConnectionError> {
273 let writer = self
274 .subgroup_io
275 .as_mut()
276 .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
277 let mut buf = Vec::new();
278 writer.write_object(object, &mut buf)?;
279 self.inner.write_all(&buf).await?;
280 Ok(())
281 }
282
283 /// Append a draft-14 fetch object to the stream. Fetch objects are
284 /// self-contained (each carries its own group/subgroup/object IDs and
285 /// priority), so no prior `write_fetch_header` bookkeeping is required.
286 /// Append a fetch object to the stream.
287 ///
288 /// Draft-14 derives the declared length from the payload itself rather than
289 /// holding it as a field, so there is nothing here for the two to disagree
290 /// about - unlike the subgroup object, whose header carries the length and
291 /// whose writer has to overwrite it.
292 ///
293 /// # Errors
294 ///
295 /// Whatever the checked encoder refuses: a payload beside a status that
296 /// forbids one, or a status the draft leaves unassigned.
297 pub async fn write_fetch_object(
298 &mut self,
299 object: &FetchObject,
300 ) -> Result<(), ConnectionError> {
301 let mut buf = Vec::new();
302 object.encode_checked(&mut buf)?;
303 self.inner.write_all(&buf).await?;
304 Ok(())
305 }
306
307 /// Finish the stream (send FIN).
308 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
309 self.inner.finish()?;
310 Ok(())
311 }
312
313 /// Returns the draft version this stream is framed for.
314 pub fn draft(&self) -> DraftVersion {
315 self.draft
316 }
317}
318
319/// What an Object Status makes of an object here.
320///
321/// Two answers where drafts 08 through 13 have three, and the missing one is
322/// the point: the end-of-track status settles where the track ended and is
323/// judged against nothing, because the rule about where one may be placed is
324/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
325/// that acceptance.
326///
327/// Every other status is a statement about objects rather than one of them.
328fn object_role(status: Option<u64>) -> ObjectRole {
329 match status {
330 None | Some(0x0) => ObjectRole::Produced,
331 Some(0x4) => ObjectRole::EndsTrack(None),
332 _ => ObjectRole::Neither,
333 }
334}
335
336/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
337pub struct FramedRecvStream {
338 inner: RecvStream,
339 buf: BytesMut,
340 draft: DraftVersion,
341 /// Stateful subgroup object decoder. Initialized by
342 /// [`FramedRecvStream::read_subgroup_header`] and used by
343 /// [`FramedRecvStream::read_subgroup_object`] to track delta-encoded
344 /// object IDs and whether extension headers are present.
345 subgroup_io: Option<SubgroupObjectReader>,
346 /// The record this stream's objects are measured against, and the Group ID
347 /// its header named.
348 ///
349 /// One group for the whole stream: a subgroup header names it once and no
350 /// object header repeats it. `None` on a stream that was never given one -
351 /// a stream for an alias no live binding names, and every stream built
352 /// outside [`Connection::accept_subgroup_stream`] - and such a stream reads
353 /// without being measured, because `note_subgroup_object` has nothing to
354 /// measure it against.
355 tracking: Option<(TrackObjects, u64)>,
356}
357
358impl FramedRecvStream {
359 /// Create a new framed receive stream for the given draft version.
360 pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
361 Self { inner, buf: BytesMut::with_capacity(4096), draft, subgroup_io: None, tracking: None }
362 }
363
364 /// Get the transport-level stream ID.
365 pub fn stream_id(&self) -> u64 {
366 self.inner.stream_id()
367 }
368
369 /// Measure this stream's objects against `objects`, all of them in `group`.
370 ///
371 /// Called by [`Connection::accept_subgroup_stream`] once the header has
372 /// been read, which is the only point at which both the track and the group
373 /// are known.
374 fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
375 self.tracking = Some((objects, group));
376 }
377
378 /// Judge one object this stream carried against where its track ended.
379 ///
380 /// The object's Group ID is the stream's and its Object ID is its own,
381 /// already resolved from the delta the wire carries; what they are measured
382 /// against is the end an end-of-track object settled on any stream.
383 fn note_subgroup_object(
384 &self,
385 object: u64,
386 status: Option<u64>,
387 ) -> Result<(), ConnectionError> {
388 let Some((objects, group)) = &self.tracking else { return Ok(()) };
389 let at = ObjectLocation { group: *group, object };
390 objects.note_past_final(at, object_role(status)).map_err(|end| {
391 ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
392 alias: objects.alias(),
393 group: at.group,
394 object: at.object,
395 final_group: end.group,
396 final_object: end.object,
397 })
398 })
399 }
400
401 /// Read more data from the stream into the internal buffer.
402 async fn fill(&mut self) -> Result<bool, ConnectionError> {
403 let mut tmp = [0u8; 4096];
404 match self.inner.read(&mut tmp).await {
405 Ok(Some(n)) => {
406 self.buf.extend_from_slice(&tmp[..n]);
407 Ok(true)
408 }
409 Ok(None) => Ok(false),
410 Err(e) => Err(ConnectionError::Transport(e)),
411 }
412 }
413
414 /// Ensure at least `n` bytes are available in the buffer.
415 async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
416 while self.buf.len() < n {
417 if !self.fill().await? {
418 return Err(ConnectionError::UnexpectedEnd);
419 }
420 }
421 Ok(())
422 }
423
424 /// Read a control message from the stream.
425 ///
426 /// When `capture_raw` is true, the returned tuple includes a clone of the
427 /// framed wire bytes (for observer emission). When false, the second
428 /// element is `None` and the payload clone is skipped.
429 pub async fn read_control(
430 &mut self,
431 capture_raw: bool,
432 ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
433 // Read type ID varint
434 self.ensure(1).await?;
435 let type_len = varint_len(self.buf[0]);
436 self.ensure(type_len).await?;
437
438 let mut cursor = &self.buf[..type_len];
439 let _type_id = VarInt::decode(&mut cursor)?;
440
441 // Read payload length (16-bit BE for draft-11+, varint for earlier drafts)
442 let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
443 self.ensure(type_len + 2).await?;
444 let hi = self.buf[type_len] as usize;
445 let lo = self.buf[type_len + 1] as usize;
446 ((hi << 8) | lo, 2)
447 } else {
448 self.ensure(type_len + 1).await?;
449 let payload_len_start = type_len;
450 let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
451 self.ensure(type_len + payload_len_varint_len).await?;
452 let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
453 let payload_len = VarInt::decode(&mut cursor)?.into_inner() as usize;
454 (payload_len, payload_len_varint_len)
455 };
456
457 // Read full payload
458 let total = type_len + len_field_size + payload_len;
459 self.ensure(total).await?;
460
461 // Capture raw bytes only if requested (observer attached).
462 let raw = capture_raw.then(|| self.buf[..total].to_vec());
463
464 // Now decode the whole message
465 let mut frame = &self.buf[..total];
466 let msg = AnyControlMessage::decode(self.draft, &mut frame)?;
467 self.buf.advance(total);
468 Ok((msg, raw))
469 }
470
471 /// Read a subgroup stream header. Also initializes the internal
472 /// delta-decoding state used by
473 /// [`FramedRecvStream::read_subgroup_object`].
474 pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
475 self.ensure(1).await?;
476 loop {
477 let mut cursor = &self.buf[..];
478 match AnySubgroupHeader::decode(self.draft, &mut cursor) {
479 Ok(header) => {
480 let consumed = self.buf.len() - cursor.remaining();
481 self.buf.advance(consumed);
482 // Clippy would rather see these two arms as an `if let`, and rustc rejects
483 // that in a single-draft build, where the pattern is irrefutable. Only a
484 // `match` satisfies both.
485 #[allow(clippy::single_match)]
486 match header {
487 AnySubgroupHeader::Draft14(ref d14) => {
488 self.subgroup_io = Some(SubgroupObjectReader::new(d14));
489 }
490 // Only this draft's header seeds the object reader. With draft 14 the only
491 // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
492 // exhaustive and this one unreachable. Compiled in every configuration with
493 // the lint allowed, rather than gated on a `cfg` naming the other thirteen
494 // drafts: such a list has to be edited in every draft module whenever a
495 // draft is added, and a copy that omits one leaves this match
496 // non-exhaustive.
497 #[allow(unreachable_patterns)]
498 _ => {}
499 }
500 return Ok(header);
501 }
502 Err(e) if e.is_incomplete() => {
503 if !self.fill().await? {
504 return Err(ConnectionError::UnexpectedEnd);
505 }
506 }
507 Err(e) => return Err(ConnectionError::Codec(e)),
508 }
509 }
510 }
511
512 /// Read a fetch response header.
513 pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
514 self.ensure(1).await?;
515 loop {
516 let mut cursor = &self.buf[..];
517 match AnyFetchHeader::decode(self.draft, &mut cursor) {
518 Ok(header) => {
519 let consumed = self.buf.len() - cursor.remaining();
520 self.buf.advance(consumed);
521 return Ok(header);
522 }
523 Err(e) if e.is_incomplete() => {
524 if !self.fill().await? {
525 return Err(ConnectionError::UnexpectedEnd);
526 }
527 }
528 Err(e) => return Err(ConnectionError::Codec(e)),
529 }
530 }
531 }
532
533 /// Read the next draft-14 subgroup object from this stream. Uses the
534 /// stateful reader installed by
535 /// [`FramedRecvStream::read_subgroup_header`] to decode the delta-
536 /// encoded object ID and handle extension headers per the stream type.
537 /// Returns an error if called before a subgroup header was read.
538 pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
539 if self.subgroup_io.is_none() {
540 return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
541 }
542 loop {
543 let reader = self.subgroup_io.as_mut().unwrap();
544 let mut probe = reader.clone();
545 let mut cursor = &self.buf[..];
546 match probe.read_object(&mut cursor) {
547 Ok(obj) => {
548 let consumed = self.buf.len() - cursor.remaining();
549 self.buf.advance(consumed);
550 // Commit state from the successful probe decode.
551 *reader = probe;
552 self.note_subgroup_object(
553 obj.object_id.into_inner(),
554 obj.status.map(|s| s as u64),
555 )?;
556 return Ok(obj);
557 }
558 Err(e) if e.is_incomplete() => {
559 if !self.fill().await? {
560 return Err(ConnectionError::UnexpectedEnd);
561 }
562 }
563 Err(e) => return Err(ConnectionError::Codec(e)),
564 }
565 }
566 }
567
568 /// Read the next draft-14 fetch object from this stream. Fetch objects
569 /// are self-describing, so no prior `read_fetch_header` state is needed
570 /// to decode each object.
571 pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
572 loop {
573 let mut cursor = &self.buf[..];
574 match FetchObject::decode(&mut cursor) {
575 Ok(obj) => {
576 let consumed = self.buf.len() - cursor.remaining();
577 self.buf.advance(consumed);
578 return Ok(obj);
579 }
580 Err(e) if e.is_incomplete() => {
581 if !self.fill().await? {
582 return Err(ConnectionError::UnexpectedEnd);
583 }
584 }
585 Err(e) => return Err(ConnectionError::Codec(e)),
586 }
587 }
588 }
589
590 /// Returns the draft version this stream is framed for.
591 pub fn draft(&self) -> DraftVersion {
592 self.draft
593 }
594}
595
596/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
597/// state machine with actual network I/O.
598pub struct Connection {
599 transport: Transport,
600 endpoint: Endpoint,
601 draft: DraftVersion,
602 /// Behind a lock because a control message is written from two kinds of
603 /// place. Most of them are the caller's own request, made through `&mut
604 /// self`. The messages that answer a Malformed Track are not: the
605 /// conditions that make a track malformed are detected on the data plane,
606 /// where this connection is reached through a shared reference. The lock
607 /// also makes one message the unit of writing, so two of them cannot
608 /// interleave on the stream.
609 control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
610 control_recv: Option<FramedRecvStream>,
611 observer: Option<Box<dyn ConnectionObserver>>,
612 /// Setup events buffered during `connect()` and replayed when an
613 /// observer attaches via `set_observer` — without this, an observer
614 /// attached after `connect` returns would never see the handshake.
615 pending_events: Vec<ClientEvent>,
616 /// The server's half of the setup handshake, kept whole.
617 ///
618 /// The endpoint acts on the parameters it recognises and retains none of
619 /// them, and which parameters a server sends — in what order, with what
620 /// values — is the sharpest thing a session says about the implementation
621 /// behind it.
622 server_setup: AnyControlMessage,
623 /// The framed wire bytes of [`Self::server_setup`].
624 server_setup_raw: Option<Vec<u8>>,
625}
626
627impl Connection {
628 /// Connect to a MoQT server as a client.
629 ///
630 /// Establishes a QUIC or WebTransport connection (based on `config.transport`),
631 /// opens a bidirectional control stream, performs the CLIENT_SETUP /
632 /// SERVER_SETUP handshake, and returns a ready-to-use connection.
633 pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
634 // PATH is for native QUIC only, and the transport is known here and
635 // nowhere further in. Refusing before dialling means a session that
636 // the server would close on sight is never opened.
637 setup::validate_client_path_transport(
638 &config.setup_parameters,
639 matches!(config.transport, TransportType::WebTransport { .. }),
640 )
641 .map_err(EndpointError::from)?;
642
643 let transport = match &config.transport {
644 TransportType::Quic => Self::connect_quic(addr, &config).await?,
645 TransportType::WebTransport { url } => {
646 let url = url.clone();
647 Self::connect_webtransport(&url, &config).await?
648 }
649 };
650
651 Self::adopt(transport, config).await
652 }
653
654 /// Run the MoQT setup handshake over a transport somebody else established.
655 ///
656 /// For choosing the draft from what the server selected: dial once through
657 /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
658 /// connection to the module its answer names. [`Self::connect`] cannot do
659 /// this — it derives its single ALPN from the draft it was given.
660 ///
661 /// `config.draft` must match this module. The transport is adopted as
662 /// given; nothing here re-checks the ALPN it was negotiated with.
663 pub async fn adopt(
664 transport: Transport,
665 config: ClientConfig,
666 ) -> Result<Self, ConnectionError> {
667 Self::adopt_offering(transport, config, None).await
668 }
669
670 /// [`Self::adopt`], offering exactly `versions` in CLIENT_SETUP.
671 ///
672 /// `None` offers what `config` implies, which is what [`Self::adopt`]
673 /// passes. `Some` replaces the list outright, and takes raw varints rather
674 /// than [`DraftVersion`]s because the reason to reach for this is to offer
675 /// a version no draft assigns — which an enum of drafts cannot name.
676 ///
677 /// A server MUST answer with a version the client offered and MUST
678 /// otherwise close the session; from draft-11 the code for that is
679 /// `VERSION_NEGOTIATION_FAILED` (0x15). How a relay spells the refusal is
680 /// a conformance measurement, and offering a version deliberately outside
681 /// the negotiable set is the only way to ask for it.
682 pub async fn adopt_offering(
683 transport: Transport,
684 config: ClientConfig,
685 versions: Option<Vec<VarInt>>,
686 ) -> Result<Self, ConnectionError> {
687 let draft = config.draft;
688 // PATH is for native QUIC only, and the transport is known here and
689 // nowhere further in. Refusing before dialling means a session that
690 // the server would close on sight is never opened.
691 setup::validate_client_path_transport(
692 &config.setup_parameters,
693 matches!(config.transport, TransportType::WebTransport { .. }),
694 )
695 .map_err(EndpointError::from)?;
696
697 // Open bidirectional control stream
698 let (send, recv) = transport.open_bi().await?;
699 let mut control_send = FramedSendStream::new(send, draft);
700 let mut control_recv = FramedRecvStream::new(recv, draft);
701
702 // Perform setup handshake
703 let mut endpoint = Endpoint::new(Role::Client);
704 endpoint.connect()?;
705 let setup_msg = endpoint.send_client_setup(
706 versions.unwrap_or_else(|| config.supported_versions()),
707 config.setup_parameters.clone(),
708 )?;
709 let any_setup = AnyControlMessage::Draft14(setup_msg);
710 let raw_setup = control_send.write_control(&any_setup).await?;
711
712 let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
713 // Unwrap to draft-14 for the endpoint (which is draft-14 only)
714 match &server_setup {
715 AnyControlMessage::Draft14(ControlMessage::ServerSetup(ref ss)) => {
716 endpoint.receive_server_setup(ss)?;
717 }
718 _ => {
719 return Err(ConnectionError::Endpoint(EndpointError::NotActive));
720 }
721 }
722
723 let mut pending_events = Vec::with_capacity(3);
724 pending_events.push(ClientEvent::ControlMessage {
725 direction: Direction::Send,
726 message: any_setup,
727 raw: Some(raw_setup),
728 });
729 pending_events.push(ClientEvent::ControlMessage {
730 direction: Direction::Receive,
731 message: server_setup.clone(),
732 raw: raw_server_setup.clone(),
733 });
734 if let Some(v) = endpoint.negotiated_version() {
735 pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
736 }
737
738 Ok(Self {
739 transport,
740 endpoint,
741 draft,
742 control_send: Some(tokio::sync::Mutex::new(control_send)),
743 control_recv: Some(control_recv),
744 observer: None,
745 pending_events,
746 server_setup,
747 server_setup_raw: raw_server_setup,
748 })
749 }
750
751 /// Establish a raw QUIC connection.
752 ///
753 /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
754 /// TLS and endpoint setup.
755 async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
756 let (transport, _negotiated) = crate::transport::dial_quic(
757 addr,
758 &crate::transport::QuicDialOptions {
759 skip_cert_verification: config.skip_cert_verification,
760 ca_certs: config.ca_certs.clone(),
761 ..crate::transport::QuicDialOptions::new(config.alpn())
762 },
763 )
764 .await?;
765 Ok(transport)
766 }
767
768 /// Establish a WebTransport connection.
769 ///
770 /// [`crate::transport::dial_webtransport`] holds the TLS and endpoint
771 /// setup, exactly as `connect_quic` above defers its own. That is not
772 /// only deduplication: both dials must trust the same roots. Settling trust
773 /// at this call site instead — from `wtransport`'s own builder settings, or
774 /// from a second config of this draft's own — puts the decision in two
775 /// places, where it can stop matching what the QUIC dial trusts, so one
776 /// relay would pass on one transport and fail on the other and a caller's
777 /// private CA would reach only the dials whose call site installed it.
778 /// Both ask the same function what to trust.
779 #[cfg(feature = "webtransport")]
780 async fn connect_webtransport(
781 url: &str,
782 config: &ClientConfig,
783 ) -> Result<Transport, ConnectionError> {
784 Ok(crate::transport::dial_webtransport(
785 url,
786 &crate::transport::QuicDialOptions {
787 skip_cert_verification: config.skip_cert_verification,
788 ca_certs: config.ca_certs.clone(),
789 ..crate::transport::QuicDialOptions::new(config.alpn())
790 },
791 )
792 .await?)
793 }
794
795 /// Stub for when the webtransport feature is not enabled.
796 #[cfg(not(feature = "webtransport"))]
797 async fn connect_webtransport(
798 _url: &str,
799 _config: &ClientConfig,
800 ) -> Result<Transport, ConnectionError> {
801 Err(ConnectionError::Transport(TransportError::Connect(
802 "webtransport feature not enabled".into(),
803 )))
804 }
805
806 // ── Observer ───────────────────────────────────────────────
807
808 /// Attach an observer. Buffered handshake events from `connect()` are
809 /// flushed in arrival order before this returns.
810 pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
811 self.observer = Some(observer);
812 for event in self.pending_events.drain(..) {
813 if let Some(ref obs) = self.observer {
814 obs.on_event_owned(event);
815 }
816 }
817 }
818
819 /// Remove the observer.
820 pub fn clear_observer(&mut self) {
821 self.observer = None;
822 }
823
824 /// Emit an event to the observer, if one is attached.
825 fn emit(&self, event: ClientEvent) {
826 if let Some(ref obs) = self.observer {
827 obs.on_event_owned(event);
828 }
829 }
830
831 // ── Control message I/O ─────────────────────────────────
832
833 /// Send a control message on the control stream.
834 ///
835 /// Wraps the draft-14 message in `AnyControlMessage::Draft14` for framing.
836 pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
837 let any = AnyControlMessage::Draft14(msg.clone());
838 let mut send =
839 self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
840 let raw = send.write_control(&any).await?;
841 drop(send);
842 self.emit(ClientEvent::ControlMessage {
843 direction: Direction::Send,
844 message: any,
845 raw: Some(raw),
846 });
847 Ok(())
848 }
849
850 /// Read the next control message from the control stream.
851 ///
852 /// Returns the `AnyControlMessage` and also extracts the draft-14
853 /// `ControlMessage` for internal endpoint dispatch.
854 pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
855 let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
856 let capture_raw = self.observer.is_some();
857 let (any, raw) = match recv.read_control(capture_raw).await {
858 Ok(v) => v,
859 Err(e) => return Err(self.close_for_codec(e)),
860 };
861 if capture_raw {
862 self.emit(ClientEvent::ControlMessage {
863 direction: Direction::Receive,
864 message: any.clone(),
865 raw,
866 });
867 }
868 // Unwrap to draft-14 for the endpoint
869 match any {
870 AnyControlMessage::Draft14(msg) => Ok(msg),
871 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 14 the
872 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
873 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
874 // naming the other drafts: such a list has to be edited in every draft module
875 // whenever a draft is added, and a copy that omits one leaves this match
876 // non-exhaustive.
877 #[allow(unreachable_patterns)]
878 _ => Err(ConnectionError::ControlMessageNarrowing),
879 }
880 }
881
882 /// Read and dispatch the next incoming control message through the endpoint
883 /// state machine. Returns the decoded message for inspection.
884 pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
885 let msg = self.recv_control().await?;
886 self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
887
888 // Emit draining event if this was a GoAway
889 if let ControlMessage::GoAway(ref ga) = msg {
890 self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
891 }
892
893 Ok(msg)
894 }
895
896 // ── Subscribe flow ──────────────────────────────────────
897
898 /// Send a SUBSCRIBE and return the allocated request ID.
899 pub async fn subscribe(
900 &mut self,
901 track_namespace: TrackNamespace,
902 track_name: Vec<u8>,
903 subscriber_priority: u8,
904 group_order: GroupOrder,
905 filter_type: FilterType,
906 parameters: Vec<KeyValuePair>,
907 ) -> Result<VarInt, ConnectionError> {
908 let (req_id, msg) = self.endpoint.subscribe(
909 track_namespace,
910 track_name,
911 subscriber_priority,
912 group_order,
913 filter_type,
914 parameters,
915 )?;
916 self.send_control(&msg).await?;
917 Ok(req_id)
918 }
919
920 /// Send a SUBSCRIBE for a range of the track, starting at a given
921 /// location.
922 ///
923 /// The Filter Type is derived from the range, so the message cannot name a
924 /// filter whose fields it does not carry.
925 #[allow(clippy::too_many_arguments)]
926 pub async fn subscribe_range(
927 &mut self,
928 track_namespace: TrackNamespace,
929 track_name: Vec<u8>,
930 subscriber_priority: u8,
931 group_order: GroupOrder,
932 start_location: Location,
933 end_group: Option<VarInt>,
934 parameters: Vec<KeyValuePair>,
935 ) -> Result<VarInt, ConnectionError> {
936 let (req_id, msg) = self.endpoint.subscribe_range(
937 track_namespace,
938 track_name,
939 subscriber_priority,
940 group_order,
941 start_location,
942 end_group,
943 parameters,
944 )?;
945 self.send_control(&msg).await?;
946 Ok(req_id)
947 }
948
949 /// Send an UNSUBSCRIBE for the given request ID.
950 pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
951 let msg = self.endpoint.unsubscribe(request_id)?;
952 self.send_control(&msg).await
953 }
954
955 /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
956 /// its track a Track Alias.
957 ///
958 /// The endpoint refuses an alias a live track of its own already holds and
959 /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
960 /// wire when it does either.
961 pub async fn subscribe_ok(
962 &mut self,
963 request_id: VarInt,
964 track_alias: VarInt,
965 expires: VarInt,
966 group_order: GroupOrder,
967 parameters: Vec<KeyValuePair>,
968 ) -> Result<(), ConnectionError> {
969 let msg = self.endpoint.send_subscribe_ok(
970 request_id,
971 track_alias,
972 expires,
973 group_order,
974 parameters,
975 )?;
976 self.send_control(&msg).await
977 }
978
979 /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
980 ///
981 /// The endpoint refuses a second answer to one SUBSCRIBE, so nothing is
982 /// written on the wire when it does.
983 pub async fn subscribe_error(
984 &mut self,
985 request_id: VarInt,
986 error_code: VarInt,
987 reason_phrase: Vec<u8>,
988 ) -> Result<(), ConnectionError> {
989 let msg = self.endpoint.send_subscribe_error(request_id, error_code, reason_phrase)?;
990 self.send_control(&msg).await
991 }
992
993 /// Accept a PUBLISH the peer sent, which establishes the subscription it
994 /// opened.
995 ///
996 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
997 /// written on the wire when it does.
998 #[allow(clippy::too_many_arguments)]
999 pub async fn publish_ok(
1000 &mut self,
1001 request_id: VarInt,
1002 forward: Forward,
1003 subscriber_priority: u8,
1004 group_order: GroupOrder,
1005 filter_type: FilterType,
1006 start_location: Option<Location>,
1007 end_group: Option<VarInt>,
1008 ) -> Result<(), ConnectionError> {
1009 let msg = self.endpoint.send_publish_ok(
1010 request_id,
1011 forward,
1012 subscriber_priority,
1013 group_order,
1014 filter_type,
1015 start_location,
1016 end_group,
1017 )?;
1018 self.send_control(&msg).await
1019 }
1020
1021 /// Reject a PUBLISH the peer sent, which ends the subscription it opened
1022 /// before it was established.
1023 ///
1024 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1025 /// written on the wire when it does.
1026 pub async fn publish_error(
1027 &mut self,
1028 request_id: VarInt,
1029 error_code: VarInt,
1030 reason_phrase: Vec<u8>,
1031 ) -> Result<(), ConnectionError> {
1032 let msg = self.endpoint.send_publish_error(request_id, error_code, reason_phrase)?;
1033 self.send_control(&msg).await
1034 }
1035
1036 /// Send a SUBSCRIBE_UPDATE for an active subscription. Returns the
1037 /// allocated request ID of the update message.
1038 pub async fn subscribe_update(
1039 &mut self,
1040 subscription_request_id: VarInt,
1041 start_location: Location,
1042 end_group: VarInt,
1043 subscriber_priority: u8,
1044 forward: Forward,
1045 parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
1046 ) -> Result<VarInt, ConnectionError> {
1047 let (req_id, msg) = self.endpoint.subscribe_update(
1048 subscription_request_id,
1049 start_location,
1050 end_group,
1051 subscriber_priority,
1052 forward,
1053 parameters,
1054 )?;
1055 self.send_control(&msg).await?;
1056 Ok(req_id)
1057 }
1058
1059 // ── Fetch flow ──────────────────────────────────────────
1060
1061 /// Send a FETCH and return the allocated request ID.
1062 #[allow(clippy::too_many_arguments)]
1063 pub async fn fetch(
1064 &mut self,
1065 track_namespace: TrackNamespace,
1066 track_name: Vec<u8>,
1067 subscriber_priority: u8,
1068 group_order: GroupOrder,
1069 start_group: VarInt,
1070 start_object: VarInt,
1071 end_group: VarInt,
1072 end_object: VarInt,
1073 parameters: Vec<KeyValuePair>,
1074 ) -> Result<VarInt, ConnectionError> {
1075 let (req_id, msg) = self.endpoint.fetch(
1076 track_namespace,
1077 track_name,
1078 subscriber_priority,
1079 group_order,
1080 start_group,
1081 start_object,
1082 end_group,
1083 end_object,
1084 parameters,
1085 )?;
1086 self.send_control(&msg).await?;
1087 Ok(req_id)
1088 }
1089
1090 /// Send a Relative Joining Fetch and return the allocated request ID.
1091 ///
1092 /// `joining_start` is a count of groups back from the subscription's live
1093 /// edge. For the form that names the group outright, see
1094 /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1095 pub async fn joining_fetch(
1096 &mut self,
1097 subscriber_priority: u8,
1098 group_order: GroupOrder,
1099 joining_request_id: VarInt,
1100 joining_start: VarInt,
1101 parameters: Vec<KeyValuePair>,
1102 ) -> Result<VarInt, ConnectionError> {
1103 let (req_id, msg) = self.endpoint.joining_fetch(
1104 subscriber_priority,
1105 group_order,
1106 joining_request_id,
1107 joining_start,
1108 parameters,
1109 )?;
1110 self.send_control(&msg).await?;
1111 Ok(req_id)
1112 }
1113
1114 /// Send an Absolute Joining Fetch and return the allocated request ID.
1115 ///
1116 /// `joining_start` is the group to begin at.
1117 pub async fn absolute_joining_fetch(
1118 &mut self,
1119 subscriber_priority: u8,
1120 group_order: GroupOrder,
1121 joining_request_id: VarInt,
1122 joining_start: VarInt,
1123 parameters: Vec<KeyValuePair>,
1124 ) -> Result<VarInt, ConnectionError> {
1125 let (req_id, msg) = self.endpoint.absolute_joining_fetch(
1126 subscriber_priority,
1127 group_order,
1128 joining_request_id,
1129 joining_start,
1130 parameters,
1131 )?;
1132 self.send_control(&msg).await?;
1133 Ok(req_id)
1134 }
1135
1136 /// Send a FETCH_CANCEL for the given request ID.
1137 pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1138 let msg = self.endpoint.fetch_cancel(request_id)?;
1139 self.send_control(&msg).await
1140 }
1141
1142 /// Accept a fetch the peer opened, sending FETCH_OK.
1143 ///
1144 /// The endpoint refuses a Joining Fetch naming a subscription this session
1145 /// cannot join and refuses a second answer to one FETCH, so nothing is
1146 /// written on the wire when it does either.
1147 pub async fn fetch_ok(
1148 &mut self,
1149 request_id: VarInt,
1150 group_order: GroupOrder,
1151 end_of_track: u8,
1152 end_location: Location,
1153 parameters: Vec<KeyValuePair>,
1154 ) -> Result<(), ConnectionError> {
1155 let msg = self.endpoint.send_fetch_ok(
1156 request_id,
1157 group_order,
1158 end_of_track,
1159 end_location,
1160 parameters,
1161 )?;
1162 self.send_control(&msg).await
1163 }
1164
1165 /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1166 ///
1167 /// The endpoint refuses a second answer to one FETCH, and refuses a
1168 /// Joining Fetch's refusal under any code but the one the draft names for
1169 /// it, so nothing is written on the wire when it does either.
1170 pub async fn fetch_error(
1171 &mut self,
1172 request_id: VarInt,
1173 error_code: VarInt,
1174 reason_phrase: Vec<u8>,
1175 ) -> Result<(), ConnectionError> {
1176 let msg = self.endpoint.send_fetch_error(request_id, error_code, reason_phrase)?;
1177 self.send_control(&msg).await
1178 }
1179
1180 // ── Namespace flows ─────────────────────────────────────
1181
1182 /// Send a SUBSCRIBE_NAMESPACE and return the request ID.
1183 pub async fn subscribe_namespace(
1184 &mut self,
1185 track_namespace: TrackNamespace,
1186 parameters: Vec<KeyValuePair>,
1187 ) -> Result<VarInt, ConnectionError> {
1188 let (req_id, msg) = self.endpoint.subscribe_namespace(track_namespace, parameters)?;
1189 self.send_control(&msg).await?;
1190 Ok(req_id)
1191 }
1192
1193 /// Accept a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_OK.
1194 ///
1195 /// The endpoint refuses a second answer to one SUBSCRIBE_NAMESPACE, so nothing is
1196 /// written on the wire when it does.
1197 pub async fn subscribe_namespace_ok(
1198 &mut self,
1199 request_id: VarInt,
1200 ) -> Result<(), ConnectionError> {
1201 let msg = self.endpoint.send_subscribe_namespace_ok(request_id)?;
1202 self.send_control(&msg).await
1203 }
1204
1205 /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_ERROR.
1206 ///
1207 /// The other half of the same sentence: one answer, and this is the other
1208 /// one it can be.
1209 pub async fn subscribe_namespace_error(
1210 &mut self,
1211 request_id: VarInt,
1212 error_code: VarInt,
1213 reason_phrase: Vec<u8>,
1214 ) -> Result<(), ConnectionError> {
1215 let msg =
1216 self.endpoint.send_subscribe_namespace_error(request_id, error_code, reason_phrase)?;
1217 self.send_control(&msg).await
1218 }
1219
1220 /// Send a PUBLISH_NAMESPACE and return the request ID.
1221 pub async fn publish_namespace(
1222 &mut self,
1223 track_namespace: TrackNamespace,
1224 parameters: Vec<KeyValuePair>,
1225 ) -> Result<VarInt, ConnectionError> {
1226 let (req_id, msg) = self.endpoint.publish_namespace(track_namespace, parameters)?;
1227 self.send_control(&msg).await?;
1228 Ok(req_id)
1229 }
1230
1231 /// Accept an announcement the peer made, sending PUBLISH_NAMESPACE_OK.
1232 ///
1233 /// The endpoint refuses a second answer to one PUBLISH_NAMESPACE, so
1234 /// nothing is written on the wire when it does.
1235 pub async fn publish_namespace_ok(
1236 &mut self,
1237 request_id: VarInt,
1238 ) -> Result<(), ConnectionError> {
1239 let msg = self.endpoint.send_publish_namespace_ok(request_id)?;
1240 self.send_control(&msg).await
1241 }
1242
1243 /// Refuse an announcement the peer made, sending PUBLISH_NAMESPACE_ERROR.
1244 ///
1245 /// The other half of the same sentence: one answer, and this is the other
1246 /// one it can be.
1247 pub async fn publish_namespace_error(
1248 &mut self,
1249 request_id: VarInt,
1250 error_code: VarInt,
1251 reason_phrase: Vec<u8>,
1252 ) -> Result<(), ConnectionError> {
1253 let msg =
1254 self.endpoint.send_publish_namespace_error(request_id, error_code, reason_phrase)?;
1255 self.send_control(&msg).await
1256 }
1257
1258 /// Revoke an acceptance, sending PUBLISH_NAMESPACE_CANCEL.
1259 ///
1260 /// The endpoint refuses one for an announcement it never accepted, so
1261 /// nothing is written on the wire when it does.
1262 pub async fn publish_namespace_cancel(
1263 &mut self,
1264 track_namespace: TrackNamespace,
1265 error_code: VarInt,
1266 reason_phrase: Vec<u8>,
1267 ) -> Result<(), ConnectionError> {
1268 let msg =
1269 self.endpoint.publish_namespace_cancel(track_namespace, error_code, reason_phrase)?;
1270 self.send_control(&msg).await
1271 }
1272
1273 /// Withdraw an announcement this endpoint made, sending
1274 /// PUBLISH_NAMESPACE_DONE.
1275 ///
1276 /// The mirror of [`Self::publish_namespace`], and the counterpart of
1277 /// [`Self::publish_namespace_cancel`]: this one ends an announcement of
1278 /// this endpoint's, that one revokes the acceptance of one the peer made.
1279 pub async fn publish_namespace_done(
1280 &mut self,
1281 track_namespace: TrackNamespace,
1282 ) -> Result<(), ConnectionError> {
1283 let msg = self.endpoint.publish_namespace_done(track_namespace)?;
1284 self.send_control(&msg).await
1285 }
1286 // ── Track Status flow ────────────────────────────────────
1287
1288 /// Send a TRACK_STATUS and return the allocated request ID.
1289 #[allow(clippy::too_many_arguments)]
1290 pub async fn track_status(
1291 &mut self,
1292 track_namespace: TrackNamespace,
1293 track_name: Vec<u8>,
1294 subscriber_priority: u8,
1295 group_order: GroupOrder,
1296 forward: Forward,
1297 filter_type: FilterType,
1298 parameters: Vec<KeyValuePair>,
1299 ) -> Result<VarInt, ConnectionError> {
1300 let (req_id, msg) = self.endpoint.track_status(
1301 track_namespace,
1302 track_name,
1303 subscriber_priority,
1304 group_order,
1305 forward,
1306 filter_type,
1307 parameters,
1308 )?;
1309 self.send_control(&msg).await?;
1310 Ok(req_id)
1311 }
1312
1313 /// Accept a track status the peer asked for, sending TRACK_STATUS_OK.
1314 ///
1315 /// The endpoint refuses a second answer to one request, so nothing is
1316 /// written on the wire when it does. It also chooses the Track Alias the
1317 /// message carries, because the draft leaves only one value open.
1318 pub async fn track_status_ok(
1319 &mut self,
1320 request_id: VarInt,
1321 expires: VarInt,
1322 group_order: GroupOrder,
1323 parameters: Vec<KeyValuePair>,
1324 ) -> Result<(), ConnectionError> {
1325 let msg =
1326 self.endpoint.send_track_status_ok(request_id, expires, group_order, parameters)?;
1327 self.send_control(&msg).await
1328 }
1329
1330 /// Refuse a track status the peer asked for, sending TRACK_STATUS_ERROR.
1331 ///
1332 /// The other answer the request can have, and the endpoint holds it to the
1333 /// same count of one.
1334 pub async fn track_status_error(
1335 &mut self,
1336 request_id: VarInt,
1337 error_code: VarInt,
1338 reason_phrase: Vec<u8>,
1339 ) -> Result<(), ConnectionError> {
1340 let msg = self.endpoint.send_track_status_error(request_id, error_code, reason_phrase)?;
1341 self.send_control(&msg).await
1342 }
1343
1344 // ── Publish flow (publisher side) ───────────────────────
1345
1346 /// Offer the peer a subscription to a track this endpoint publishes, and
1347 /// return the Request ID the offer was allocated.
1348 ///
1349 /// Nothing is written on the wire when the endpoint refuses to build the
1350 /// offer, which it does when the Track Alias is one another live track of
1351 /// this session already holds.
1352 #[allow(clippy::too_many_arguments)]
1353 pub async fn publish(
1354 &mut self,
1355 track_namespace: TrackNamespace,
1356 track_name: Vec<u8>,
1357 track_alias: VarInt,
1358 group_order: GroupOrder,
1359 largest_location: Option<Location>,
1360 forward: Forward,
1361 parameters: Vec<KeyValuePair>,
1362 ) -> Result<VarInt, ConnectionError> {
1363 let (req_id, msg) = self.endpoint.publish(
1364 track_namespace,
1365 track_name,
1366 track_alias,
1367 group_order,
1368 largest_location,
1369 forward,
1370 parameters,
1371 )?;
1372 self.send_control(&msg).await?;
1373 Ok(req_id)
1374 }
1375
1376 /// Send a PUBLISH_DONE for the given request ID.
1377 pub async fn publish_done(
1378 &mut self,
1379 request_id: VarInt,
1380 status_code: VarInt,
1381 reason_phrase: Vec<u8>,
1382 ) -> Result<(), ConnectionError> {
1383 let msg = self.endpoint.send_publish_done(request_id, status_code, reason_phrase)?;
1384 self.send_control(&msg).await
1385 }
1386
1387 // ── Malformed Tracks ────────────────────────────────────
1388
1389 /// Send what Section 2.5 asks for when this endpoint finds a track
1390 /// malformed.
1391 ///
1392 /// "it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for
1393 /// that Track from that publisher" — one message per request, in Request
1394 /// ID order, and the endpoint decides which message each request takes.
1395 ///
1396 /// A write that fails is not reported. The caller is on its way to
1397 /// returning an error that says what went wrong with the track, and a
1398 /// control stream that will not take an UNSUBSCRIBE is a session on its
1399 /// way out for a reason of its own; replacing the condition's report with
1400 /// a transport error would lose the only account of why the track was
1401 /// withdrawn. The rest of the withdrawal is abandoned, because a stream
1402 /// that refused one message will refuse the next.
1403 async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
1404 for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
1405 if self.send_control(&msg).await.is_err() {
1406 break;
1407 }
1408 }
1409 }
1410
1411 /// Record the framing an arriving object was sent with, and withdraw from
1412 /// the track when it is the second framing that track has been sent.
1413 ///
1414 /// The receiving half of a pair. The two writing paths call the endpoint
1415 /// directly and answer a mixed track by refusing to write it, because
1416 /// Section 2.5's sentence is a subscriber's: an endpoint about to send an
1417 /// object is that object's Original Publisher, and a publisher has no
1418 /// subscription of its own to withdraw and no fetch of its own to cancel.
1419 async fn note_received_framing(
1420 &self,
1421 alias: u64,
1422 seen: ObjectForwardingPreference,
1423 ) -> Result<(), ConnectionError> {
1424 let Err(err) = self.endpoint.note_object_forwarding_preference(alias, seen) else {
1425 return Ok(());
1426 };
1427 self.withdraw_malformed_track(alias, MalformedTrackCondition::MixedForwardingPreference)
1428 .await;
1429 Err(err.into())
1430 }
1431
1432 // ── Data streams ────────────────────────────────────────
1433
1434 /// Open a new unidirectional stream for sending subgroup data.
1435 pub async fn open_subgroup_stream(
1436 &self,
1437 header: &AnySubgroupHeader,
1438 ) -> Result<FramedSendStream, ConnectionError> {
1439 // Before the stream is opened: the Original Publisher is who the rule
1440 // binds, so a header that would mix this track's framing is refused
1441 // here rather than written and answered by the peer.
1442 self.endpoint.note_object_forwarding_preference(
1443 header.track_alias(),
1444 ObjectForwardingPreference::Subgroup,
1445 )?;
1446 let send = self.transport.open_uni().await?;
1447 let mut framed = FramedSendStream::new(send, self.draft);
1448 let sid = framed.stream_id();
1449 framed.write_subgroup_header(header).await?;
1450 self.emit(ClientEvent::StreamOpened {
1451 direction: Direction::Send,
1452 stream_kind: StreamKind::Subgroup,
1453 stream_id: sid,
1454 });
1455 self.emit(ClientEvent::DataStreamHeader {
1456 stream_id: sid,
1457 direction: Direction::Send,
1458 header: header.clone(),
1459 });
1460 Ok(framed)
1461 }
1462
1463 /// Open a new unidirectional stream for sending a FETCH's objects.
1464 ///
1465 /// The objects answering a FETCH do not go on the request's own stream:
1466 /// they go on a unidirectional stream of their own, which opens with a
1467 /// FETCH_HEADER naming the request they belong to. This writes that header
1468 /// and hands back the stream, the same way
1469 /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1470 /// subgroup.
1471 ///
1472 /// The caller owns the stream that comes back. Nothing here remembers
1473 /// which request it belongs to, so an endpoint serving several fetches at
1474 /// once keeps its own map from Request ID to stream.
1475 pub async fn open_fetch_stream(
1476 &self,
1477 header: &AnyFetchHeader,
1478 ) -> Result<FramedSendStream, ConnectionError> {
1479 let send = self.transport.open_uni().await?;
1480 let mut framed = FramedSendStream::new(send, self.draft);
1481 let sid = framed.stream_id();
1482 framed.write_fetch_header(header).await?;
1483 self.emit(ClientEvent::StreamOpened {
1484 direction: Direction::Send,
1485 stream_kind: StreamKind::Fetch,
1486 stream_id: sid,
1487 });
1488 Ok(framed)
1489 }
1490
1491 /// Accept the next unidirectional stream and read its fetch header.
1492 ///
1493 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1494 /// are separate because the header decides how every object after it is
1495 /// framed, so a caller has to know which it is expecting before the first
1496 /// byte is read.
1497 ///
1498 /// Objects come off the returned stream with
1499 /// [`FramedRecvStream::read_fetch_object`].
1500 pub async fn accept_fetch_stream(
1501 &self,
1502 ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1503 let recv = self.transport.accept_uni().await?;
1504 let mut framed = FramedRecvStream::new(recv, self.draft);
1505 let sid = framed.stream_id();
1506 let header = framed.read_fetch_header().await?;
1507 self.emit(ClientEvent::StreamOpened {
1508 direction: Direction::Receive,
1509 stream_kind: StreamKind::Fetch,
1510 stream_id: sid,
1511 });
1512 self.emit(ClientEvent::FetchStreamHeader {
1513 stream_id: sid,
1514 direction: Direction::Receive,
1515 header: header.clone(),
1516 });
1517 // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1518 // carries an `AnySubgroupHeader` and cannot express one. What
1519 // `accept_subgroup_stream` does beyond this - the forwarding-preference
1520 // note, the object measurement - is about a subgroup and has no
1521 // counterpart on a fetch stream.
1522 Ok((header, framed))
1523 }
1524
1525 /// Accept an incoming unidirectional data stream and read its subgroup
1526 /// header.
1527 pub async fn accept_subgroup_stream(
1528 &self,
1529 ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1530 let recv = self.transport.accept_uni().await?;
1531 let mut framed = FramedRecvStream::new(recv, self.draft);
1532 let sid = framed.stream_id();
1533 let header = framed.read_subgroup_header().await?;
1534 self.emit(ClientEvent::StreamOpened {
1535 direction: Direction::Receive,
1536 stream_kind: StreamKind::Subgroup,
1537 stream_id: sid,
1538 });
1539 self.emit(ClientEvent::DataStreamHeader {
1540 stream_id: sid,
1541 direction: Direction::Receive,
1542 header: header.clone(),
1543 });
1544 // Every object on a subgroup stream has the Subgroup preference, so
1545 // the header settles the track's framing before a single object is
1546 // read.
1547 self.note_received_framing(header.track_alias(), ObjectForwardingPreference::Subgroup)
1548 .await?;
1549 // The track is resolved here and not inside the stream: it takes the
1550 // endpoint's alias table, which a stream handle has no way back to.
1551 if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1552 framed.measure_objects_against(objects, header.group_id());
1553 }
1554 Ok((header, framed))
1555 }
1556
1557 /// Send an object via datagram.
1558 ///
1559 /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1560 /// header whose Object Status the framing it names cannot carry. Such a
1561 /// header errors here and nothing is sent, rather than going out as an
1562 /// ordinary payload datagram with the status quietly dropped.
1563 pub fn send_datagram(
1564 &self,
1565 header: &AnyDatagramHeader,
1566 payload: &[u8],
1567 ) -> Result<(), ConnectionError> {
1568 // Before anything is encoded, for the reason `open_subgroup_stream`
1569 // gives.
1570 self.endpoint.note_object_forwarding_preference(
1571 header.meta().track_alias,
1572 ObjectForwardingPreference::Datagram,
1573 )?;
1574 let mut buf = Vec::new();
1575 header.encode(&mut buf)?;
1576 buf.extend_from_slice(payload);
1577 self.emit(ClientEvent::DatagramReceived {
1578 direction: Direction::Send,
1579 header: header.clone(),
1580 payload_len: payload.len(),
1581 });
1582 self.transport.send_datagram(bytes::Bytes::from(buf))?;
1583 Ok(())
1584 }
1585
1586 /// Receive a datagram and decode its header.
1587 pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1588 let data = self.transport.recv_datagram().await?;
1589 let mut cursor = &data[..];
1590 let header = AnyDatagramHeader::decode(self.draft, &mut cursor)?;
1591 let consumed = data.len() - cursor.len();
1592 let payload = data.slice(consumed..);
1593 self.emit(ClientEvent::DatagramReceived {
1594 direction: Direction::Receive,
1595 header: header.clone(),
1596 payload_len: payload.len(),
1597 });
1598 // A datagram is the other framing, and it settles the track's just as a
1599 // subgroup header does.
1600 self.note_received_framing(header.meta().track_alias, ObjectForwardingPreference::Datagram)
1601 .await?;
1602 // A datagram is a whole object, so the connection can measure it
1603 // without help from the caller - and answer the condition itself,
1604 // because an UNSUBSCRIBE takes the connection an object on a stream
1605 // cannot reach.
1606 let meta = header.meta();
1607 if let Err(err) = self.endpoint.note_received_object(
1608 meta.track_alias,
1609 ObjectLocation { group: meta.group_id, object: meta.object_id },
1610 object_role(meta.status),
1611 ) {
1612 self.withdraw_malformed_track(
1613 meta.track_alias,
1614 MalformedTrackCondition::ObjectPastFinalObject,
1615 )
1616 .await;
1617 return Err(err.into());
1618 }
1619 Ok((header, payload))
1620 }
1621
1622 // ── Accessors ───────────────────────────────────────────
1623
1624 /// Access the underlying endpoint state machine.
1625 pub fn endpoint(&self) -> &Endpoint {
1626 &self.endpoint
1627 }
1628
1629 /// Mutable access to the endpoint state machine.
1630 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1631 &mut self.endpoint
1632 }
1633
1634 /// The SETUP message the server answered the handshake with.
1635 ///
1636 /// `SERVER_SETUP` through draft-16, the server's half of the unified
1637 /// `SETUP` from draft-17. [`AnyControlMessage::fields`] renders it under
1638 /// this draft's own parameter names, in the order they arrived.
1639 pub fn server_setup(&self) -> &AnyControlMessage {
1640 &self.server_setup
1641 }
1642
1643 /// The framed wire bytes of [`Self::server_setup`], as they arrived.
1644 ///
1645 /// Kept beside the decoded form because the encoding is evidence the
1646 /// decoding discards: two relays sending the same parameter can still
1647 /// disagree on how wide a varint they wrote it in.
1648 pub fn server_setup_raw(&self) -> Option<&[u8]> {
1649 self.server_setup_raw.as_deref()
1650 }
1651
1652 /// Get the negotiated MoQT version.
1653 pub fn negotiated_version(&self) -> Option<VarInt> {
1654 self.endpoint.negotiated_version()
1655 }
1656
1657 /// Returns the draft version this connection is using.
1658 pub fn draft(&self) -> DraftVersion {
1659 self.draft
1660 }
1661
1662 /// Which of this draft's *own* `ConnectionError` variants this error is,
1663 /// and which kind of thing it says.
1664 ///
1665 /// Draft-14 adds none. Every variant of its [`ConnectionError`] is one of the
1666 /// ten every draft carries, and [`AnyConnectionError`] classifies those
1667 /// itself — so `None` here is this draft's answer rather than a stub, and it
1668 /// stays right for exactly as long as that list does.
1669 ///
1670 /// Matched exhaustively, with no wildcard arm and deliberately so: a
1671 /// variant added to this draft's error type has to arrive here as a compile
1672 /// error, beside the doc comment that quotes the sentence it enforces,
1673 /// rather than as a silent [`ErrorCause::Unclassified`] in the facade.
1674 ///
1675 /// [`AnyConnectionError`]: crate::dispatch::AnyConnectionError
1676 /// [`ErrorCause::Unclassified`]: crate::dispatch::ErrorCause::Unclassified
1677 pub fn draft_specific_cause(
1678 err: &ConnectionError,
1679 ) -> Option<crate::above_codec_rules::DraftSpecificCause> {
1680 match err {
1681 ConnectionError::Endpoint(_)
1682 | ConnectionError::Codec(_)
1683 | ConnectionError::Transport(_)
1684 | ConnectionError::VarInt(_)
1685 | ConnectionError::NoControlStream
1686 | ConnectionError::UnexpectedEnd
1687 | ConnectionError::StreamFinished
1688 | ConnectionError::InvalidAddress(_)
1689 | ConnectionError::TlsConfig(_)
1690 | ConnectionError::DataStreamState(_) => None,
1691 // This build decoding a message and then failing to narrow it to
1692 // its own draft. Nothing reached the wire and no peer is
1693 // implicated, which is the whole reason it is not
1694 // `ConnectionError::Codec`: under that name it would carry
1695 // `Some(PROTOCOL_VIOLATION)` out of `codec_session_error_code` and
1696 // publish a relay for this build's defect. See the variant's own
1697 // doc.
1698 ConnectionError::ControlMessageNarrowing => {
1699 Some(crate::above_codec_rules::DraftSpecificCause::LocalRefusal)
1700 }
1701 }
1702 }
1703
1704 /// The code to close the session with when a message could not be decoded
1705 /// because the peer broke a rule draft-14 answers with a close.
1706 ///
1707 /// Every variant listed here comes from a sentence in this draft that names
1708 /// the consequence, and the list is per draft: answering a bound this draft
1709 /// does not state would close a session over traffic a conforming peer may
1710 /// send.
1711 ///
1712 /// - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
1713 /// exceeding the maximum, it MUST close the session with a
1714 /// PROTOCOL_VIOLATION."
1715 /// - GOAWAY New Session URI, maximum 8,192 bytes, with the same sentence.
1716 /// Drafts 11 through 19 state it; 07 through 10 state no maximum for
1717 /// the field at all.
1718 /// - Key-Value-Pair value, maximum 2^16-1 bytes, with the same sentence.
1719 /// - Track Namespace tuple size: "If an endpoint receives a Track
1720 /// Namespace tuple with an N of 0 or more than 32, it MUST close the
1721 /// session with a Protocol Violation." Note the lower bound - an empty
1722 /// tuple is refused here, where drafts 17 and later permit one.
1723 /// - Full Track Name, maximum 4,096 bytes, "computed as the sum of the
1724 /// lengths of each Track Namespace tuple field and the Track Name
1725 /// length field". This draft bounds the pair and not the namespace
1726 /// alone; draft-16 widened it.
1727 /// - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1728 /// check that there are no unauthorized duplicate parameters and close
1729 /// the session as a PROTOCOL_VIOLATION if found." The rule is
1730 /// asymmetric here - "Receivers MUST allow duplicates of unknown
1731 /// parameters", and one known type is granted repeats - and the codec
1732 /// reports only the repeats this draft actually forbids.
1733 /// - Unknown control message type: "An endpoint that receives an unknown
1734 /// message type MUST close the session."
1735 /// - Extension headers on an Object whose status is Object Does Not
1736 /// Exist, Section 10.2.1.2. That one arrives on a data stream or a
1737 /// datagram, so [`Connection::close_for_data_stream`] is what carries
1738 /// it.
1739 ///
1740 /// **Not** the zero-length Track Namespace Field, the delta-encoded
1741 /// parameter type overflow, or the Object ID delta wrap. Those enter the
1742 /// specification at drafts 16 and 18, and this draft states none of them.
1743 ///
1744 /// **Not** the 2^16-1 control message length either. This draft states the
1745 /// limit - "the total length of a control message is limited to 2^16-1
1746 /// bytes" - and states no consequence for exceeding it, so an oversized
1747 /// message is refused by the decoder and stops there.
1748 ///
1749 /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
1750 /// reader raises it whenever a message is still arriving, and
1751 /// `read_control` loops on it. Closing over it would end a session on an
1752 /// ordinary short read.
1753 ///
1754 /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
1755 /// a close for a Message Parameter whose type the negotiated version does
1756 /// not define. This draft states the opposite and states it about the same
1757 /// parameters: "Receivers MUST allow duplicates of unknown parameters",
1758 /// which presumes an unknown parameter arrives and is carried. Refusing one
1759 /// here would close a session over an extension this draft leaves room for.
1760 ///
1761 /// `None` for everything else, including [`CodecError::InvalidField`]. That
1762 /// variant is shared by a dozen unrelated malformations, only some of which
1763 /// the draft answers with a close, so a session cannot be ended on it
1764 /// without ending sessions the draft does not ask to be ended. Splitting it
1765 /// is the way to bring the rest of those rules under this function;
1766 /// widening the match is not - the extension-header rule above is in this
1767 /// table because it has a variant of its own.
1768 pub fn codec_session_error_code(
1769 err: &CodecError,
1770 ) -> Option<moqtap_codec::draft14::error_codes::SessionErrorCode> {
1771 use moqtap_codec::draft14::error_codes::SessionErrorCode;
1772 use moqtap_codec::kvp::KvpError;
1773 match err {
1774 // The declared Length disagreeing with the fields, which every
1775 // draft answers with a close. Drafts 07 through 10 name no code for
1776 // it, so it takes the one their other unnamed rules take.
1777 // A Filter Type outside the four this draft assigns, Section 9.7:
1778 // "An endpoint that receives a filter type other than the above MUST
1779 // be close the session with PROTOCOL_VIOLATION" — the missing word
1780 // is the draft's. This is the draft the rule gained a consequence
1781 // on. Drafts 07 through 13 write the same sentence as "MUST be
1782 // treated as error", which names none - draft-13 Section 8.7 among
1783 // them - and their tables leave it unanswered.
1784 CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
1785 // A Fetch Type outside the three this draft assigns, Section
1786 // 9.16: "An endpoint that receives a Fetch Type other than 0x1,
1787 // 0x2 or 0x3 MUST be close the session with a PROTOCOL_VIOLATION."
1788 // The missing word is the draft's, as it is for the filter type
1789 // above. The value decides which
1790 // fields follow it — a Standalone fetch carries a track name and a
1791 // range where a joining fetch carries a Request ID and an offset —
1792 // so a reader that cannot name the type cannot find the end of the
1793 // message.
1794 CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
1795 CodecError::ControlMessageLengthMismatch { .. } => {
1796 Some(SessionErrorCode::ProtocolViolation)
1797 }
1798 CodecError::ReasonPhraseTooLong
1799 | CodecError::GoAwayUriTooLong
1800 | CodecError::InvalidNamespaceTupleSize(_)
1801 | CodecError::TrackNameTooLong
1802 | CodecError::DuplicateParameter(_)
1803 | CodecError::UnknownMessageType(_)
1804 | CodecError::ExtensionsOnNonExistentObject(_)
1805 | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
1806 Some(SessionErrorCode::ProtocolViolation)
1807 }
1808 // An unknown data-plane type, Section 10: "An endpoint that
1809 // receives an unknown stream or datagram type MUST close the
1810 // session." One sentence covering two tables, which is why both
1811 // variants sit here.
1812 // A Content Exists field that is neither zero nor one, Sections 9.8
1813 // and 9.13: "Any other value is a protocol error and MUST terminate
1814 // the session with a PROTOCOL_VIOLATION".
1815 CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
1816 // A Forward field that is neither zero nor one. Sections 9.7 and
1817 // 9.10 use the sentence above; Section 9.13 says "Any value other
1818 // than 0 or 1 is a PROTOCOL_VIOLATION". Section 9.14 names the two
1819 // legal values without a consequence and is answered the same way,
1820 // for the reason given on the variant. Draft-15 replaces the field
1821 // with the FORWARD parameter, which carries the same rule in a
1822 // different shape and reports it as `ParameterValueOutOfRange`.
1823 CodecError::InvalidForward(_) => Some(SessionErrorCode::ProtocolViolation),
1824 CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
1825 Some(SessionErrorCode::ProtocolViolation)
1826 }
1827 // A key-value pair whose value is not the serialization its own
1828 // Type defines, Section 1.4.2: "If a receiver understands a Type,
1829 // and the following Value or Length/Value does not match the
1830 // serialization defined by that Type, the receiver MUST terminate the
1831 // session with error code KEY_VALUE_FORMATTING_ERROR."
1832 //
1833 // Section 9.2.1.1 states the same answer for the one structure this
1834 // draft spells out: "If the Token structure cannot be decoded, the
1835 // receiver MUST close the Session with Key-Value Formatting error."
1836 //
1837 // The one rule in this table that names a code other than Protocol
1838 // Violation.
1839 CodecError::KeyValueFormatting { .. } => {
1840 Some(SessionErrorCode::KeyValueFormattingError)
1841 }
1842 // Everything this draft does not answer, named rather than swept up
1843 // by a wildcard. The arm is exhaustive deliberately: a new
1844 // `CodecError` variant will not compile until it has been placed on
1845 // one side or the other, on this draft, which is the decision a `_`
1846 // arm makes silently and invisibly in every draft module at once.
1847 //
1848 // Adding one variant to `CodecError` produces an `E0004` in every
1849 // draft module that matches it exhaustively, each naming the
1850 // variant that has nowhere to go. That is the whole mechanism.
1851 //
1852 // The nesting stops at `VarInt`, whose variants report how the bytes
1853 // ran out rather than a rule an endpoint states, so there is nothing
1854 // in it for a draft to answer. `Kvp` is spelled out because it does
1855 // carry one.
1856 // Not `ParameterValueOutOfRange`: no parameter this draft defines
1857 // restricts its value's range. Forwarding is a message field here
1858 // and is answered above.
1859 CodecError::ParameterValueOutOfRange { .. }
1860 | CodecError::UnexpectedEnd
1861 | CodecError::MessageTooLong(_)
1862 | CodecError::VarInt(_)
1863 | CodecError::InvalidField
1864 | CodecError::EmptyNamespaceField
1865 | CodecError::InvalidRange(..)
1866 | CodecError::ParameterLengthMismatch(_)
1867 | CodecError::EndOfTrackObjectId(_)
1868 | CodecError::KeyDeltaOverflow(..)
1869 // Not `TrackPropertyValueOutOfRange`: this draft has neither
1870 // namespace the variant is about. Draft-16 opens an extension header
1871 // registry with value rules of its own, and draft-17 renames it to
1872 // the Track Property registry. Before that, everything with a
1873 // restricted range is either a message field or a Message Parameter.
1874 | CodecError::TrackPropertyValueOutOfRange { .. }
1875 | CodecError::ParametersOutOfOrder(..)
1876 | CodecError::ObjectIdOverflow(..)
1877 | CodecError::InvalidRequiredRequestIdDelta(..)
1878 | CodecError::InvalidStreamTypeValue { .. }
1879 | CodecError::InvalidDatagramTypeValue { .. }
1880 | CodecError::UnknownMessageParameter(_)
1881 // Not `ParameterOutOfScope`: this draft states the scope rule and
1882 // answers it the other way. Section 9.2.1 Version Specific Parameters: "Each
1883 // version-specific parameter definition indicates the message types in which it can
1884 // appear. If it appears in some other type of message, it MUST be
1885 // ignored." The codec carries such a parameter on this draft and never
1886 // raises the variant, so this arm records a rule this draft has and
1887 // does not close over, not one it is missing. Draft-17 is where the
1888 // second sentence becomes a close.
1889 | CodecError::ParameterOutOfScope { .. }
1890 // Both belong to the parameter form of the filter. Draft-15 moved
1891 // the Filter Type, Start Location and End Group out of SUBSCRIBE and
1892 // into one length-prefixed parameter; this draft still carries them
1893 // as fields, where a value that runs short is a truncated frame and
1894 // an End Group is written out rather than added to anything.
1895 | CodecError::SubscriptionFilterMalformed { .. }
1896 | CodecError::FilterEndGroupOverflow { .. }
1897 // The object payload rule, Section 10.2.1.1: "Any object with a status
1898 // code other than zero MUST have an empty payload." A MUST on the
1899 // sender with no receiver action named anywhere — the "SHOULD be
1900 // treated as a protocol error" in the same paragraph belongs to the
1901 // sentence before it, which is about a status value this draft does
1902 // not assign — so an object carrying a payload it may not is refused
1903 // and the session stays open.
1904 | CodecError::PayloadNotPermitted { .. }
1905 | CodecError::UnsupportedDraft(_)
1906 | CodecError::Kvp(
1907 KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
1908 ) => None,
1909 }
1910 }
1911
1912 /// Close the session on the wire when a decode failure is one draft-14
1913 /// answers with a close, and hand the error back unchanged.
1914 /// Without it every bound the decoder enforces would stop at *this endpoint
1915 /// refused the frame* while the peer, which is the one that broke the rule,
1916 /// saw a session that was still open and went on sending. "MUST close the
1917 /// session" is a statement about the wire.
1918 fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
1919 if let ConnectionError::Codec(inner) = &err {
1920 if let Some(code) = Self::codec_session_error_code(inner) {
1921 // QUIC application error codes are 62-bit; every code in this
1922 // registry is far below `u32::MAX`, and saturating rather than
1923 // truncating means a future code that is not could never be
1924 // reported as a different, assigned one.
1925 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1926 self.close(wire_code, inner.to_string().as_bytes());
1927 }
1928 }
1929 err
1930 }
1931
1932 /// Close the session on the wire when the endpoint says a violation is
1933 /// fatal to it, and hand the error back unchanged.
1934 ///
1935 /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1936 /// errors this draft ends the session over, and the endpoint has already
1937 /// moved its own state machine to Closed by the time this runs. Without
1938 /// this step that move is purely internal: the local endpoint refuses to
1939 /// start anything new while the peer, which is the one that broke the
1940 /// rule, sees a session that is still open and goes on sending. A rule
1941 /// that names a session termination code is a statement about the wire,
1942 /// so it takes a CONNECTION_CLOSE to satisfy it.
1943 ///
1944 /// The reason phrase is the error's own `Display` text, which names the
1945 /// rule rather than repeating the numeric code the close already carries.
1946 ///
1947 /// Errors that answer `None` are recoverable and nothing is sent.
1948 fn close_for(&self, err: &EndpointError) {
1949 if let Some(code) = err.session_error_code() {
1950 // QUIC application error codes are 62-bit; every code in this
1951 // registry is far below `u32::MAX`, and saturating rather than
1952 // truncating means a future code that is not could never be
1953 // reported as a different, assigned one.
1954 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1955 self.close(wire_code, err.to_string().as_bytes());
1956 }
1957 }
1958
1959 /// [`close_for`](Self::close_for), then the error unchanged, for the
1960 /// common case where the endpoint's error is also what the caller returns.
1961 fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1962 self.close_for(&err);
1963 ConnectionError::Endpoint(err)
1964 }
1965
1966 /// Withdraw from a track a data stream found malformed, reporting whether
1967 /// it did.
1968 ///
1969 /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
1970 /// separate from it for the same reason and one more. The same one: a
1971 /// [`FramedRecvStream`] holds no connection, so the reader that finds the
1972 /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
1973 /// two answers are opposites - that call ends the session, this one gives
1974 /// up a track and leaves it running - and a single entry point would have
1975 /// to decide between them from the error alone, which is exactly the
1976 /// decision a caller reproducing a capture wants to make itself.
1977 ///
1978 /// The datagram path needs none of this. It is read through the connection,
1979 /// so [`Connection::recv_datagram`] answers the condition where it finds
1980 /// it, and this is only for the objects that arrive on a stream the caller
1981 /// holds.
1982 pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
1983 let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
1984 else {
1985 return false;
1986 };
1987 self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
1988 true
1989 }
1990
1991 /// Close the session over a rule broken on a data stream, reporting whether
1992 /// it did.
1993 ///
1994 /// A data stream cannot close for itself the way `recv_control` does:
1995 /// [`Connection::accept_subgroup_stream`] hands the caller a
1996 /// [`FramedRecvStream`] holding no connection, so the reader that finds the
1997 /// violation is not the object that can act on it. Keeping it a separate
1998 /// call is deliberate as well - a permissive caller, one reproducing a
1999 /// capture, can read a violating stream and report it without tearing the
2000 /// session down.
2001 ///
2002 /// The rule this draft answers here is extension headers on an Object whose
2003 /// status is Object Does Not Exist, which reaches subgroup streams, fetch
2004 /// streams and status datagrams alike. It shares `codec_session_error_code`
2005 /// with the control path, so a rule is answered with one code whichever
2006 /// stream carried it.
2007 ///
2008 /// Not every rule that reaches here is the decoder's. A track whose objects
2009 /// mix forwarding preferences is the endpoint's to notice — it takes the
2010 /// alias table to know which track an object belongs to — and it arrives on
2011 /// exactly these streams. Both kinds are asked for a code the same way, and
2012 /// a rule with no code is declined rather than guessed at.
2013 pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
2014 match err {
2015 ConnectionError::Codec(inner) => {
2016 let Some(code) = Self::codec_session_error_code(inner) else { return false };
2017 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2018 self.close(wire_code, inner.to_string().as_bytes());
2019 true
2020 }
2021 // A rule the endpoint raises rather than the decoder. The two
2022 // reach their codes through different tables and mean the same
2023 // thing here: `Some` is a rule this draft ends the session over.
2024 ConnectionError::Endpoint(inner) => {
2025 let Some(code) = inner.session_error_code() else { return false };
2026 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2027 self.close(wire_code, inner.to_string().as_bytes());
2028 true
2029 }
2030 _ => false,
2031 }
2032 }
2033
2034 /// Close the connection.
2035 pub fn close(&self, code: u32, reason: &[u8]) {
2036 self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
2037 self.transport.close(code, reason);
2038 }
2039}
2040
2041/// Determine the encoded length of a varint from its first byte.
2042fn varint_len(first_byte: u8) -> usize {
2043 1 << (first_byte >> 6)
2044}
2045
2046#[cfg(test)]
2047mod tests {
2048 use super::*;
2049
2050 /// This build failing to narrow a message it decoded is never a finding
2051 /// about the peer.
2052 ///
2053 /// The arm that raises `ControlMessageNarrowing` is unreachable — this
2054 /// draft's decoder can only hand back this draft's variant — and nothing
2055 /// pins that. What is pinned here is the half that matters.
2056 /// `CodecError::UnknownMessageType(0)` is what the arm must not raise:
2057 /// `codec_session_error_code` answers it `Some(PROTOCOL_VIOLATION)` on
2058 /// every draft in range, so the day the narrowing failed a conformance
2059 /// probe would publish a relay for sending a control message type this
2060 /// draft does not assign — with `0x00` attached as the codepoint that
2061 /// proved it, which is an accusation better evidenced than any real one
2062 /// this build makes. The section stating that rule is renumbered several
2063 /// times across the series, and the point does not turn on the
2064 /// number.
2065 ///
2066 /// Ablated by putting the arm back to
2067 /// `ConnectionError::Codec(CodecError::UnknownMessageType(0))`: this test
2068 /// reddens on the cause, and so does the probe's own
2069 /// `violation::a_message_this_build_could_not_narrow_names_nobody`.
2070 #[test]
2071 fn a_message_this_build_could_not_narrow_names_nobody() {
2072 use crate::dispatch::{AnyConnectionError, ErrorCause};
2073
2074 let err: AnyConnectionError = ConnectionError::ControlMessageNarrowing.into();
2075 assert!(err.is_local(), "a narrowing this build could not do is this build's");
2076 assert_eq!(
2077 err.cause(),
2078 &ErrorCause::Facade,
2079 "nothing reached the wire, so there is no rule and no close code to read"
2080 );
2081 }
2082
2083 #[test]
2084 fn varint_len_single_byte() {
2085 // 0b00xxxxxx -> 1 byte
2086 assert_eq!(varint_len(0x00), 1);
2087 assert_eq!(varint_len(0x3F), 1);
2088 }
2089
2090 #[test]
2091 fn varint_len_two_bytes() {
2092 // 0b01xxxxxx -> 2 bytes
2093 assert_eq!(varint_len(0x40), 2);
2094 assert_eq!(varint_len(0x7F), 2);
2095 }
2096
2097 #[test]
2098 fn varint_len_four_bytes() {
2099 // 0b10xxxxxx -> 4 bytes
2100 assert_eq!(varint_len(0x80), 4);
2101 assert_eq!(varint_len(0xBF), 4);
2102 }
2103
2104 #[test]
2105 fn varint_len_eight_bytes() {
2106 // 0b11xxxxxx -> 8 bytes
2107 assert_eq!(varint_len(0xC0), 8);
2108 assert_eq!(varint_len(0xFF), 8);
2109 }
2110
2111 #[test]
2112 fn client_config_supported_versions_draft14() {
2113 let config = ClientConfig {
2114 draft: DraftVersion::Draft14,
2115 additional_versions: Vec::new(),
2116 transport: TransportType::Quic,
2117 skip_cert_verification: false,
2118 ca_certs: Vec::new(),
2119 setup_parameters: Vec::new(),
2120 };
2121 let versions = config.supported_versions();
2122 assert_eq!(versions.len(), 1);
2123 assert_eq!(versions[0].into_inner(), 0xff000000 + 14);
2124 }
2125
2126 #[test]
2127 fn client_config_supported_versions_draft07() {
2128 let config = ClientConfig {
2129 draft: DraftVersion::Draft07,
2130 additional_versions: Vec::new(),
2131 transport: TransportType::Quic,
2132 skip_cert_verification: false,
2133 ca_certs: Vec::new(),
2134 setup_parameters: Vec::new(),
2135 };
2136 let versions = config.supported_versions();
2137 assert_eq!(versions.len(), 1);
2138 assert_eq!(versions[0].into_inner(), 0xff000000 + 7);
2139 }
2140
2141 #[test]
2142 fn client_config_alpn_quic() {
2143 let config = ClientConfig {
2144 draft: DraftVersion::Draft14,
2145 additional_versions: Vec::new(),
2146 transport: TransportType::Quic,
2147 skip_cert_verification: false,
2148 ca_certs: Vec::new(),
2149 setup_parameters: Vec::new(),
2150 };
2151 assert_eq!(config.alpn(), vec![b"moq-00".to_vec()]);
2152 }
2153
2154 #[test]
2155 fn client_config_alpn_webtransport() {
2156 let config = ClientConfig {
2157 draft: DraftVersion::Draft14,
2158 additional_versions: Vec::new(),
2159 transport: TransportType::WebTransport { url: "https://example.com".to_string() },
2160 skip_cert_verification: false,
2161 ca_certs: Vec::new(),
2162 setup_parameters: Vec::new(),
2163 };
2164 assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
2165 }
2166
2167 #[test]
2168 fn moqt_alpn_value() {
2169 assert_eq!(MOQT_ALPN, b"moq-00");
2170 }
2171
2172 #[test]
2173 fn transport_type_debug() {
2174 let quic = TransportType::Quic;
2175 assert!(format!("{quic:?}").contains("Quic"));
2176
2177 let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
2178 assert!(format!("{wt:?}").contains("WebTransport"));
2179 }
2180}