moqtap_client/draft09/connection.rs
1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft09::endpoint::{Endpoint, EndpointError, Role};
4use crate::draft09::event::{ClientEvent, Direction, FetchObject, StreamKind, SubgroupObject};
5use crate::draft09::observer::ConnectionObserver;
6use crate::draft09::session::setup;
7use crate::forwarding_preference::ObjectForwardingPreference;
8use crate::track_locations::{EndOfTrackForm, ObjectLocation, ObjectRole, TrackObjects};
9use crate::transport::{RecvStream, SendStream, Transport, TransportError};
10use moqtap_codec::dispatch::{
11 AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
12};
13use moqtap_codec::draft09::data_stream::{FetchObjectHeader, ObjectHeader};
14use moqtap_codec::draft09::message::ControlMessage;
15use moqtap_codec::error::CodecError;
16use moqtap_codec::kvp::KeyValuePair;
17use moqtap_codec::types::*;
18use moqtap_codec::varint::VarInt;
19use moqtap_codec::version::DraftVersion;
20
21/// MoQT ALPN identifier (used by raw QUIC transport).
22pub const MOQT_ALPN: &[u8] = b"moq-00";
23
24/// Errors from the draft-09 connection layer.
25#[derive(Debug, thiserror::Error)]
26pub enum ConnectionError {
27 /// Endpoint state machine error.
28 #[error("endpoint error: {0}")]
29 Endpoint(#[from] EndpointError),
30 /// Wire codec error.
31 #[error("codec error: {0}")]
32 Codec(#[from] CodecError),
33 /// Transport-level error.
34 #[error("transport error: {0}")]
35 Transport(#[from] TransportError),
36 /// Variable-length integer decoding error.
37 #[error("varint error: {0}")]
38 VarInt(#[from] moqtap_codec::varint::VarIntError),
39 /// Control stream was not opened.
40 #[error("control stream not open")]
41 NoControlStream,
42 /// Stream ended before a complete message was read.
43 #[error("unexpected end of stream")]
44 UnexpectedEnd,
45 /// Stream was finished by the peer.
46 #[error("stream finished")]
47 StreamFinished,
48 /// Invalid server address string.
49 #[error("invalid server address: {0}")]
50 InvalidAddress(String),
51 /// TLS configuration error.
52 #[error("TLS config error: {0}")]
53 TlsConfig(String),
54 /// Data stream used out of order: an object before its header, or an
55 /// Object ID that does not advance on the last one written.
56 #[error("data stream state error: {0}")]
57 DataStreamState(&'static str),
58 /// A control message this build decoded for draft-09 and then could not
59 /// narrow to draft-09's own message type.
60 ///
61 /// Unreachable, and that is not the same as harmless. `read_control`
62 /// decodes with this connection's own draft, so the `AnyControlMessage` it
63 /// hands back can only carry this draft's variant — but the narrowing arm
64 /// is compiled in every configuration anyway, under
65 /// `#[allow(unreachable_patterns)]` rather than a `cfg` naming the other
66 /// drafts, because such a list has to be edited in every draft
67 /// module whenever a draft is added, and a copy that omits one leaves the
68 /// match non-exhaustive.
69 ///
70 /// Spelled as `CodecError::UnknownMessageType(0)` it would not stay inert:
71 /// every draft's
72 /// [`codec_session_error_code`](Connection::codec_session_error_code)
73 /// answers that variant `Some(PROTOCOL_VIOLATION)`. So the day the
74 /// narrowing did fail, this build's own defect would reach a caller as *the
75 /// peer sent a control message type this draft does not assign, and the
76 /// session must be closed with a Protocol Violation* — carrying `0x00` as
77 /// the codepoint that proved it. A conformance report reading that
78 /// publishes a named, well-evidenced accusation against a relay for
79 /// something no relay did.
80 ///
81 /// A variant of its own is what stops that.
82 /// [`draft_specific_cause`](Connection::draft_specific_cause) answers it
83 /// [`LocalRefusal`], the facade turns that into [`ErrorCause::Facade`], and
84 /// nothing downstream can read a rule out of a cause that says nothing
85 /// reached the wire. What is pinned is the consequence rather than the
86 /// unreachability: nothing pins the arm's reachability, which is exactly
87 /// why the consequence must not be an accusation.
88 ///
89 /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
90 /// [`ErrorCause::Facade`]: crate::dispatch::ErrorCause::Facade
91 #[error(
92 "a control message decoded for draft-09 did not narrow to draft-09: a defect in this build, and evidence about nothing the peer did"
93 )]
94 ControlMessageNarrowing,
95}
96
97impl From<crate::transport::DialError> for ConnectionError {
98 /// Maps a dial failure onto the variants this error already has, so a
99 /// caller matches `InvalidAddress` or `TlsConfig`.
100 ///
101 /// # `LocalSocket` joins `InvalidAddress`, and that is the answer being kept
102 ///
103 /// A socket this machine would not open has a variant of its own on
104 /// [`DialError`](crate::transport::DialError), and it still arrives here.
105 /// Not laziness about the churn — `InvalidAddress` is one of the
106 /// variants the facade reads as
107 /// [`ErrorCause::Facade`](crate::dispatch::ErrorCause::Facade), which
108 /// `is_local` answers **true** for, and a failed bind is this side's by
109 /// definition. Routing it to `Transport` would read better in prose and
110 /// would publish this machine's missing IPv6 stack as the relay's doing.
111 ///
112 /// The phase is not lost, only unread on this path. A caller measuring
113 /// which stage of a dial died reads
114 /// [`DialError::phase`](crate::transport::DialError::phase) off the dial
115 /// itself; a caller who arrived at this type named a `host:port` and asked
116 /// for a connection, not for a measurement, and a public variant here for
117 /// a distinction nothing on this path reads is churn with no reader, which
118 /// is why this impl stays flat.
119 fn from(e: crate::transport::DialError) -> Self {
120 match e {
121 // Two variants, one arm, deliberately — see above.
122 crate::transport::DialError::InvalidAddress(s)
123 | crate::transport::DialError::LocalSocket(s) => ConnectionError::InvalidAddress(s),
124 crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
125 crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
126 }
127 }
128}
129
130/// Transport type for the connection.
131#[derive(Debug, Clone)]
132pub enum TransportType {
133 /// Raw QUIC via quinn. The `addr` field should be `host:port`.
134 Quic,
135 /// WebTransport via wtransport. The `url` field is the WebTransport URL.
136 WebTransport {
137 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
138 url: String,
139 },
140}
141
142/// Configuration for a draft-09 MoQT client connection.
143pub struct ClientConfig {
144 /// Additional draft versions to offer in CLIENT_SETUP (draft-09 is always
145 /// offered first).
146 pub additional_versions: Vec<DraftVersion>,
147 /// The transport type (QUIC or WebTransport).
148 pub transport: TransportType,
149 /// Whether to skip TLS certificate verification (for testing).
150 pub skip_cert_verification: bool,
151 /// Custom CA certificates to trust (DER-encoded).
152 pub ca_certs: Vec<Vec<u8>>,
153 /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
154 pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
155}
156
157impl ClientConfig {
158 /// Returns the MoQT version varints for the CLIENT_SETUP message.
159 /// Draft-09 first, then any additional versions.
160 pub fn supported_versions(&self) -> Vec<VarInt> {
161 let mut versions = vec![DraftVersion::Draft09.version_varint()];
162 for v in &self.additional_versions {
163 let varint = v.version_varint();
164 if !versions.contains(&varint) {
165 versions.push(varint);
166 }
167 }
168 versions
169 }
170
171 /// Returns the ALPN protocol identifiers for the transport.
172 pub fn alpn(&self) -> Vec<Vec<u8>> {
173 match &self.transport {
174 TransportType::Quic => vec![DraftVersion::Draft09.quic_alpn().to_vec()],
175 TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
176 }
177 }
178}
179
180/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
181pub struct FramedSendStream {
182 inner: SendStream,
183 /// The last Object ID written on this subgroup stream, once one has been.
184 ///
185 /// `None` before the first object; the outer `Option` is `None` until a
186 /// subgroup header has been written, which is what makes an object sent
187 /// before its header answerable rather than unframed bytes.
188 subgroup_objects: Option<Option<u64>>,
189}
190
191impl FramedSendStream {
192 /// Create a new framed send stream.
193 pub fn new(inner: SendStream) -> Self {
194 Self { inner, subgroup_objects: None }
195 }
196
197 /// Get the transport-level stream ID.
198 pub fn stream_id(&self) -> u64 {
199 self.inner.stream_id()
200 }
201
202 /// Write a control message to the stream with type+length framing.
203 /// Returns the raw bytes that were written (for event capture).
204 pub async fn write_control(
205 &mut self,
206 msg: &AnyControlMessage,
207 ) -> Result<Vec<u8>, ConnectionError> {
208 let mut buf = Vec::new();
209 msg.encode(&mut buf)?;
210 self.inner.write_all(&buf).await?;
211 Ok(buf)
212 }
213
214 /// Write a subgroup stream header. Also opens the Object ID bookkeeping
215 /// [`FramedSendStream::write_subgroup_object`] holds the stream to.
216 ///
217 /// Written through the checked encoder, which on this draft refuses
218 /// nothing: SUBGROUP_HEADER has one shape here, every field goes out every
219 /// time, and no type byte selects between them. The drafts with a header
220 /// type table do need the refusal, and every draft's writer makes the same
221 /// checked call, so no draft can be the one that skips it.
222 pub async fn write_subgroup_header(
223 &mut self,
224 header: &AnySubgroupHeader,
225 ) -> Result<(), ConnectionError> {
226 let mut buf = Vec::new();
227 header.encode_stream_checked(&mut buf)?;
228 self.inner.write_all(&buf).await?;
229 self.subgroup_objects = Some(None);
230 Ok(())
231 }
232
233 /// Write a fetch response header.
234 pub async fn write_fetch_header(
235 &mut self,
236 header: &AnyFetchHeader,
237 ) -> Result<(), ConnectionError> {
238 let mut buf = Vec::new();
239 header.encode_stream(&mut buf);
240 self.inner.write_all(&buf).await?;
241 Ok(())
242 }
243
244 /// Append a draft-09 subgroup object (header + payload) to the stream.
245 ///
246 /// Section 8.4.1: "A publisher MUST NOT send an Object on a stream if its
247 /// Object ID is less than a previously sent Object ID within a given group
248 /// in that stream." A subgroup stream carries one group, so the Object IDs
249 /// written here are exactly the ones that sentence compares, and the
250 /// comparison needs the object before - which no per-header check can see.
251 /// The state advances only once the object has been written, so declining to
252 /// write an object leaves the next one measured against the last one kept.
253 ///
254 /// An equal Object ID is refused as well as a smaller one. The draft's own
255 /// sentence forbids only "less than", but an Object ID names an Object
256 /// within a Group: writing one twice on a stream describes the same Object
257 /// with two different payloads, and a reader has no way to choose. The
258 /// dispatch-level writer in the codec draws the line in the same place, and
259 /// two writers that disagreed about it would be worse than either answer.
260 ///
261 /// # Errors
262 ///
263 /// [`ConnectionError::DataStreamState`] if no subgroup header has been
264 /// written yet, or if `object` does not advance past the last one written.
265 pub async fn write_subgroup_object(
266 &mut self,
267 object: &SubgroupObject,
268 ) -> Result<(), ConnectionError> {
269 let previous = self
270 .subgroup_objects
271 .as_mut()
272 .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
273 let object_id = object.header.object_id.into_inner();
274 if matches!(*previous, Some(prev) if object_id <= prev) {
275 return Err(ConnectionError::DataStreamState(
276 "object id does not advance on the last one written to this stream",
277 ));
278 }
279 // The declared length comes from the payload rather than from the
280 // caller's field: a header that disagrees with the bytes beside it
281 // desynchronises every object after it on the stream, and nothing
282 // downstream can recover.
283 let mut header = object.header.clone();
284 header.payload_length = VarInt::from_usize(object.payload.len());
285 let mut buf = Vec::new();
286 header.encode_checked(&mut buf)?;
287 buf.extend_from_slice(&object.payload);
288 self.inner.write_all(&buf).await?;
289 *previous = Some(object_id);
290 Ok(())
291 }
292
293 /// Append a draft-09 fetch object (header + payload) to the stream.
294 pub async fn write_fetch_object(
295 &mut self,
296 object: &FetchObject,
297 ) -> Result<(), ConnectionError> {
298 // The declared length comes from the payload rather than from the
299 // caller's field: a header that disagrees with the bytes beside it
300 // desynchronises every object after it on the stream, and nothing
301 // downstream can recover.
302 let mut header = object.header.clone();
303 header.payload_length = VarInt::from_usize(object.payload.len());
304 let mut buf = Vec::new();
305 header.encode_checked(&mut buf)?;
306 buf.extend_from_slice(&object.payload);
307 self.inner.write_all(&buf).await?;
308 Ok(())
309 }
310
311 /// Finish the stream (send FIN).
312 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
313 self.inner.finish()?;
314 Ok(())
315 }
316}
317
318/// What an Object Status makes of an object, for Section 8.1.1.1's table.
319///
320/// `None` is Normal and not "no status": a datagram carrying a payload has no
321/// status field at all, and an object with a payload is an ordinary object.
322fn object_role(status: Option<u64>) -> ObjectRole {
323 match status {
324 None | Some(0x0) => ObjectRole::Produced,
325 // 0x4, end of Track and Group: its Group ID names the track's last
326 // group, so it may equal the largest seen.
327 Some(0x4) => ObjectRole::EndsTrack(Some(EndOfTrackForm::LastGroup)),
328 // 0x5, end of Track: its Group ID names the group *after* the last, so
329 // it may not.
330 Some(0x5) => ObjectRole::EndsTrack(Some(EndOfTrackForm::PastLastGroup)),
331 // Every other status is a statement about objects rather than one of
332 // them. Two of them name an Object ID one past the largest on purpose,
333 // so counting one as produced would refuse the end-of-track object the
334 // draft goes on to define.
335 _ => ObjectRole::Neither,
336 }
337}
338
339/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
340pub struct FramedRecvStream {
341 inner: RecvStream,
342 buf: BytesMut,
343 /// The record this stream's objects are measured against, and the Group ID
344 /// its header named.
345 ///
346 /// One group for the whole stream: a subgroup header names it once and no
347 /// object header repeats it. `None` on a stream that was never given one —
348 /// a stream for an alias no live binding names, and every stream built
349 /// outside [`Connection::accept_subgroup_stream`] — and on such a stream
350 /// the objects are read without being measured against the track at all.
351 tracking: Option<(TrackObjects, u64)>,
352}
353
354impl FramedRecvStream {
355 /// Create a new framed receive stream.
356 pub fn new(inner: RecvStream) -> Self {
357 Self { inner, buf: BytesMut::with_capacity(4096), tracking: None }
358 }
359
360 /// Measure this stream's objects against `objects`, all of them in `group`.
361 ///
362 /// Called by [`Connection::accept_subgroup_stream`] once the header has been
363 /// read, which is the only point at which both the track and the group are
364 /// known.
365 fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
366 self.tracking = Some((objects, group));
367 }
368
369 /// Record or judge one object this stream carried.
370 ///
371 /// The whole of Section 8.1.1.1's rule that a single header cannot settle: the
372 /// object's Group ID is the stream's, its Object ID is its own, and what
373 /// they are measured against is everything the track has carried on any
374 /// stream.
375 fn note_subgroup_object(&self, header: &ObjectHeader) -> Result<(), ConnectionError> {
376 let Some((objects, group)) = &self.tracking else { return Ok(()) };
377 let at = ObjectLocation { group: *group, object: header.object_id.into_inner() };
378 objects.note(at, object_role(Some(header.object_status as u64))).map_err(|placement| {
379 ConnectionError::Endpoint(EndpointError::EndOfTrackOutOfPlace {
380 alias: objects.alias(),
381 group: at.group,
382 object: at.object,
383 placement,
384 })
385 })
386 }
387
388 /// Get the transport-level stream ID.
389 pub fn stream_id(&self) -> u64 {
390 self.inner.stream_id()
391 }
392
393 /// Read more data from the stream into the internal buffer.
394 async fn fill(&mut self) -> Result<bool, ConnectionError> {
395 let mut tmp = [0u8; 4096];
396 match self.inner.read(&mut tmp).await {
397 Ok(Some(n)) => {
398 self.buf.extend_from_slice(&tmp[..n]);
399 Ok(true)
400 }
401 Ok(None) => Ok(false),
402 Err(e) => Err(ConnectionError::Transport(e)),
403 }
404 }
405
406 /// Ensure at least `n` bytes are available in the buffer.
407 async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
408 while self.buf.len() < n {
409 if !self.fill().await? {
410 return Err(ConnectionError::UnexpectedEnd);
411 }
412 }
413 Ok(())
414 }
415
416 /// Read a control message from the stream.
417 ///
418 /// When `capture_raw` is true, the returned tuple includes a clone of the
419 /// framed wire bytes (for observer emission). When false, the second
420 /// element is `None` and the payload clone is skipped.
421 pub async fn read_control(
422 &mut self,
423 capture_raw: bool,
424 ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
425 // Read type ID varint
426 self.ensure(1).await?;
427 let type_len = varint_len(self.buf[0]);
428 self.ensure(type_len).await?;
429
430 let mut cursor = &self.buf[..type_len];
431 let _type_id = VarInt::decode(&mut cursor)?;
432
433 // Draft-09 uses varint length framing.
434 self.ensure(type_len + 1).await?;
435 let payload_len_start = type_len;
436 let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
437 self.ensure(type_len + payload_len_varint_len).await?;
438 let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
439 let payload_len = VarInt::decode(&mut cursor)?.into_inner() as usize;
440 let len_field_size = payload_len_varint_len;
441
442 // Read full payload
443 let total = type_len + len_field_size + payload_len;
444 self.ensure(total).await?;
445
446 // Capture raw bytes only if requested (observer attached).
447 let raw = capture_raw.then(|| self.buf[..total].to_vec());
448
449 // Now decode the whole message using the draft-09 dispatcher
450 let mut frame = &self.buf[..total];
451 let msg = AnyControlMessage::decode(DraftVersion::Draft09, &mut frame)?;
452 self.buf.advance(total);
453 Ok((msg, raw))
454 }
455
456 /// Read a subgroup stream header.
457 pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
458 self.ensure(1).await?;
459 loop {
460 let mut cursor = &self.buf[..];
461 match AnySubgroupHeader::decode_stream(DraftVersion::Draft09, &mut cursor) {
462 Ok(header) => {
463 let consumed = self.buf.len() - cursor.remaining();
464 self.buf.advance(consumed);
465 return Ok(header);
466 }
467 Err(e) if e.is_incomplete() => {
468 if !self.fill().await? {
469 return Err(ConnectionError::UnexpectedEnd);
470 }
471 }
472 Err(e) => return Err(ConnectionError::Codec(e)),
473 }
474 }
475 }
476
477 /// Read a fetch response header.
478 pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
479 self.ensure(1).await?;
480 loop {
481 let mut cursor = &self.buf[..];
482 match AnyFetchHeader::decode_stream(DraftVersion::Draft09, &mut cursor) {
483 Ok(header) => {
484 let consumed = self.buf.len() - cursor.remaining();
485 self.buf.advance(consumed);
486 return Ok(header);
487 }
488 Err(e) if e.is_incomplete() => {
489 if !self.fill().await? {
490 return Err(ConnectionError::UnexpectedEnd);
491 }
492 }
493 Err(e) => return Err(ConnectionError::Codec(e)),
494 }
495 }
496 }
497
498 /// Read the next draft-09 subgroup object (header + payload). Since
499 /// draft-09 subgroup objects are stateless, this does not require any
500 /// prior header-decoding state.
501 pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
502 loop {
503 let mut cursor = &self.buf[..];
504 match ObjectHeader::decode(&mut cursor) {
505 Ok(header) => {
506 let header_consumed = self.buf.len() - cursor.remaining();
507 let payload_len = header.payload_length.into_inner() as usize;
508 let total = header_consumed + payload_len;
509 if self.buf.len() < total {
510 if !self.fill().await? {
511 return Err(ConnectionError::UnexpectedEnd);
512 }
513 continue;
514 }
515 let payload = self.buf[header_consumed..total].to_vec();
516 self.buf.advance(total);
517 self.note_subgroup_object(&header)?;
518 return Ok(SubgroupObject { header, payload });
519 }
520 Err(e) if e.is_incomplete() => {
521 if !self.fill().await? {
522 return Err(ConnectionError::UnexpectedEnd);
523 }
524 }
525 Err(e) => return Err(ConnectionError::Codec(e)),
526 }
527 }
528 }
529
530 /// Read the next draft-09 fetch object (header + payload).
531 pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
532 loop {
533 let mut cursor = &self.buf[..];
534 match FetchObjectHeader::decode(&mut cursor) {
535 Ok(header) => {
536 let header_consumed = self.buf.len() - cursor.remaining();
537 let payload_len = header.payload_length.into_inner() as usize;
538 let total = header_consumed + payload_len;
539 if self.buf.len() < total {
540 if !self.fill().await? {
541 return Err(ConnectionError::UnexpectedEnd);
542 }
543 continue;
544 }
545 let payload = self.buf[header_consumed..total].to_vec();
546 self.buf.advance(total);
547 return Ok(FetchObject { header, payload });
548 }
549 Err(e) if e.is_incomplete() => {
550 if !self.fill().await? {
551 return Err(ConnectionError::UnexpectedEnd);
552 }
553 }
554 Err(e) => return Err(ConnectionError::Codec(e)),
555 }
556 }
557 }
558}
559
560/// A live draft-09 MoQT connection over QUIC or WebTransport.
561pub struct Connection {
562 transport: Transport,
563 endpoint: Endpoint,
564 control_send: Option<FramedSendStream>,
565 control_recv: Option<FramedRecvStream>,
566 observer: Option<Box<dyn ConnectionObserver>>,
567 /// Setup events buffered during `connect()` and replayed when an
568 /// observer attaches via `set_observer` — without this, an observer
569 /// attached after `connect` returns would never see the handshake.
570 pending_events: Vec<ClientEvent>,
571 /// The server's half of the setup handshake, kept whole.
572 ///
573 /// The endpoint acts on the parameters it recognises and retains none of
574 /// them, and which parameters a server sends — in what order, with what
575 /// values — is the sharpest thing a session says about the implementation
576 /// behind it.
577 server_setup: AnyControlMessage,
578 /// The framed wire bytes of [`Self::server_setup`].
579 server_setup_raw: Option<Vec<u8>>,
580}
581
582impl Connection {
583 /// Connect to a draft-09 MoQT server as a client.
584 pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
585 // PATH is for native QUIC only, and the transport is known here and
586 // nowhere further in. Refusing before dialling means a session that
587 // the server would close on sight is never opened.
588 setup::validate_client_path_transport(
589 &config.setup_parameters,
590 matches!(config.transport, TransportType::WebTransport { .. }),
591 )
592 .map_err(EndpointError::from)?;
593
594 let transport = match &config.transport {
595 TransportType::Quic => Self::connect_quic(addr, &config).await?,
596 TransportType::WebTransport { url } => {
597 let url = url.clone();
598 Self::connect_webtransport(&url, &config).await?
599 }
600 };
601
602 Self::adopt(transport, config).await
603 }
604
605 /// Run the MoQT setup handshake over a transport somebody else established.
606 ///
607 /// For choosing the draft from what the server selected: dial once through
608 /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
609 /// connection to the module its answer names. [`Self::connect`] cannot do
610 /// this — it derives its single ALPN from the draft it was given.
611 ///
612 /// `config.draft` must match this module. The transport is adopted as
613 /// given; nothing here re-checks the ALPN it was negotiated with.
614 pub async fn adopt(
615 transport: Transport,
616 config: ClientConfig,
617 ) -> Result<Self, ConnectionError> {
618 Self::adopt_offering(transport, config, None).await
619 }
620
621 /// [`Self::adopt`], offering exactly `versions` in CLIENT_SETUP.
622 ///
623 /// `None` offers what `config` implies, which is what [`Self::adopt`]
624 /// passes. `Some` replaces the list outright, and takes raw varints rather
625 /// than [`DraftVersion`]s because the reason to reach for this is to offer
626 /// a version no draft assigns — which an enum of drafts cannot name.
627 ///
628 /// A server MUST answer with a version the client offered and MUST
629 /// otherwise close the session; from draft-11 the code for that is
630 /// `VERSION_NEGOTIATION_FAILED` (0x15). How a relay spells the refusal is
631 /// a conformance measurement, and offering a version deliberately outside
632 /// the negotiable set is the only way to ask for it.
633 pub async fn adopt_offering(
634 transport: Transport,
635 config: ClientConfig,
636 versions: Option<Vec<VarInt>>,
637 ) -> Result<Self, ConnectionError> {
638 // PATH is for native QUIC only, and the transport is known here and
639 // nowhere further in. Refusing before dialling means a session that
640 // the server would close on sight is never opened.
641 setup::validate_client_path_transport(
642 &config.setup_parameters,
643 matches!(config.transport, TransportType::WebTransport { .. }),
644 )
645 .map_err(EndpointError::from)?;
646
647 // Open bidirectional control stream
648 let (send, recv) = transport.open_bi().await?;
649 let mut control_send = FramedSendStream::new(send);
650 let mut control_recv = FramedRecvStream::new(recv);
651
652 // Perform setup handshake
653 let mut endpoint = Endpoint::new(Role::Client);
654 endpoint.connect()?;
655 let setup_msg = endpoint.send_client_setup(
656 versions.unwrap_or_else(|| config.supported_versions()),
657 config.setup_parameters.clone(),
658 )?;
659 let any_setup = AnyControlMessage::Draft09(setup_msg);
660 let raw_setup = control_send.write_control(&any_setup).await?;
661
662 let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
663 match &server_setup {
664 AnyControlMessage::Draft09(ControlMessage::ServerSetup(ref ss)) => {
665 endpoint.receive_server_setup(ss)?;
666 }
667 _ => {
668 return Err(ConnectionError::Endpoint(EndpointError::NotActive));
669 }
670 }
671
672 let mut pending_events = Vec::with_capacity(3);
673 pending_events.push(ClientEvent::ControlMessage {
674 direction: Direction::Send,
675 message: any_setup,
676 raw: Some(raw_setup),
677 });
678 pending_events.push(ClientEvent::ControlMessage {
679 direction: Direction::Receive,
680 message: server_setup.clone(),
681 raw: raw_server_setup.clone(),
682 });
683 if let Some(v) = endpoint.negotiated_version() {
684 pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
685 }
686
687 Ok(Self {
688 transport,
689 endpoint,
690 control_send: Some(control_send),
691 control_recv: Some(control_recv),
692 observer: None,
693 pending_events,
694 server_setup,
695 server_setup_raw: raw_server_setup,
696 })
697 }
698
699 /// Establish a raw QUIC connection.
700 ///
701 /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
702 /// TLS and endpoint setup.
703 async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
704 let (transport, _negotiated) = crate::transport::dial_quic(
705 addr,
706 &crate::transport::QuicDialOptions {
707 skip_cert_verification: config.skip_cert_verification,
708 ca_certs: config.ca_certs.clone(),
709 ..crate::transport::QuicDialOptions::new(config.alpn())
710 },
711 )
712 .await?;
713 Ok(transport)
714 }
715
716 /// Establish a WebTransport connection.
717 ///
718 /// [`crate::transport::dial_webtransport`] holds the TLS and endpoint
719 /// setup, exactly as `connect_quic` above defers its own. That is not
720 /// only deduplication: both dials hand the same `QuicDialOptions` to the
721 /// same config constructor in `transport::quic`, so the bundled roots and
722 /// `config.ca_certs` are what each of them trusts and one relay gets one
723 /// verdict whichever transport carries it. Settling trust at this call site
724 /// instead — from `wtransport`'s own builder settings, or from a second
725 /// config of this draft's own — puts the decision in two places, and a
726 /// caller's private CA then reaches only the dials whose call site
727 /// installed it.
728 #[cfg(feature = "webtransport")]
729 async fn connect_webtransport(
730 url: &str,
731 config: &ClientConfig,
732 ) -> Result<Transport, ConnectionError> {
733 Ok(crate::transport::dial_webtransport(
734 url,
735 &crate::transport::QuicDialOptions {
736 skip_cert_verification: config.skip_cert_verification,
737 ca_certs: config.ca_certs.clone(),
738 ..crate::transport::QuicDialOptions::new(config.alpn())
739 },
740 )
741 .await?)
742 }
743
744 /// Stub for when the webtransport feature is not enabled.
745 #[cfg(not(feature = "webtransport"))]
746 async fn connect_webtransport(
747 _url: &str,
748 _config: &ClientConfig,
749 ) -> Result<Transport, ConnectionError> {
750 Err(ConnectionError::Transport(TransportError::Connect(
751 "webtransport feature not enabled".into(),
752 )))
753 }
754
755 // ── Observer ───────────────────────────────────────────────
756
757 /// Attach an observer. Buffered handshake events from `connect()` are
758 /// flushed in arrival order before this returns.
759 pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
760 self.observer = Some(observer);
761 for event in self.pending_events.drain(..) {
762 if let Some(ref obs) = self.observer {
763 obs.on_event_owned(event);
764 }
765 }
766 }
767
768 /// Remove the observer.
769 pub fn clear_observer(&mut self) {
770 self.observer = None;
771 }
772
773 /// Emit an event to the observer, if one is attached.
774 fn emit(&self, event: ClientEvent) {
775 if let Some(ref obs) = self.observer {
776 obs.on_event_owned(event);
777 }
778 }
779
780 // ── Control message I/O ─────────────────────────────────
781
782 /// Send a control message on the control stream.
783 pub async fn send_control(&mut self, msg: &ControlMessage) -> Result<(), ConnectionError> {
784 let any = AnyControlMessage::Draft09(msg.clone());
785 let send = self.control_send.as_mut().ok_or(ConnectionError::NoControlStream)?;
786 let raw = send.write_control(&any).await?;
787 self.emit(ClientEvent::ControlMessage {
788 direction: Direction::Send,
789 message: any,
790 raw: Some(raw),
791 });
792 Ok(())
793 }
794
795 /// Read the next control message from the control stream.
796 pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
797 let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
798 let capture_raw = self.observer.is_some();
799 let (any, raw) = match recv.read_control(capture_raw).await {
800 Ok(v) => v,
801 Err(e) => return Err(self.close_for_codec(e)),
802 };
803 if capture_raw {
804 self.emit(ClientEvent::ControlMessage {
805 direction: Direction::Receive,
806 message: any.clone(),
807 raw,
808 });
809 }
810 match any {
811 AnyControlMessage::Draft09(msg) => Ok(msg),
812 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 09 the
813 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
814 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
815 // naming the other drafts: such a list has to be edited in every draft
816 // module whenever a draft is added, and a copy that omits one leaves this match
817 // non-exhaustive.
818 #[allow(unreachable_patterns)]
819 _ => Err(ConnectionError::ControlMessageNarrowing),
820 }
821 }
822
823 /// Read and dispatch the next incoming control message through the endpoint
824 /// state machine. Returns the decoded message for inspection.
825 pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
826 let msg = self.recv_control().await?;
827 self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
828
829 if let ControlMessage::GoAway(ref ga) = msg {
830 self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
831 }
832
833 Ok(msg)
834 }
835
836 // ── Subscribe flow ──────────────────────────────────────
837 /// The lowest Track Alias no live subscription on this connection holds.
838 ///
839 /// This draft makes the Track Alias the subscriber's to choose, so
840 /// [`Self::subscribe`] takes one and every later-era caller has nothing to
841 /// pass. See
842 /// [`Endpoint::next_free_track_alias`](crate::draft09::endpoint::Endpoint::next_free_track_alias)
843 /// for why the value is read off the endpoint rather than asked of the
844 /// caller, and why picking one here stays correct alongside a caller that
845 /// picks its own.
846 pub fn next_free_track_alias(&self) -> VarInt {
847 self.endpoint.next_free_track_alias()
848 }
849
850 /// Send a SUBSCRIBE and return the allocated subscribe ID.
851 #[allow(clippy::too_many_arguments)]
852 pub async fn subscribe(
853 &mut self,
854 track_alias: VarInt,
855 track_namespace: TrackNamespace,
856 track_name: Vec<u8>,
857 subscriber_priority: u8,
858 group_order: GroupOrder,
859 filter_type: FilterType,
860 ) -> Result<VarInt, ConnectionError> {
861 let (sub_id, msg) = self.endpoint.subscribe(
862 track_alias,
863 track_namespace,
864 track_name,
865 subscriber_priority,
866 group_order,
867 filter_type,
868 )?;
869 self.send_control(&msg).await?;
870 Ok(sub_id)
871 }
872
873 /// Send a SUBSCRIBE for a range of the track and return the allocated ID.
874 ///
875 /// The Filter Type comes from the arguments, so the message cannot name a
876 /// filter whose fields it does not carry.
877 #[allow(clippy::too_many_arguments)]
878 pub async fn subscribe_range(
879 &mut self,
880 track_alias: VarInt,
881 track_namespace: TrackNamespace,
882 track_name: Vec<u8>,
883 subscriber_priority: u8,
884 group_order: GroupOrder,
885 start_location: Location,
886 end_group: Option<VarInt>,
887 ) -> Result<VarInt, ConnectionError> {
888 let (sub_id, msg) = self.endpoint.subscribe_range(
889 track_alias,
890 track_namespace,
891 track_name,
892 subscriber_priority,
893 group_order,
894 start_location,
895 end_group,
896 )?;
897 self.send_control(&msg).await?;
898 Ok(sub_id)
899 }
900
901 /// Send an UNSUBSCRIBE for the given subscribe ID.
902 pub async fn unsubscribe(&mut self, subscribe_id: VarInt) -> Result<(), ConnectionError> {
903 let msg = self.endpoint.unsubscribe(subscribe_id)?;
904 self.send_control(&msg).await
905 }
906
907 /// Accept a subscription the peer opened, sending SUBSCRIBE_OK.
908 pub async fn subscribe_ok(
909 &mut self,
910 subscribe_id: VarInt,
911 expires: VarInt,
912 group_order: GroupOrder,
913 parameters: Vec<KeyValuePair>,
914 ) -> Result<(), ConnectionError> {
915 let msg =
916 self.endpoint.send_subscribe_ok(subscribe_id, expires, group_order, parameters)?;
917 self.send_control(&msg).await
918 }
919
920 /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
921 ///
922 /// The Track Alias travels back with the refusal: under the 'Retry Track
923 /// Alias' code it is the alias the peer should try again with, and under
924 /// any other code it is ignored.
925 pub async fn subscribe_error(
926 &mut self,
927 subscribe_id: VarInt,
928 error_code: VarInt,
929 reason_phrase: Vec<u8>,
930 track_alias: VarInt,
931 ) -> Result<(), ConnectionError> {
932 let msg = self.endpoint.send_subscribe_error(
933 subscribe_id,
934 error_code,
935 reason_phrase,
936 track_alias,
937 )?;
938 self.send_control(&msg).await
939 }
940
941 /// End a subscription this endpoint accepted, sending SUBSCRIBE_DONE.
942 pub async fn subscribe_done(
943 &mut self,
944 subscribe_id: VarInt,
945 status_code: VarInt,
946 reason_phrase: Vec<u8>,
947 ) -> Result<(), ConnectionError> {
948 let msg = self.endpoint.send_subscribe_done(subscribe_id, status_code, reason_phrase)?;
949 self.send_control(&msg).await
950 }
951
952 // ── Fetch flow ──────────────────────────────────────────
953
954 /// Send a FETCH and return the allocated subscribe ID.
955 #[allow(clippy::too_many_arguments)]
956 pub async fn fetch(
957 &mut self,
958 track_namespace: TrackNamespace,
959 track_name: Vec<u8>,
960 subscriber_priority: u8,
961 group_order: GroupOrder,
962 start_group: VarInt,
963 start_object: VarInt,
964 end_group: VarInt,
965 end_object: VarInt,
966 ) -> Result<VarInt, ConnectionError> {
967 let (sub_id, msg) = self.endpoint.fetch(
968 track_namespace,
969 track_name,
970 subscriber_priority,
971 group_order,
972 start_group,
973 start_object,
974 end_group,
975 end_object,
976 )?;
977 self.send_control(&msg).await?;
978 Ok(sub_id)
979 }
980
981 /// Send a Joining Fetch and return the allocated subscribe ID.
982 ///
983 /// `preceding_group_offset` counts groups back from the live edge of the
984 /// subscription it names rather than naming a group of its own. Section
985 /// 7.7.1 has the publisher compute "Fetch StartGroup: Subscribe Largest
986 /// Group - Preceding Group Offset", so 0 asks for the current group and
987 /// nothing before it.
988 ///
989 /// This draft draws one joining kind, at Fetch Type 0x2, so there is
990 /// nothing here for a caller to choose between and nothing to choose
991 /// wrongly.
992 pub async fn joining_fetch(
993 &mut self,
994 subscriber_priority: u8,
995 group_order: GroupOrder,
996 joining_subscribe_id: VarInt,
997 preceding_group_offset: VarInt,
998 ) -> Result<VarInt, ConnectionError> {
999 let (sub_id, msg) = self.endpoint.joining_fetch(
1000 subscriber_priority,
1001 group_order,
1002 joining_subscribe_id,
1003 preceding_group_offset,
1004 )?;
1005 self.send_control(&msg).await?;
1006 Ok(sub_id)
1007 }
1008
1009 /// Send a FETCH_CANCEL for the given subscribe ID.
1010 pub async fn fetch_cancel(&mut self, subscribe_id: VarInt) -> Result<(), ConnectionError> {
1011 let msg = self.endpoint.fetch_cancel(subscribe_id)?;
1012 self.send_control(&msg).await
1013 }
1014
1015 /// Accept a fetch the peer opened, sending FETCH_OK.
1016 ///
1017 /// The endpoint refuses a Joining Fetch naming a subscription this session
1018 /// cannot join and refuses a second answer to one FETCH, so nothing is
1019 /// written on the wire when it does either.
1020 pub async fn fetch_ok(
1021 &mut self,
1022 subscribe_id: VarInt,
1023 group_order: GroupOrder,
1024 end_of_track: u8,
1025 largest_group_id: VarInt,
1026 largest_object_id: VarInt,
1027 parameters: Vec<KeyValuePair>,
1028 ) -> Result<(), ConnectionError> {
1029 let msg = self.endpoint.send_fetch_ok(
1030 subscribe_id,
1031 group_order,
1032 end_of_track,
1033 largest_group_id,
1034 largest_object_id,
1035 parameters,
1036 )?;
1037 self.send_control(&msg).await
1038 }
1039
1040 /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1041 ///
1042 /// The endpoint refuses a second answer to one FETCH, so nothing is
1043 /// written on the wire when it does.
1044 pub async fn fetch_error(
1045 &mut self,
1046 subscribe_id: VarInt,
1047 error_code: VarInt,
1048 reason_phrase: Vec<u8>,
1049 ) -> Result<(), ConnectionError> {
1050 let msg = self.endpoint.send_fetch_error(subscribe_id, error_code, reason_phrase)?;
1051 self.send_control(&msg).await
1052 }
1053
1054 // ── Namespace flows ─────────────────────────────────────
1055
1056 /// Send a SUBSCRIBE_ANNOUNCES.
1057 pub async fn subscribe_announces(
1058 &mut self,
1059 track_namespace_prefix: TrackNamespace,
1060 ) -> Result<(), ConnectionError> {
1061 let msg = self.endpoint.subscribe_announces(track_namespace_prefix)?;
1062 self.send_control(&msg).await
1063 }
1064
1065 /// Accept a namespace subscription the peer made, sending SUBSCRIBE_ANNOUNCES_OK.
1066 ///
1067 /// The endpoint refuses a second answer to one SUBSCRIBE_ANNOUNCES, so nothing is
1068 /// written on the wire when it does.
1069 pub async fn subscribe_announces_ok(
1070 &mut self,
1071 track_namespace_prefix: TrackNamespace,
1072 ) -> Result<(), ConnectionError> {
1073 let msg = self.endpoint.send_subscribe_announces_ok(track_namespace_prefix)?;
1074 self.send_control(&msg).await
1075 }
1076
1077 /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_ANNOUNCES_ERROR.
1078 ///
1079 /// The other half of the same sentence: one answer, and this is the other
1080 /// one it can be.
1081 pub async fn subscribe_announces_error(
1082 &mut self,
1083 track_namespace_prefix: TrackNamespace,
1084 error_code: VarInt,
1085 reason_phrase: Vec<u8>,
1086 ) -> Result<(), ConnectionError> {
1087 let msg = self.endpoint.send_subscribe_announces_error(
1088 track_namespace_prefix,
1089 error_code,
1090 reason_phrase,
1091 )?;
1092 self.send_control(&msg).await
1093 }
1094
1095 /// Send an ANNOUNCE.
1096 pub async fn announce(
1097 &mut self,
1098 track_namespace: TrackNamespace,
1099 ) -> Result<(), ConnectionError> {
1100 let msg = self.endpoint.announce(track_namespace)?;
1101 self.send_control(&msg).await
1102 }
1103
1104 /// Send an UNANNOUNCE.
1105 pub async fn unannounce(
1106 &mut self,
1107 track_namespace: TrackNamespace,
1108 ) -> Result<(), ConnectionError> {
1109 let msg = self.endpoint.unannounce(track_namespace)?;
1110 self.send_control(&msg).await
1111 }
1112
1113 /// Accept an announcement the peer made, sending ANNOUNCE_OK.
1114 ///
1115 /// The endpoint refuses a second answer to one ANNOUNCE, so nothing is
1116 /// written on the wire when it does.
1117 pub async fn announce_ok(
1118 &mut self,
1119 track_namespace: TrackNamespace,
1120 ) -> Result<(), ConnectionError> {
1121 let msg = self.endpoint.send_announce_ok(track_namespace)?;
1122 self.send_control(&msg).await
1123 }
1124
1125 /// Refuse an announcement the peer made, sending ANNOUNCE_ERROR.
1126 ///
1127 /// The other half of the same sentence: one answer, and this is the other
1128 /// one it can be.
1129 pub async fn announce_error(
1130 &mut self,
1131 track_namespace: TrackNamespace,
1132 error_code: VarInt,
1133 reason_phrase: Vec<u8>,
1134 ) -> Result<(), ConnectionError> {
1135 let msg = self.endpoint.send_announce_error(track_namespace, error_code, reason_phrase)?;
1136 self.send_control(&msg).await
1137 }
1138
1139 /// Revoke an acceptance, sending ANNOUNCE_CANCEL.
1140 ///
1141 /// The endpoint refuses one for an announcement it never accepted, so
1142 /// nothing is written on the wire when it does.
1143 pub async fn announce_cancel(
1144 &mut self,
1145 track_namespace: TrackNamespace,
1146 error_code: VarInt,
1147 reason_phrase: Vec<u8>,
1148 ) -> Result<(), ConnectionError> {
1149 let msg = self.endpoint.announce_cancel(track_namespace, error_code, reason_phrase)?;
1150 self.send_control(&msg).await
1151 }
1152 // ── Track Status flow ────────────────────────────────────
1153
1154 /// Send a TRACK_STATUS_REQUEST.
1155 pub async fn track_status_request(
1156 &mut self,
1157 track_namespace: TrackNamespace,
1158 track_name: Vec<u8>,
1159 ) -> Result<(), ConnectionError> {
1160 let msg = self.endpoint.track_status_request(track_namespace, track_name)?;
1161 self.send_control(&msg).await
1162 }
1163
1164 /// Answer a TRACK_STATUS_REQUEST the peer sent, sending TRACK_STATUS.
1165 ///
1166 /// The endpoint refuses a second answer to one request, so nothing is
1167 /// written on the wire when it does.
1168 pub async fn track_status(
1169 &mut self,
1170 track_namespace: TrackNamespace,
1171 track_name: Vec<u8>,
1172 status_code: VarInt,
1173 last_group_id: VarInt,
1174 last_object_id: VarInt,
1175 ) -> Result<(), ConnectionError> {
1176 let msg = self.endpoint.send_track_status(
1177 track_namespace,
1178 track_name,
1179 status_code,
1180 last_group_id,
1181 last_object_id,
1182 )?;
1183 self.send_control(&msg).await
1184 }
1185
1186 // ── Data streams ────────────────────────────────────────
1187
1188 /// Open a new unidirectional stream for sending subgroup data.
1189 pub async fn open_subgroup_stream(
1190 &self,
1191 header: &AnySubgroupHeader,
1192 ) -> Result<FramedSendStream, ConnectionError> {
1193 // Before the stream is opened: the Original Publisher is who the rule
1194 // binds, so a header that would mix this track's framing is refused
1195 // here rather than written and answered by the peer.
1196 self.endpoint.note_object_forwarding_preference(
1197 header.track_alias(),
1198 ObjectForwardingPreference::Subgroup,
1199 )?;
1200 let send = self.transport.open_uni().await?;
1201 let mut framed = FramedSendStream::new(send);
1202 let sid = framed.stream_id();
1203 framed.write_subgroup_header(header).await?;
1204 self.emit(ClientEvent::StreamOpened {
1205 direction: Direction::Send,
1206 stream_kind: StreamKind::Subgroup,
1207 stream_id: sid,
1208 });
1209 self.emit(ClientEvent::DataStreamHeader {
1210 stream_id: sid,
1211 direction: Direction::Send,
1212 header: header.clone(),
1213 });
1214 Ok(framed)
1215 }
1216
1217 /// Open a new unidirectional stream for sending a FETCH's objects.
1218 ///
1219 /// The objects answering a FETCH do not go on the request's own stream:
1220 /// they go on a unidirectional stream of their own, which opens with a
1221 /// FETCH_HEADER naming the request they belong to. This writes that header
1222 /// and hands back the stream, the same way
1223 /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1224 /// subgroup.
1225 ///
1226 /// The caller owns the stream that comes back. Nothing here remembers
1227 /// which request it belongs to, so an endpoint serving several fetches at
1228 /// once keeps its own map from Request ID to stream.
1229 pub async fn open_fetch_stream(
1230 &self,
1231 header: &AnyFetchHeader,
1232 ) -> Result<FramedSendStream, ConnectionError> {
1233 let send = self.transport.open_uni().await?;
1234 let mut framed = FramedSendStream::new(send);
1235 let sid = framed.stream_id();
1236 framed.write_fetch_header(header).await?;
1237 self.emit(ClientEvent::StreamOpened {
1238 direction: Direction::Send,
1239 stream_kind: StreamKind::Fetch,
1240 stream_id: sid,
1241 });
1242 Ok(framed)
1243 }
1244
1245 /// Accept the next unidirectional stream and read its fetch header.
1246 ///
1247 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1248 /// are separate because the header decides how every object after it is
1249 /// framed, so a caller has to know which it is expecting before the first
1250 /// byte is read.
1251 ///
1252 /// Objects come off the returned stream with
1253 /// [`FramedRecvStream::read_fetch_object`].
1254 pub async fn accept_fetch_stream(
1255 &self,
1256 ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1257 let recv = self.transport.accept_uni().await?;
1258 let mut framed = FramedRecvStream::new(recv);
1259 let sid = framed.stream_id();
1260 let header = framed.read_fetch_header().await?;
1261 self.emit(ClientEvent::StreamOpened {
1262 direction: Direction::Receive,
1263 stream_kind: StreamKind::Fetch,
1264 stream_id: sid,
1265 });
1266 self.emit(ClientEvent::FetchStreamHeader {
1267 stream_id: sid,
1268 direction: Direction::Receive,
1269 header: header.clone(),
1270 });
1271 // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1272 // carries an `AnySubgroupHeader` and cannot express one. What
1273 // `accept_subgroup_stream` does beyond this - the forwarding-preference
1274 // note, the object measurement - is about a subgroup and has no
1275 // counterpart on a fetch stream.
1276 Ok((header, framed))
1277 }
1278
1279 /// Accept an incoming unidirectional data stream and read its subgroup
1280 /// header.
1281 pub async fn accept_subgroup_stream(
1282 &self,
1283 ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1284 let recv = self.transport.accept_uni().await?;
1285 let mut framed = FramedRecvStream::new(recv);
1286 let sid = framed.stream_id();
1287 let header = framed.read_subgroup_header().await?;
1288 self.emit(ClientEvent::StreamOpened {
1289 direction: Direction::Receive,
1290 stream_kind: StreamKind::Subgroup,
1291 stream_id: sid,
1292 });
1293 self.emit(ClientEvent::DataStreamHeader {
1294 stream_id: sid,
1295 direction: Direction::Receive,
1296 header: header.clone(),
1297 });
1298 // Every object on a subgroup stream has the Subgroup preference, so
1299 // the header settles the track's framing before a single object is
1300 // read.
1301 self.endpoint.note_object_forwarding_preference(
1302 header.track_alias(),
1303 ObjectForwardingPreference::Subgroup,
1304 )?;
1305 // The track is resolved here and not inside the stream: it takes the
1306 // endpoint's alias table, which a stream handle has no way back to.
1307 if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1308 framed.measure_objects_against(objects, header.group_id());
1309 }
1310 Ok((header, framed))
1311 }
1312
1313 /// Send an object via datagram.
1314 ///
1315 /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1316 /// header whose Object Status the framing it names cannot carry. Such a
1317 /// header errors here and nothing is sent, rather than going out as an
1318 /// ordinary payload datagram with the status quietly dropped.
1319 pub fn send_datagram(
1320 &self,
1321 header: &AnyDatagramHeader,
1322 payload: &[u8],
1323 ) -> Result<(), ConnectionError> {
1324 // Before anything is encoded, for the reason `open_subgroup_stream`
1325 // gives.
1326 self.endpoint.note_object_forwarding_preference(
1327 header.meta().track_alias,
1328 ObjectForwardingPreference::Datagram,
1329 )?;
1330 let mut buf = Vec::new();
1331 header.encode(&mut buf)?;
1332 buf.extend_from_slice(payload);
1333 self.emit(ClientEvent::DatagramReceived {
1334 direction: Direction::Send,
1335 header: header.clone(),
1336 payload_len: payload.len(),
1337 });
1338 self.transport.send_datagram(bytes::Bytes::from(buf))?;
1339 Ok(())
1340 }
1341
1342 /// Receive a datagram and decode its header.
1343 pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1344 let data = self.transport.recv_datagram().await?;
1345 let mut cursor = &data[..];
1346 let header = AnyDatagramHeader::decode(DraftVersion::Draft09, &mut cursor)?;
1347 let consumed = data.len() - cursor.len();
1348 let payload = data.slice(consumed..);
1349 self.emit(ClientEvent::DatagramReceived {
1350 direction: Direction::Receive,
1351 header: header.clone(),
1352 payload_len: payload.len(),
1353 });
1354 // A datagram is the other framing, and it settles the track's just as a
1355 // subgroup header does.
1356 self.endpoint.note_object_forwarding_preference(
1357 header.meta().track_alias,
1358 ObjectForwardingPreference::Datagram,
1359 )?;
1360 // A datagram is a whole object, so the connection can measure it
1361 // without help from the caller.
1362 let meta = header.meta();
1363 self.endpoint.note_received_object(
1364 meta.track_alias,
1365 ObjectLocation { group: meta.group_id, object: meta.object_id },
1366 object_role(meta.status),
1367 )?;
1368 Ok((header, payload))
1369 }
1370
1371 // ── Accessors ───────────────────────────────────────────
1372
1373 /// Access the underlying endpoint state machine.
1374 pub fn endpoint(&self) -> &Endpoint {
1375 &self.endpoint
1376 }
1377
1378 /// Mutable access to the endpoint state machine.
1379 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1380 &mut self.endpoint
1381 }
1382
1383 /// The SETUP message the server answered the handshake with.
1384 ///
1385 /// `SERVER_SETUP` through draft-16, the server's half of the unified
1386 /// `SETUP` from draft-17. [`AnyControlMessage::fields`] renders it under
1387 /// this draft's own parameter names, in the order they arrived.
1388 pub fn server_setup(&self) -> &AnyControlMessage {
1389 &self.server_setup
1390 }
1391
1392 /// The framed wire bytes of [`Self::server_setup`], as they arrived.
1393 ///
1394 /// Kept beside the decoded form because the encoding is evidence the
1395 /// decoding discards: two relays sending the same parameter can still
1396 /// disagree on how wide a varint they wrote it in.
1397 pub fn server_setup_raw(&self) -> Option<&[u8]> {
1398 self.server_setup_raw.as_deref()
1399 }
1400
1401 /// Get the negotiated MoQT version.
1402 pub fn negotiated_version(&self) -> Option<VarInt> {
1403 self.endpoint.negotiated_version()
1404 }
1405
1406 /// Which of this draft's *own* `ConnectionError` variants this error is,
1407 /// and which kind of thing it says.
1408 ///
1409 /// Draft-09 adds none. Every variant of its [`ConnectionError`] is one of the
1410 /// ten every draft carries, and [`AnyConnectionError`] classifies those
1411 /// itself — so `None` here is this draft's answer rather than a stub, and it
1412 /// stays right for exactly as long as that list does.
1413 ///
1414 /// Matched exhaustively, with no wildcard arm and deliberately so: a
1415 /// variant added to this draft's error type has to arrive here as a compile
1416 /// error, beside the doc comment that quotes the sentence it enforces,
1417 /// rather than as a silent [`ErrorCause::Unclassified`] in the facade.
1418 ///
1419 /// [`AnyConnectionError`]: crate::dispatch::AnyConnectionError
1420 /// [`ErrorCause::Unclassified`]: crate::dispatch::ErrorCause::Unclassified
1421 pub fn draft_specific_cause(
1422 err: &ConnectionError,
1423 ) -> Option<crate::above_codec_rules::DraftSpecificCause> {
1424 match err {
1425 ConnectionError::Endpoint(_)
1426 | ConnectionError::Codec(_)
1427 | ConnectionError::Transport(_)
1428 | ConnectionError::VarInt(_)
1429 | ConnectionError::NoControlStream
1430 | ConnectionError::UnexpectedEnd
1431 | ConnectionError::StreamFinished
1432 | ConnectionError::InvalidAddress(_)
1433 | ConnectionError::TlsConfig(_)
1434 | ConnectionError::DataStreamState(_) => None,
1435 // This build decoding a message and then failing to narrow it to
1436 // its own draft. Nothing reached the wire and no peer is
1437 // implicated, which is the whole reason it is not
1438 // `ConnectionError::Codec`: under that name it would carry
1439 // `Some(PROTOCOL_VIOLATION)` out of `codec_session_error_code` and
1440 // publish a relay for this build's defect. See the variant's own
1441 // doc.
1442 ConnectionError::ControlMessageNarrowing => {
1443 Some(crate::above_codec_rules::DraftSpecificCause::LocalRefusal)
1444 }
1445 }
1446 }
1447
1448 /// The code to close the session with when a message could not be decoded
1449 /// because the peer broke a rule draft-09 answers with a close.
1450 ///
1451 /// Every variant listed here comes from a sentence in this draft that names
1452 /// the consequence, and the list is per draft: answering a bound this draft
1453 /// does not state would close a session over traffic a conforming peer may
1454 /// send.
1455 ///
1456 /// - Track Namespace tuple size: "If an endpoint receives a Track
1457 /// Namespace tuple with an N of 0 or more than 32, it MUST close the
1458 /// session with a Protocol Violation." Note the lower bound — an empty
1459 /// tuple is refused here, where drafts 17 and later permit one.
1460 /// - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1461 /// check that there are no duplicate parameters and close the session as
1462 /// a 'Protocol Violation' if found." Unqualified on this draft, where
1463 /// later ones exempt a repeat the message authorizes.
1464 /// - Unknown control message type: "An endpoint that receives an unknown
1465 /// message type MUST close the session."
1466 /// - End of Track at a non-zero Object ID, Section 8.1.1.1: "An object
1467 /// with this status that has a Group ID less than or equal to any other
1468 /// Group ID, or an Object ID other than zero, is a protocol error, and
1469 /// the receiver MUST terminate the session." This one arrives on a data
1470 /// stream, so [`Connection::close_for_data_stream`] is what carries it.
1471 /// - A parameter whose value does not match the length its type implies —
1472 /// the one rule here that is **not** answered with a Protocol Violation:
1473 /// "the receiver MUST terminate the session with error code 'Parameter
1474 /// Length Mismatch'."
1475 ///
1476 /// **Not** the Full Track Name maximum, the Track Namespace byte cap, the
1477 /// zero-length namespace field, or the parameter value maximum. Those enter
1478 /// the specification at drafts 11 and 16 and this draft states none of them.
1479 ///
1480 /// **Not** [`CodecError::UnexpectedEnd`] either, which reports no rule at
1481 /// all: the reader raises it whenever a message is still arriving, and
1482 /// `read_control` loops on it. Closing over it would end a session on an
1483 /// ordinary short read.
1484 ///
1485 /// **Not** the key-value pair serialization rule. Drafts 11 and later
1486 /// require a close with KEY_VALUE_FORMATTING_ERROR when a value does not
1487 /// match the serialization its Type defines; this draft has no Key-Value
1488 /// Pair at all. What it states instead is the Parameter Length Mismatch
1489 /// rule above, over the Parameter framing it has in its place.
1490 ///
1491 /// **Not** the unknown Message Parameter rule, which enters at draft-16 and
1492 /// requires a close for a Message Parameter type the negotiated version does
1493 /// not define. This draft states nothing of the kind, and its parameters are
1494 /// not Key-Value-Pairs at all.
1495 ///
1496 /// `None` for everything else, including [`CodecError::InvalidField`]. That
1497 /// variant is shared by a dozen unrelated malformations, only some of which
1498 /// the draft answers with a close, so a session cannot be ended on it
1499 /// without ending sessions the draft does not ask to be ended. Splitting it
1500 /// is the way to bring the rest of those rules under this function; widening
1501 /// the match is not.
1502 pub fn codec_session_error_code(
1503 err: &CodecError,
1504 ) -> Option<moqtap_codec::draft09::error_codes::SessionErrorCode> {
1505 use moqtap_codec::draft09::error_codes::SessionErrorCode;
1506 use moqtap_codec::kvp::KvpError;
1507 match err {
1508 // The declared Length disagreeing with the fields, which every
1509 // draft answers with a close. Drafts 07 through 10 name no code for
1510 // it, so it takes the one their other unnamed rules take.
1511 CodecError::ControlMessageLengthMismatch { .. } => {
1512 Some(SessionErrorCode::ProtocolViolation)
1513 }
1514 CodecError::InvalidNamespaceTupleSize(_)
1515 | CodecError::DuplicateParameter(_)
1516 | CodecError::UnknownMessageType(_)
1517 | CodecError::EndOfTrackObjectId(_) => Some(SessionErrorCode::ProtocolViolation),
1518 // A Content Exists field that is neither zero nor one,
1519 // Section 7.15: "Any other value is a protocol error and
1520 // MUST terminate the session with a Protocol Violation".
1521 CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
1522 CodecError::ParameterLengthMismatch(_) => {
1523 Some(SessionErrorCode::ParameterLengthMismatch)
1524 }
1525 // An unknown data-plane type, Section 8: "An endpoint that
1526 // receives an unknown stream or datagram type MUST close the
1527 // session." One sentence covering two tables, which is why both
1528 // variants sit here.
1529 CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
1530 Some(SessionErrorCode::ProtocolViolation)
1531 }
1532 // Everything this draft does not answer, named rather than swept up
1533 // by a wildcard. The arm is exhaustive deliberately: a new
1534 // `CodecError` variant will not compile until it has been placed on
1535 // one side or the other, on this draft, which is the decision a `_`
1536 // arm makes silently and invisibly in every draft module at once.
1537 //
1538 // Adding one variant to `CodecError` produces an `E0004` in every
1539 // draft module that matches it exhaustively, each naming the
1540 // variant that has nowhere to go. That is the whole mechanism.
1541 //
1542 // The nesting stops at `VarInt`, whose variants report how the bytes
1543 // ran out rather than a rule an endpoint states, so there is nothing
1544 // in it for a draft to answer. `Kvp` is spelled out because it does
1545 // carry one.
1546 // Not `InvalidForward`: draft-09 has no Forward field. Not
1547 // `ParameterValueOutOfRange`: no parameter this draft defines
1548 // restricts its value's range. Both arrive with drafts above.
1549 CodecError::InvalidForward(_)
1550 | CodecError::ParameterValueOutOfRange { .. }
1551 | CodecError::UnexpectedEnd
1552 | CodecError::MessageTooLong(_)
1553 | CodecError::VarInt(_)
1554 | CodecError::InvalidField
1555 | CodecError::EmptyNamespaceField
1556 | CodecError::TrackNameTooLong
1557 | CodecError::InvalidRange(..)
1558 | CodecError::KeyDeltaOverflow(..)
1559 // Not `TrackPropertyValueOutOfRange`: this draft has neither
1560 // namespace the variant is about. Draft-16 opens an extension header
1561 // registry with value rules of its own, and draft-17 renames it to
1562 // the Track Property registry. Before that, everything with a
1563 // restricted range is either a message field or a Message Parameter.
1564 | CodecError::TrackPropertyValueOutOfRange { .. }
1565 | CodecError::ParametersOutOfOrder(..)
1566 | CodecError::ObjectIdOverflow(..)
1567 | CodecError::ExtensionsOnNonExistentObject(_)
1568 | CodecError::InvalidRequiredRequestIdDelta(..)
1569 | CodecError::InvalidStreamTypeValue { .. }
1570 | CodecError::InvalidDatagramTypeValue { .. }
1571 | CodecError::ReasonPhraseTooLong
1572 | CodecError::GoAwayUriTooLong
1573 | CodecError::KeyValueFormatting { .. }
1574 | CodecError::UnknownMessageParameter(_)
1575 // Not `ParameterOutOfScope`: this draft states the scope rule and
1576 // answers it the other way. Section 7.1.1 Version Specific Parameters: "Each
1577 // version-specific parameter definition indicates the message types in which it can
1578 // appear. If it appears in some other type of message, it MUST be
1579 // ignored." The codec carries such a parameter on this draft and never
1580 // raises the variant, so this arm records a rule this draft has and
1581 // does not close over, not one it is missing. Draft-17 is where the
1582 // second sentence becomes a close.
1583 | CodecError::ParameterOutOfScope { .. }
1584 // A Filter Type outside the set this draft assigns. Section 7.4
1585 // states the rule and stops there: "A filter type other than the
1586 // above MUST be treated as error." No code, no close, and no
1587 // sentence elsewhere in the draft that turns an error into one — so
1588 // the message is refused and the session stays open.
1589 // Draft-14 Section 9.7 is where the same sentence gained "MUST be
1590 // close the session with PROTOCOL_VIOLATION", and it is answered
1591 // there.
1592 //
1593 // The assigned set is not the same on every draft either: 07 and 08
1594 // assign 0x1 as Latest Group, 09 and 10 withdraw it, and 11 and
1595 // later reinstate it as Next Group Start. The decoder holds each
1596 // draft to its own list; this arm only decides what a refusal does
1597 // to the session.
1598 //
1599 // The two rules below belong to the parameter form of the filter,
1600 // which arrives at draft-15. This draft carries the Filter Type as a
1601 // field of SUBSCRIBE, so there is no parameter for either to be
1602 // about.
1603 | CodecError::InvalidFilterType(_)
1604 | CodecError::SubscriptionFilterMalformed { .. }
1605 | CodecError::FilterEndGroupOverflow { .. }
1606 // A Fetch Type outside the set this draft assigns. Section 7.7
1607 // states the rule and stops there: "A Fetch Type other than 0x1 or
1608 // 0x2 MUST be treated as an error", naming no code and no close,
1609 // exactly as this draft's Filter Type sentence does. The third
1610 // type, Absolute Joining, arrives at draft-11 and joins the
1611 // sentence with it. Draft-14 Section 9.16 is where both gained
1612 // "MUST be close the session with a PROTOCOL_VIOLATION", and it
1613 // answers them there.
1614 | CodecError::InvalidFetchType(_)
1615 // The object payload rule, Section 8.1.1.1: "Any object with a status
1616 // code other than zero MUST have an empty payload." A MUST on the
1617 // sender with no receiver action named anywhere — the "SHOULD be
1618 // treated as a protocol error" in the same paragraph belongs to the
1619 // sentence before it, which is about a status value this draft does
1620 // not assign — so an object carrying a payload it may not is refused
1621 // and the session stays open.
1622 | CodecError::PayloadNotPermitted { .. }
1623 | CodecError::UnsupportedDraft(_)
1624 | CodecError::Kvp(
1625 KvpError::ValueTooLong(_)
1626 | KvpError::MissingLength
1627 | KvpError::UnexpectedEnd
1628 | KvpError::VarInt(_),
1629 ) => None,
1630 }
1631 }
1632
1633 /// Close the session on the wire when a decode failure is one draft-09
1634 /// answers with a close, and hand the error back unchanged.
1635 /// Without it every bound the decoder enforces would stop at *this endpoint
1636 /// refused the frame* while the peer, which is the one that broke the rule,
1637 /// saw a session that was still open and went on sending. "MUST close the
1638 /// session" is a statement about the wire.
1639 fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
1640 if let ConnectionError::Codec(inner) = &err {
1641 if let Some(code) = Self::codec_session_error_code(inner) {
1642 // QUIC application error codes are 62-bit; every code in this
1643 // registry is far below `u32::MAX`, and saturating rather than
1644 // truncating means a future code that is not could never be
1645 // reported as a different, assigned one.
1646 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1647 self.close(wire_code, inner.to_string().as_bytes());
1648 }
1649 }
1650 err
1651 }
1652
1653 /// Close the session on the wire when the endpoint says a violation is
1654 /// fatal to it, and hand the error back unchanged.
1655 ///
1656 /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1657 /// errors this draft ends the session over, and the endpoint has already
1658 /// moved its own state machine to Closed by the time this runs. Without
1659 /// this step that move is purely internal: the local endpoint refuses to
1660 /// start anything new while the peer, which is the one that broke the
1661 /// rule, sees a session that is still open and goes on sending. A rule
1662 /// that names a session termination code is a statement about the wire,
1663 /// so it takes a CONNECTION_CLOSE to satisfy it.
1664 ///
1665 /// The reason phrase is the error's own `Display` text, which names the
1666 /// rule rather than repeating the numeric code the close already carries.
1667 ///
1668 /// Errors that answer `None` are recoverable and nothing is sent.
1669 fn close_for(&self, err: &EndpointError) {
1670 if let Some(code) = err.session_error_code() {
1671 // QUIC application error codes are 62-bit; every code in this
1672 // registry is far below `u32::MAX`, and saturating rather than
1673 // truncating means a future code that is not could never be
1674 // reported as a different, assigned one.
1675 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1676 self.close(wire_code, err.to_string().as_bytes());
1677 }
1678 }
1679
1680 /// [`close_for`](Self::close_for), then the error unchanged, for the
1681 /// common case where the endpoint's error is also what the caller returns.
1682 fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1683 self.close_for(&err);
1684 ConnectionError::Endpoint(err)
1685 }
1686
1687 /// Close the session over a rule broken on a data stream, reporting whether
1688 /// it did.
1689 ///
1690 /// A data stream cannot close for itself the way `recv_control` does:
1691 /// [`Connection::accept_subgroup_stream`] hands the caller a
1692 /// [`FramedRecvStream`] holding no connection, so the reader that finds the
1693 /// violation is not the object that can act on it. Keeping it a separate
1694 /// call is deliberate as well — a permissive caller, one reproducing a
1695 /// capture, can read a violating stream and report it without tearing the
1696 /// session down.
1697 ///
1698 /// The rule this draft answers here is the end-of-track Object ID, which
1699 /// arrives on a subgroup or fetch stream and nowhere else. It shares
1700 /// `codec_session_error_code` with the control path, so a rule is answered
1701 /// with one code whichever stream carried it.
1702 ///
1703 /// Not every rule that reaches here is the decoder's. A track whose objects
1704 /// mix forwarding preferences is the endpoint's to notice — it takes the
1705 /// alias table to know which track an object belongs to — and it arrives on
1706 /// exactly these streams. Both kinds are asked for a code the same way, and
1707 /// a rule with no code is declined rather than guessed at.
1708 pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
1709 match err {
1710 ConnectionError::Codec(inner) => {
1711 let Some(code) = Self::codec_session_error_code(inner) else { return false };
1712 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1713 self.close(wire_code, inner.to_string().as_bytes());
1714 true
1715 }
1716 // A rule the endpoint raises rather than the decoder. The two
1717 // reach their codes through different tables and mean the same
1718 // thing here: `Some` is a rule this draft ends the session over.
1719 ConnectionError::Endpoint(inner) => {
1720 let Some(code) = inner.session_error_code() else { return false };
1721 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1722 self.close(wire_code, inner.to_string().as_bytes());
1723 true
1724 }
1725 _ => false,
1726 }
1727 }
1728
1729 /// Close the connection.
1730 pub fn close(&self, code: u32, reason: &[u8]) {
1731 self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
1732 self.transport.close(code, reason);
1733 }
1734}
1735
1736/// Determine the encoded length of a varint from its first byte.
1737fn varint_len(first_byte: u8) -> usize {
1738 1 << (first_byte >> 6)
1739}
1740
1741#[cfg(test)]
1742mod tests {
1743 use super::*;
1744
1745 /// This build failing to narrow a message it decoded is never a finding
1746 /// about the peer.
1747 ///
1748 /// The arm that raises `ControlMessageNarrowing` is unreachable — this
1749 /// draft's decoder can only hand back this draft's variant — and nothing
1750 /// pins that. What is pinned here is the half that matters.
1751 /// `CodecError::UnknownMessageType(0)` is what the arm must not raise:
1752 /// `codec_session_error_code` answers it `Some(PROTOCOL_VIOLATION)` on
1753 /// every draft in range, so the day the narrowing failed a conformance
1754 /// probe would publish a relay for sending a control message type this
1755 /// draft does not assign — with `0x00` attached as the codepoint that
1756 /// proved it, which is an accusation better evidenced than any real one
1757 /// this build makes. The section stating that rule is numbered differently
1758 /// on every draft, and the point does not turn on the number.
1759 ///
1760 /// Ablated by putting the arm back to
1761 /// `ConnectionError::Codec(CodecError::UnknownMessageType(0))`: this test
1762 /// reddens on the cause, and so does the probe's own
1763 /// `violation::a_message_this_build_could_not_narrow_names_nobody`.
1764 #[test]
1765 fn a_message_this_build_could_not_narrow_names_nobody() {
1766 use crate::dispatch::{AnyConnectionError, ErrorCause};
1767
1768 let err: AnyConnectionError = ConnectionError::ControlMessageNarrowing.into();
1769 assert!(err.is_local(), "a narrowing this build could not do is this build's");
1770 assert_eq!(
1771 err.cause(),
1772 &ErrorCause::Facade,
1773 "nothing reached the wire, so there is no rule and no close code to read"
1774 );
1775 }
1776
1777 #[test]
1778 fn client_config_supported_versions_default() {
1779 let config = ClientConfig {
1780 additional_versions: Vec::new(),
1781 transport: TransportType::Quic,
1782 skip_cert_verification: false,
1783 ca_certs: Vec::new(),
1784 setup_parameters: Vec::new(),
1785 };
1786 let versions = config.supported_versions();
1787 assert_eq!(versions.len(), 1);
1788 assert_eq!(versions[0].into_inner(), 0xff000000 + 9);
1789 }
1790
1791 #[test]
1792 fn client_config_alpn_quic() {
1793 let config = ClientConfig {
1794 additional_versions: Vec::new(),
1795 transport: TransportType::Quic,
1796 skip_cert_verification: false,
1797 ca_certs: Vec::new(),
1798 setup_parameters: Vec::new(),
1799 };
1800 assert_eq!(config.alpn(), vec![DraftVersion::Draft09.quic_alpn().to_vec()]);
1801 }
1802
1803 #[test]
1804 fn moqt_alpn_value() {
1805 assert_eq!(MOQT_ALPN, b"moq-00");
1806 }
1807}