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