Skip to main content

moq_net/
client.rs

1use crate::origin;
2use crate::{
3	ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05,
4	ALPN_LITE_06_WIP, Consume, Driver, Error, NEGOTIATED, Session, Version, Versions,
5	coding::{self, Decode, Encode, Stream},
6	ietf, lite, setup, stats,
7};
8
9/// A MoQ client session builder.
10#[derive(Default, Clone)]
11pub struct Client {
12	publish: Option<origin::Consumer>,
13	subscribe: Option<origin::Producer>,
14	stats: stats::Session,
15	versions: Versions,
16	setup_path: Option<String>,
17	cost: Option<u64>,
18	peer_origin: Option<crate::Origin>,
19}
20
21impl Client {
22	/// A client that neither publishes nor subscribes until configured.
23	pub fn new() -> Self {
24		Default::default()
25	}
26
27	/// Publish local broadcasts to the remote: the session reads from the given
28	/// origin (pass an [`origin::Producer`] or [`origin::Consumer`] by reference) and
29	/// forwards its announcements. Omit to publish nothing.
30	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
31		self.publish = Some(publish.consume());
32		self
33	}
34
35	/// Subscribe to remote broadcasts: the session writes the broadcasts the
36	/// remote announces into this [`origin::Producer`]. Omit to subscribe to nothing.
37	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
38		self.subscribe = Some(subscribe);
39		self
40	}
41
42	/// Attach a per-connection [`stats::Session`] context. The session's publish
43	/// (egress) and subscribe (ingress) origin handles are tagged with it, so all
44	/// traffic counters are attributed through the model for this session's lifetime.
45	/// Pass [`stats::Session::default`] (a no-op context) to opt out.
46	pub fn with_stats(mut self, stats: stats::Session) -> Self {
47		self.stats = stats;
48		self
49	}
50
51	/// Set both publish and subscribe from one shared [`origin::Producer`].
52	///
53	/// Equivalent to [`with_publisher`](Self::with_publisher) and
54	/// [`with_subscriber`](Self::with_subscriber) with the same origin.
55	pub fn with_origin(self, origin: origin::Producer) -> Self {
56		self.with_publisher(&origin).with_subscriber(origin)
57	}
58
59	/// Restrict which protocol versions to offer, in preference order.
60	/// Defaults to every version this crate supports.
61	pub fn with_versions(mut self, versions: Versions) -> Self {
62		self.versions = versions;
63		self
64	}
65
66	/// Set the request path to advertise in the SETUP (moq-lite-05 and every
67	/// moq-transport draft we speak).
68	///
69	/// Only for transports that carry no request URI of their own (native QUIC, qmux
70	/// over TCP/TLS, unix sockets), so the server learns which path the client wants.
71	/// Append `?` and the URI query when there is one: that is how a credential in the
72	/// query (`?jwt=`) reaches the server.
73	/// Bindings that already carry a URI (WebTransport, qmux over WebSocket) convey
74	/// the path there and MUST NOT send this; a server is entitled to treat it as a
75	/// protocol violation. An empty path is equivalent to omitting it. Ignored by
76	/// versions with no in-band request path (lite 01-04).
77	pub fn with_path(mut self, path: impl Into<String>) -> Self {
78		self.setup_path = Some(path.into());
79		self
80	}
81
82	/// Price what subscribing from us costs, in the units the rest of the mesh uses
83	/// (moq-lite-06+, and `moqt-17`+ via the MoQ Cluster extension).
84	///
85	/// Declared in our SETUP; the peer adds it to the route cost of every announcement
86	/// we forward it, so routing prefers cheap paths over short ones. Use `0` for a
87	/// direction that should look free (a sibling in the same datacenter), and
88	/// something large for one that should be a last resort (metered egress). When we
89	/// declare nothing the peer charges `1`, which makes the cost track the hop count
90	/// and so reproduces plain shortest-path routing.
91	///
92	/// This prices one direction only. What *we* pay to pull from the peer is whatever
93	/// the peer declares, so an asymmetric link is described by each side setting its
94	/// own value, and a symmetric one by both setting the same.
95	pub fn with_cost(mut self, cost: u64) -> Self {
96		self.cost = Some(cost);
97		self
98	}
99
100	/// Assign an origin (hop) id to the peer, used whenever the peer doesn't declare
101	/// one itself.
102	///
103	/// Some relays never declare their identity: moq-lite peers without the hops
104	/// extension, and moq-transport peers that don't negotiate the MoQ Cluster
105	/// extension (or predate it, on `moqt-16` and earlier).
106	/// Broadcasts received from such a peer are normally attributed to the reserved
107	/// origin 0 ("unknown"), which identifies nothing: it never proves continuity,
108	/// so their advertisements neither splice nor survive a restart in place. This
109	/// knob pins a real identity instead, exactly as if the peer had declared it:
110	///
111	/// - broadcasts received from the peer carry `origin` in their hop chains, so
112	///   every session dialing the same relay (with the same id) resolves to one
113	///   route and loop checks can recognize it;
114	/// - broadcasts whose hop chain already contains `origin` are neither announced
115	///   nor served back to the peer, preventing an echo through a relay that does
116	///   no loop detection of its own.
117	///
118	/// An identity the peer does declare wins over this one.
119	pub fn with_peer_origin(mut self, origin: crate::Origin) -> Self {
120		self.peer_origin = Some(origin);
121		self
122	}
123
124	/// Perform the MoQ handshake, returning the [`Session`] and the [`Driver`] that
125	/// runs its protocol work. The driver must be polled (spawned or awaited) for
126	/// the session to make progress.
127	pub async fn connect<S: web_transport_trait::Session>(&self, session: S) -> Result<(Session, Driver), Error> {
128		if self.publish.is_none() && self.subscribe.is_none() {
129			tracing::warn!("not publishing or consuming anything");
130		}
131
132		// Tag the origin pair with the stats context: reads through the publish
133		// (egress) consumer and writes through the subscribe (ingress) producer are
134		// then attributed by the model. One shared context, so presence and viewer
135		// counts are never double-attributed across the two halves.
136		let publish = self.publish.clone().map(|origin| origin.with_stats(self.stats.clone()));
137		let subscribe = self
138			.subscribe
139			.clone()
140			.map(|origin| origin.with_stats(self.stats.clone()));
141
142		// An assigned peer identity means subscriptions from the peer resolve to a
143		// source whose hop chain excludes it, the same split-horizon rule applied
144		// when a peer declares its own id. Announce filtering is per-protocol and
145		// handled inside each publisher.
146		let publish = match self.peer_origin {
147			Some(peer) => publish.map(|origin| origin.excluding(peer)),
148			None => publish,
149		};
150
151		// If ALPN was used to negotiate the version, use the appropriate encoding.
152		// Default to IETF 14 if no ALPN was used and we'll negotiate the version later.
153		let (encoding, supported) = match session.protocol() {
154			Some(ALPN_19) => {
155				let v = self
156					.versions
157					.select(Version::Ietf(ietf::Version::Draft19))
158					.ok_or(Error::Version)?;
159
160				// Draft-17+: SETUP is exchanged by the connection driver.
161				let protocol = ietf::start(ietf::Config {
162					session: session.clone(),
163					setup: None,
164					request_id_max: None,
165					client: true,
166					publish: publish.clone(),
167					subscribe: subscribe.clone(),
168					peer_origin: self.peer_origin,
169					cost: self.cost,
170					version: ietf::Version::Draft19,
171					path: self.setup_path.clone(),
172					peer_setup_stream: None,
173					peer_cluster: None,
174				})?;
175
176				tracing::debug!(version = ?v, "connected");
177				return Ok(Session::new(session, v, None, protocol));
178			}
179			Some(ALPN_18) => {
180				let v = self
181					.versions
182					.select(Version::Ietf(ietf::Version::Draft18))
183					.ok_or(Error::Version)?;
184
185				// Draft-17+: SETUP is exchanged by the connection driver.
186				// We advertise the request path in our SETUP for URL-less transports.
187				let protocol = ietf::start(ietf::Config {
188					session: session.clone(),
189					setup: None,
190					request_id_max: None,
191					client: true,
192					publish: publish.clone(),
193					subscribe: subscribe.clone(),
194					peer_origin: self.peer_origin,
195					cost: self.cost,
196					version: ietf::Version::Draft18,
197					path: self.setup_path.clone(),
198					peer_setup_stream: None,
199					peer_cluster: None,
200				})?;
201
202				tracing::debug!(version = ?v, "connected");
203				return Ok(Session::new(session, v, None, protocol));
204			}
205			Some(ALPN_17) => {
206				let v = self
207					.versions
208					.select(Version::Ietf(ietf::Version::Draft17))
209					.ok_or(Error::Version)?;
210
211				// Draft-17+: SETUP is exchanged by the connection driver.
212				// We advertise the request path in our SETUP for URL-less transports.
213				let protocol = ietf::start(ietf::Config {
214					session: session.clone(),
215					setup: None,
216					request_id_max: None,
217					client: true,
218					publish: publish.clone(),
219					subscribe: subscribe.clone(),
220					peer_origin: self.peer_origin,
221					cost: self.cost,
222					version: ietf::Version::Draft17,
223					path: self.setup_path.clone(),
224					peer_setup_stream: None,
225					peer_cluster: None,
226				})?;
227
228				tracing::debug!(version = ?v, "connected");
229				return Ok(Session::new(session, v, None, protocol));
230			}
231			Some(ALPN_16) => {
232				let v = self
233					.versions
234					.select(Version::Ietf(ietf::Version::Draft16))
235					.ok_or(Error::Version)?;
236				(v, v.into())
237			}
238			Some(ALPN_15) => {
239				let v = self
240					.versions
241					.select(Version::Ietf(ietf::Version::Draft15))
242					.ok_or(Error::Version)?;
243				(v, v.into())
244			}
245			Some(ALPN_14) => {
246				let v = self
247					.versions
248					.select(Version::Ietf(ietf::Version::Draft14))
249					.ok_or(Error::Version)?;
250				(v, v.into())
251			}
252			Some(alpn @ (ALPN_LITE_05 | ALPN_LITE_06_WIP)) => {
253				let version = match alpn {
254					ALPN_LITE_06_WIP => lite::Version::Lite06Wip,
255					_ => lite::Version::Lite05,
256				};
257				self.versions.select(Version::Lite(version)).ok_or(Error::Version)?;
258
259				// Advertise our capabilities (we report send bitrate; we don't pad) plus
260				// the request path on URI-less transports, and the direction we intend to
261				// use so the server can reject a token that lacks the matching scope during
262				// the handshake instead of silently carrying no media.
263				let our_setup = lite::Setup {
264					probe: lite::ProbeLevel::Report,
265					path: self.setup_path.clone(),
266					role: lite::Role::from_origins(self.publish.is_some(), self.subscribe.is_some()),
267					cost: self.cost,
268					// Filled by `lite::start` from the attached origin handles.
269					origin: None,
270				};
271
272				let start = lite::start(
273					session.clone(),
274					None,
275					publish.clone(),
276					subscribe.clone(),
277					self.peer_origin,
278					version,
279					our_setup,
280					None,
281				)?;
282
283				// Block until the initial announce set has landed (Lite05+ reports it
284				// via AnnounceOk + N), so a `request_broadcast()` for a live path resolves
285				// immediately instead of racing announcement gossip.
286				let (session, mut driver) = Session::new(session, version.into(), start.recv_bandwidth, start.driver);
287				driver.wait_ready(|waiter| start.connecting.poll_ready(waiter)).await;
288
289				return Ok((session, driver));
290			}
291			Some(ALPN_LITE_04) => {
292				self.versions
293					.select(Version::Lite(lite::Version::Lite04))
294					.ok_or(Error::Version)?;
295
296				let start = lite::start(
297					session.clone(),
298					None,
299					publish.clone(),
300					subscribe.clone(),
301					self.peer_origin,
302					lite::Version::Lite04,
303					lite::Setup::default(),
304					None,
305				)?;
306
307				// Lite04 has no initial-set boundary, so this resolves immediately.
308				let (session, mut driver) = Session::new(
309					session,
310					lite::Version::Lite04.into(),
311					start.recv_bandwidth,
312					start.driver,
313				);
314				driver.wait_ready(|waiter| start.connecting.poll_ready(waiter)).await;
315
316				return Ok((session, driver));
317			}
318			Some(ALPN_LITE_03) => {
319				self.versions
320					.select(Version::Lite(lite::Version::Lite03))
321					.ok_or(Error::Version)?;
322
323				// Starting with draft-03, there's no more SETUP control stream.
324				let start = lite::start(
325					session.clone(),
326					None,
327					publish.clone(),
328					subscribe.clone(),
329					self.peer_origin,
330					lite::Version::Lite03,
331					lite::Setup::default(),
332					None,
333				)?;
334
335				// Lite03 has no initial-set boundary, so this resolves immediately.
336				let (session, mut driver) = Session::new(
337					session,
338					lite::Version::Lite03.into(),
339					start.recv_bandwidth,
340					start.driver,
341				);
342				driver.wait_ready(|waiter| start.connecting.poll_ready(waiter)).await;
343
344				return Ok((session, driver));
345			}
346			Some(ALPN_LITE) | None => {
347				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
348				(Version::Ietf(ietf::Version::Draft14), supported)
349			}
350			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
351		};
352
353		let mut stream = Stream::open(&session, encoding).await?;
354
355		// The encoding is always an IETF version for SETUP negotiation.
356		let ietf_encoding = ietf::Version::try_from(encoding).map_err(|_| Error::Version)?;
357
358		let mut parameters = ietf::Parameters::default();
359		parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
360		parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
361		// Advertise the request path in-band (draft 14-16), same as the lite-05 SETUP.
362		if let Some(path) = &self.setup_path {
363			parameters.set_bytes(ietf::ParameterBytes::Path, path.clone().into_bytes());
364		}
365		let parameters = parameters.encode_bytes(ietf_encoding)?;
366
367		let client = setup::Client {
368			versions: supported.clone().into(),
369			parameters,
370		};
371
372		stream.writer.encode(&client).await?;
373
374		let mut server: setup::Server = stream.reader.decode().await?;
375
376		let version = supported
377			.iter()
378			.find(|v| coding::Version::from(**v) == server.version)
379			.copied()
380			.ok_or(Error::Version)?;
381
382		let (recv_bw, protocol, connecting) = match version {
383			Version::Lite(v) => {
384				let stream = stream.with_version(v);
385				let start = lite::start(
386					session.clone(),
387					Some(stream),
388					publish.clone(),
389					subscribe.clone(),
390					self.peer_origin,
391					v,
392					// This path only handles versions negotiated via the bidi SETUP exchange
393					// (pre-lite-05), which have no Setup Stream.
394					lite::Setup::default(),
395					None,
396				)?;
397
398				(start.recv_bandwidth, start.driver, Some(start.connecting))
399			}
400			Version::Ietf(v) => {
401				// Decode the parameters to get the initial request ID.
402				let parameters = ietf::Parameters::decode(&mut server.parameters, v)?;
403				let request_id_max = parameters
404					.get_varint(ietf::ParameterVarInt::MaxRequestId)
405					.map(ietf::RequestId);
406
407				let stream = stream.with_version(v);
408				// Draft 14-16: the path rode in the bidi SETUP above, not the uni one.
409				let protocol = ietf::start(ietf::Config {
410					session: session.clone(),
411					setup: Some(stream),
412					request_id_max,
413					client: true,
414					publish: publish.clone(),
415					subscribe: subscribe.clone(),
416					peer_origin: self.peer_origin,
417					cost: self.cost,
418					version: v,
419					path: None,
420					peer_setup_stream: None,
421					peer_cluster: None,
422				})?;
423				(None, protocol, None)
424			}
425		};
426
427		let (session, mut driver) = Session::new(session, version, recv_bw, protocol);
428		if let Some(connecting) = connecting {
429			// Block until the initial announce set has landed (for versions that
430			// report one); resolves immediately otherwise.
431			driver.wait_ready(|waiter| connecting.poll_ready(waiter)).await;
432		}
433
434		Ok((session, driver))
435	}
436}
437
438#[cfg(test)]
439mod tests {
440	use super::*;
441	use std::{
442		collections::VecDeque,
443		sync::{Arc, Mutex},
444	};
445
446	use crate::coding::{Decode, Encode};
447	use bytes::{BufMut, Bytes};
448
449	#[derive(Debug, Clone, Default)]
450	struct FakeError;
451
452	impl std::fmt::Display for FakeError {
453		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454			write!(f, "fake transport error")
455		}
456	}
457
458	impl std::error::Error for FakeError {}
459
460	impl web_transport_trait::Error for FakeError {
461		fn session_error(&self) -> Option<(u32, String)> {
462			Some((0, "closed".to_string()))
463		}
464	}
465
466	#[derive(Clone, Default)]
467	struct FakeSession {
468		state: Arc<FakeSessionState>,
469	}
470
471	#[derive(Default)]
472	struct FakeSessionState {
473		protocol: Option<&'static str>,
474		control_stream: Mutex<Option<(FakeSendStream, FakeRecvStream)>>,
475		close_events: Mutex<Vec<(u32, String)>>,
476		close_notify: tokio::sync::Notify,
477		control_writes: Arc<Mutex<Vec<u8>>>,
478		send_rate: Mutex<Option<u64>>,
479	}
480
481	impl FakeSession {
482		fn new(protocol: Option<&'static str>, server_control_bytes: Vec<u8>) -> Self {
483			let writes = Arc::new(Mutex::new(Vec::new()));
484			let send = FakeSendStream { writes: writes.clone() };
485			let recv = FakeRecvStream {
486				data: VecDeque::from(server_control_bytes),
487			};
488			let state = FakeSessionState {
489				protocol,
490				control_stream: Mutex::new(Some((send, recv))),
491				close_events: Mutex::new(Vec::new()),
492				close_notify: tokio::sync::Notify::new(),
493				control_writes: writes,
494				send_rate: Mutex::new(None),
495			};
496			Self { state: Arc::new(state) }
497		}
498
499		fn set_send_rate(&self, rate: Option<u64>) {
500			*self.state.send_rate.lock().unwrap() = rate;
501		}
502
503		fn control_writes(&self) -> Vec<u8> {
504			self.state.control_writes.lock().unwrap().clone()
505		}
506
507		async fn wait_for_first_close(&self) -> (u32, String) {
508			loop {
509				let notified = self.state.close_notify.notified();
510				if let Some(close) = self.state.close_events.lock().unwrap().first().cloned() {
511					return close;
512				}
513				notified.await;
514			}
515		}
516	}
517
518	impl web_transport_trait::Session for FakeSession {
519		type SendStream = FakeSendStream;
520		type RecvStream = FakeRecvStream;
521		type Error = FakeError;
522
523		async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
524			std::future::pending().await
525		}
526
527		async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
528			std::future::pending().await
529		}
530
531		async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
532			self.state.control_stream.lock().unwrap().take().ok_or(FakeError)
533		}
534
535		async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
536			std::future::pending().await
537		}
538
539		fn send_datagram(&self, _payload: Bytes) -> Result<(), Self::Error> {
540			Ok(())
541		}
542
543		async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
544			std::future::pending().await
545		}
546
547		fn max_datagram_size(&self) -> usize {
548			1200
549		}
550
551		fn protocol(&self) -> Option<&str> {
552			self.state.protocol
553		}
554
555		fn close(&self, code: u32, reason: &str) {
556			self.state.close_events.lock().unwrap().push((code, reason.to_string()));
557			self.state.close_notify.notify_waiters();
558		}
559
560		async fn closed(&self) -> Self::Error {
561			loop {
562				let notified = self.state.close_notify.notified();
563				if !self.state.close_events.lock().unwrap().is_empty() {
564					return FakeError;
565				}
566				notified.await;
567			}
568		}
569
570		fn stats(&self) -> impl web_transport_trait::Stats {
571			FakeStats {
572				send_rate: *self.state.send_rate.lock().unwrap(),
573			}
574		}
575	}
576
577	struct FakeStats {
578		send_rate: Option<u64>,
579	}
580
581	impl web_transport_trait::Stats for FakeStats {
582		fn estimated_send_rate(&self) -> Option<u64> {
583			self.send_rate
584		}
585	}
586
587	#[derive(Clone, Default)]
588	struct FakeSendStream {
589		writes: Arc<Mutex<Vec<u8>>>,
590	}
591
592	impl web_transport_trait::SendStream for FakeSendStream {
593		type Error = FakeError;
594
595		async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
596			self.writes.lock().unwrap().put_slice(buf);
597			Ok(buf.len())
598		}
599
600		fn set_priority(&mut self, _order: u8) {}
601
602		fn finish(&mut self) -> Result<(), Self::Error> {
603			Ok(())
604		}
605
606		fn reset(&mut self, _code: u32) {}
607
608		async fn closed(&mut self) -> Result<(), Self::Error> {
609			Ok(())
610		}
611	}
612
613	struct FakeRecvStream {
614		data: VecDeque<u8>,
615	}
616
617	impl web_transport_trait::RecvStream for FakeRecvStream {
618		type Error = FakeError;
619
620		async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
621			if self.data.is_empty() {
622				return Ok(None);
623			}
624
625			let size = dst.len().min(self.data.len());
626			for slot in dst.iter_mut().take(size) {
627				*slot = self.data.pop_front().unwrap();
628			}
629			Ok(Some(size))
630		}
631
632		fn stop(&mut self, _code: u32) {}
633
634		async fn closed(&mut self) -> Result<(), Self::Error> {
635			Ok(())
636		}
637	}
638
639	fn mock_server_setup(negotiated: Version) -> Vec<u8> {
640		let mut encoded = Vec::new();
641		let server = setup::Server {
642			version: negotiated.into(),
643			parameters: Bytes::new(),
644		};
645		server
646			.encode(&mut encoded, Version::Ietf(ietf::Version::Draft14))
647			.unwrap();
648
649		// Add a setup-stream SessionInfo frame using the negotiated Lite version.
650		let info = lite::SessionInfo { bitrate: Some(1) };
651		let lite_v = lite::Version::try_from(negotiated).unwrap();
652		info.encode(&mut encoded, lite_v).unwrap();
653
654		encoded
655	}
656
657	async fn run_alpn_lite_fallback_case(protocol: Option<&'static str>) {
658		let fake = FakeSession::new(protocol, mock_server_setup(Version::Lite(lite::Version::Lite01)));
659		let client = Client::new().with_versions(
660			[
661				Version::Lite(lite::Version::Lite03),
662				Version::Lite(lite::Version::Lite02),
663				Version::Lite(lite::Version::Lite01),
664				Version::Ietf(ietf::Version::Draft14),
665			]
666			.into(),
667		);
668
669		let _connection = client.connect(fake.clone()).await.unwrap();
670
671		// Verify the client setup was encoded using Draft14 framing (ALPN_LITE fallback path).
672		let mut setup_bytes = Bytes::from(fake.control_writes());
673		let setup = setup::Client::decode(&mut setup_bytes, Version::Ietf(ietf::Version::Draft14)).unwrap();
674		let advertised: Vec<Version> = setup.versions.iter().map(|v| Version::try_from(*v).unwrap()).collect();
675		assert_eq!(
676			advertised,
677			vec![
678				Version::Lite(lite::Version::Lite02),
679				Version::Lite(lite::Version::Lite01),
680				Version::Ietf(ietf::Version::Draft14),
681			]
682		);
683
684		// The first close comes from the lite connection driver.
685		// Any non-Version error here means SessionInfo decoded successfully
686		// after set_version(). This test cares about the SETUP framing
687		// fallback, not the specific close code. Cancel is what we'd see
688		// with no origin; RequiredExtension (or similar) is what an
689		// auto-created origin's first interaction with a Lite01 peer trips.
690		let (code, _) = fake.wait_for_first_close().await;
691		assert_ne!(code, Error::Version.to_code(), "SessionInfo failed to decode");
692	}
693
694	#[tokio::test(start_paused = true)]
695	async fn alpn_lite_falls_back_to_draft14_and_switches_version_post_setup() {
696		run_alpn_lite_fallback_case(Some(ALPN_LITE)).await;
697	}
698
699	#[tokio::test(start_paused = true)]
700	async fn no_alpn_falls_back_to_draft14_and_switches_version_post_setup() {
701		run_alpn_lite_fallback_case(None).await;
702	}
703
704	// This fake reports no send-rate estimate, so it never reaches the tokio timer in
705	// the bandwidth loop. A driver is NOT runtime-free in general; see the Async
706	// docs in lib.rs.
707	//
708	// The driver must hold no Session clone (the #2286 leak), so the transport still
709	// closes when the caller drops their last session handle, which is what lets a
710	// spawned driver task finish.
711	#[test]
712	fn driver_is_caller_polled_and_holds_no_session() {
713		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
714		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
715
716		let (session, mut driver) = futures::executor::block_on(client.connect(fake.clone())).unwrap();
717		assert_eq!(session.version(), Version::Lite(lite::Version::Lite04));
718
719		// An arbitrary waiter drives it kio-style: nothing was spawned onto a runtime.
720		assert!(driver.poll(&kio::Waiter::noop()).is_pending());
721
722		// The driver is also a plain future (stand in for spawning it).
723		let mut context = std::task::Context::from_waker(std::task::Waker::noop());
724		assert!(std::future::Future::poll(std::pin::Pin::new(&mut driver), &mut context).is_pending());
725
726		// The caller drops their only session clone, so the transport closes even
727		// though the driver is still alive.
728		drop(session);
729		assert_eq!(fake.state.close_events.lock().unwrap()[0].0, Error::Cancel.to_code());
730	}
731
732	// Clones share the connection: the transport closes on the LAST drop, and
733	// abort() closes it explicitly (first close wins).
734	#[test]
735	fn session_clones_share_the_close() {
736		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
737		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
738
739		let (session, _driver) = futures::executor::block_on(client.connect(fake.clone())).unwrap();
740		let clone = session.clone();
741
742		// One clone dropping does nothing while another is alive.
743		drop(session);
744		assert!(fake.state.close_events.lock().unwrap().is_empty());
745
746		clone.abort(Error::Cancel);
747		assert_eq!(fake.state.close_events.lock().unwrap()[0].0, Error::Cancel.to_code());
748
749		// The final drop is a no-op thanks to close-once.
750		drop(clone);
751		assert_eq!(fake.state.close_events.lock().unwrap().len(), 1);
752	}
753
754	// The send-bandwidth sampler lives inside the driver: it samples as soon as a
755	// consumer exists and keeps sampling on its interval. Paused tokio time makes
756	// the interval fire deterministically.
757	#[tokio::test(start_paused = true)]
758	async fn send_bandwidth_samples_while_the_driver_runs() {
759		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
760		fake.set_send_rate(Some(1_000_000));
761
762		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
763		let (session, driver) = client.connect(fake.clone()).await.unwrap();
764		tokio::spawn(driver);
765
766		let mut bandwidth = session.send_bandwidth().expect("backend reports an estimate");
767		assert_eq!(bandwidth.changed().await.unwrap(), Some(1_000_000));
768
769		// A later change is picked up by the next interval tick.
770		fake.set_send_rate(Some(2_000_000));
771		assert_eq!(bandwidth.changed().await.unwrap(), Some(2_000_000));
772	}
773}