Skip to main content

moq_net/lite/
setup.rs

1//! The lite-05+ SETUP message: each endpoint advertises its capabilities once, as
2//! the sole message on a unidirectional Setup Stream, then closes it.
3
4use crate::coding::*;
5
6use super::{Message, Parameters, Version};
7
8/// Setup Parameter id for the Probe capability level.
9const PARAM_PROBE: u64 = 0x1;
10/// Setup Parameter id for the request Path (client-only, URI-less transports).
11const PARAM_PATH: u64 = 0x2;
12/// Setup Parameter id for the client's intended [`Role`] (client-only).
13const PARAM_ROLE: u64 = 0x3;
14/// Setup Parameter id for the link cost the dialer assigns to this connection.
15const PARAM_COST: u64 = 0x4;
16/// Setup Parameter id for the endpoint's Hop ID.
17const PARAM_HOP: u64 = 0x5;
18
19/// The cost of crossing a link that neither end priced.
20///
21/// One, so a mesh that configures no costs accumulates a route cost equal to the
22/// hop count and ranks routes exactly as pre-lite-06 shortest-path routing did. Pricing
23/// a link at 0 makes it free (a sibling in the same datacenter); pricing it higher
24/// makes it a last resort (a metered backbone).
25pub const DEFAULT_COST: u64 = 1;
26
27/// The probe capability an endpoint advertises in SETUP.
28///
29/// Monotonic: a higher level implies every lower one. An unknown (future) value
30/// decodes as the highest level we understand, so a peer that gains a new level is
31/// treated as at least [`Increase`](Self::Increase).
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
33pub enum ProbeLevel {
34	/// No probing. Equivalent to omitting the parameter.
35	#[default]
36	None,
37	/// The publisher can measure and periodically report at least one of the PROBE
38	/// metrics: its estimated bitrate, its round-trip time, or both. Either may be
39	/// unknown in any given report, since the two are independent on the wire.
40	Report,
41	/// The publisher can additionally pad the connection (or send redundant data).
42	Increase,
43}
44
45impl ProbeLevel {
46	/// The level to advertise for `session`, from what its transport actually exposes.
47	///
48	/// [`Report`](Self::Report) claims the publisher can measure and periodically
49	/// report. A transport that exposes neither a send-rate estimate nor an RTT can
50	/// honour neither, and the draft requires such a publisher to reset any Probe
51	/// Stream a subscriber opens. Advertising [`None`](Self::None) instead stops the
52	/// subscriber opening one at all.
53	///
54	/// Both metrics are sampled rather than declared, so this only works for a
55	/// transport whose figures exist by the time the session starts. QUIC and TCP
56	/// both qualify: their RTT comes from the handshake, which has already happened.
57	pub fn detect<S: crate::transport::poll::Session>(session: &S) -> Self {
58		use web_transport_trait::Stats as _;
59		let stats = session.stats();
60		match stats.estimated_send_rate().is_some() || stats.rtt().is_some() {
61			true => Self::Report,
62			false => Self::None,
63		}
64	}
65
66	/// Map the wire value to a level, saturating unknown values to [`Increase`](Self::Increase).
67	fn from_code(code: u64) -> Self {
68		match code {
69			0 => Self::None,
70			1 => Self::Report,
71			_ => Self::Increase,
72		}
73	}
74
75	/// The wire value for this level.
76	fn to_code(self) -> u64 {
77		match self {
78			Self::None => 0,
79			Self::Report => 1,
80			Self::Increase => 2,
81		}
82	}
83}
84
85/// The single direction a client intends to use the session for.
86///
87/// A client advertises this in its SETUP so the server can reject a token that lacks
88/// the matching scope during the handshake, instead of accepting a connection that
89/// then silently carries no media (a subscribe-only token used to publish, or vice
90/// versa). It only ever narrows what the server grants, so it is not a security
91/// boundary: the server still enforces the token's scope regardless.
92///
93/// A session is bidirectional by default, which the wire says by omitting the
94/// parameter. `Option<Role>` mirrors that: `None` is the default, and it's also what
95/// a client that predates the parameter decodes to.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum Role {
99	/// The client will publish tracks (ingest); the server must consume.
100	Publisher,
101	/// The client will subscribe to tracks (egress); the server must publish.
102	Subscriber,
103}
104
105impl Role {
106	/// Map the wire value to a role. `0` and any unrecognized future value are `None`
107	/// (bidirectional): the draft requires a receiver that does not recognize the value
108	/// to treat it as both directions, so a newer client can't break an older server (it
109	/// just loses the early reject and defers fully to the token's scope).
110	fn from_code(code: u64) -> Option<Self> {
111		match code {
112			1 => Some(Role::Publisher),
113			2 => Some(Role::Subscriber),
114			_ => None,
115		}
116	}
117
118	/// The wire value for this role.
119	fn to_code(self) -> u64 {
120		match self {
121			Role::Publisher => 1,
122			Role::Subscriber => 2,
123		}
124	}
125
126	/// Derive the advertised role from which origins a client wired up: publish-only is
127	/// a [`Publisher`](Role::Publisher), consume-only a [`Subscriber`](Role::Subscriber),
128	/// and both (or neither) advertises nothing. This keeps the advertised role from
129	/// drifting away from what the session actually does.
130	pub(crate) fn from_origins(publishes: bool, consumes: bool) -> Option<Self> {
131		match (publishes, consumes) {
132			(true, false) => Some(Role::Publisher),
133			(false, true) => Some(Role::Subscriber),
134			_ => None,
135		}
136	}
137
138	/// Lowercase label for this role (`"publisher"` / `"subscriber"`).
139	pub fn as_str(self) -> &'static str {
140		match self {
141			Role::Publisher => "publisher",
142			Role::Subscriber => "subscriber",
143		}
144	}
145}
146
147impl std::fmt::Display for Role {
148	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149		f.write_str(self.as_str())
150	}
151}
152
153/// The SETUP message, sent once per endpoint on the unidirectional Setup Stream.
154///
155/// lite-05+ only. The two endpoints' SETUP messages are independent: neither side
156/// blocks on the peer's before opening other streams, but a stream whose encoding
157/// depends on a negotiated capability (e.g. PROBE) must wait for it.
158#[derive(Clone, Default, PartialEq, Eq)]
159pub struct Setup {
160	/// The probe capability this endpoint supports. [`ProbeLevel::None`] when absent.
161	pub probe: ProbeLevel,
162	/// The request path, for transports that carry no request URI (native QUIC,
163	/// qmux over TCP/TLS, unix sockets), with `?` and the URI query appended when
164	/// there is one. Sent only by the client; a server never sends one and a relay
165	/// never forwards it. `None` on URI-carrying bindings, where it would be a
166	/// protocol violation. An empty path means the same thing as `None`; both are
167	/// on the wire so a client need not special-case the root.
168	pub path: Option<String>,
169	/// The single direction the client intends to use, or `None` for a bidirectional
170	/// session. `None` is sent as the absence of the parameter, which is also how a
171	/// client that predates the parameter decodes.
172	pub role: Option<Role>,
173	/// What subscribing from this endpoint costs (lite-06+), added by the peer to the
174	/// route cost of every announcement we forward it.
175	///
176	/// Directional: it prices the sender's own egress, so both ends declare their own
177	/// and the two need not match. `None` means the default cost of 1.
178	pub cost: Option<u64>,
179	/// This endpoint's Hop ID, the identity it stamps onto forwarded
180	/// announcements. The peer uses it to serve this endpoint's subscriptions from
181	/// a route that does not flow through it (the same split horizon the announce
182	/// filter applies). `None` when the endpoint has no meaningful identity (a
183	/// leaf that never forwards); a wire value of 0 decodes as `None`.
184	pub hop: Option<crate::Hop>,
185}
186
187impl std::fmt::Debug for Setup {
188	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189		f.debug_struct("Setup")
190			.field("probe", &self.probe)
191			// Native transports carry the JWT in this request target.
192			.field(
193				"path",
194				&self.path.as_deref().map(|path| path.split('?').next().unwrap()),
195			)
196			.field("role", &self.role)
197			.field("cost", &self.cost)
198			.field("hop", &self.hop)
199			.finish()
200	}
201}
202
203impl Message for Setup {
204	const CONTROL: Option<crate::stats::Control> = Some(crate::stats::Control::Session);
205	const MAX_SIZE: usize = crate::setup::MAX_SETUP_SIZE;
206
207	fn decode_msg<R: bytes::Buf>(r: &mut R, version: Version) -> Result<Self, DecodeError> {
208		if !version.has_setup_stream() {
209			return Err(DecodeError::Version);
210		}
211
212		let params = Parameters::decode(r, version)?;
213		let probe = params
214			.get_varint(PARAM_PROBE, version)?
215			.map(ProbeLevel::from_code)
216			.unwrap_or_default();
217		let path = match params.get_bytes(PARAM_PATH) {
218			Some(bytes) => Some(
219				std::str::from_utf8(bytes)
220					.map_err(|_| DecodeError::InvalidValue)?
221					.to_string(),
222			),
223			None => None,
224		};
225		let role = params.get_varint(PARAM_ROLE, version)?.and_then(Role::from_code);
226		let cost = params.get_varint(PARAM_COST, version)?;
227		// 0 is legal on the wire but carries no identity (it can't be excluded),
228		// so it decodes as "not declared" rather than an error.
229		let hop = params
230			.get_varint(PARAM_HOP, version)?
231			.and_then(|id| crate::Hop::new(id).ok());
232
233		Ok(Self {
234			probe,
235			path,
236			role,
237			cost,
238			hop,
239		})
240	}
241
242	fn encode_msg<W: bytes::BufMut>(&self, w: &mut W, version: Version) -> Result<(), EncodeError> {
243		if !version.has_setup_stream() {
244			return Err(EncodeError::Version);
245		}
246
247		let mut params = Parameters::default();
248		// None is the wire default, so omit it to keep the message empty when nothing is set.
249		if self.probe != ProbeLevel::None {
250			params.set_varint(PARAM_PROBE, self.probe.to_code(), version)?;
251		}
252		if let Some(path) = &self.path {
253			params.set_bytes(PARAM_PATH, path.as_bytes().to_vec());
254		}
255		// Bidirectional is the wire default (absence of the parameter), so only a
256		// directional role is encoded.
257		if let Some(role) = self.role {
258			params.set_varint(PARAM_ROLE, role.to_code(), version)?;
259		}
260		if let Some(cost) = self.cost {
261			params.set_varint(PARAM_COST, cost, version)?;
262		}
263		if let Some(hop) = self.hop {
264			params.set_varint(PARAM_HOP, hop.id(), version)?;
265		}
266
267		params.encode(w, version)
268	}
269}
270
271/// Shared slot for the peer's SETUP, written once when its Setup stream is read.
272///
273/// Streams whose encoding depends on a negotiated capability (e.g. the PROBE
274/// stream) wait on this before deciding what to do. Cheap to clone: every handle
275/// shares the same slot.
276#[derive(Clone, Default)]
277pub(crate) struct PeerSetup(kio::Shared<PeerSetupState>);
278
279#[derive(Default)]
280struct PeerSetupState {
281	seen: bool,
282	setup: Option<Setup>,
283}
284
285impl PeerSetup {
286	/// Record the peer's SETUP.
287	pub fn set(&self, setup: Setup) {
288		let mut state = self.0.lock();
289		state.seen = true;
290		state.setup = Some(setup);
291	}
292
293	/// Claim the Setup Stream before decoding its body, refusing any second stream.
294	pub fn claim(&self) -> Result<(), crate::Error> {
295		let mut state = self.0.lock();
296		if state.seen {
297			return Err(crate::Error::ProtocolViolation);
298		}
299		state.seen = true;
300		Ok(())
301	}
302
303	/// Poll for the peer's advertised probe level, waiting until its SETUP arrives.
304	pub fn poll_probe_level(&self, waiter: &kio::Waiter) -> std::task::Poll<ProbeLevel> {
305		self.poll_get(waiter, |setup| setup.probe)
306	}
307
308	/// Poll for the link cost the peer (the dialing side) declared in its SETUP.
309	/// `None` when it declared none, meaning the default cost of 1.
310	pub fn poll_cost(&self, waiter: &kio::Waiter) -> std::task::Poll<Option<u64>> {
311		self.poll_get(waiter, |setup| setup.cost)
312	}
313
314	/// Poll for the [`Hop`](crate::Hop) id the peer declared in its SETUP `Hop`
315	/// parameter. `None` when it declared none: a leaf with no identity worth excluding.
316	pub fn poll_hop(&self, waiter: &kio::Waiter) -> std::task::Poll<Option<crate::Hop>> {
317		self.poll_get(waiter, |setup| setup.hop)
318	}
319
320	/// Poll for a field of the peer's SETUP.
321	///
322	/// The peer MUST send exactly one SETUP, so this resolves once that stream is read.
323	/// Pends forever if it never does; the caller is session state, dropped with the
324	/// driver.
325	fn poll_get<T>(&self, waiter: &kio::Waiter, f: impl FnOnce(&Setup) -> T) -> std::task::Poll<T> {
326		let slot = std::task::ready!(self.0.poll(waiter, |setup| {
327			if setup.setup.is_some() {
328				std::task::Poll::Ready(())
329			} else {
330				std::task::Poll::Pending
331			}
332		}));
333		std::task::Poll::Ready(f(slot.setup.as_ref().expect("waited for Some")))
334	}
335}
336
337#[cfg(test)]
338mod tests {
339	use super::*;
340
341	fn round_trip(msg: &Setup) -> Setup {
342		let mut buf = bytes::BytesMut::new();
343		msg.encode(&mut buf, Version::Lite05).unwrap();
344		let mut slice = &buf[..];
345		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
346		assert!(bytes::Buf::remaining(&slice) == 0, "trailing bytes after decode");
347		got
348	}
349
350	#[test]
351	fn empty_round_trip() {
352		let msg = Setup::default();
353		assert_eq!(round_trip(&msg), msg);
354	}
355
356	/// A transport exposing neither metric can't honour a `Report` claim, and the
357	/// draft makes such a publisher reset any Probe Stream a subscriber opens. It
358	/// must advertise `None` so the subscriber never opens one.
359	#[test]
360	fn detect_reports_nothing_without_stats() {
361		use crate::lite::test_transport::{SinkSession, SinkStats};
362		let session = SinkSession::new(Default::default()).with_stats(SinkStats::default());
363		assert_eq!(ProbeLevel::detect(&session), ProbeLevel::None);
364	}
365
366	/// Either metric alone is enough to report, since the two PROBE fields are
367	/// independent on the wire.
368	#[test]
369	fn detect_reports_with_either_metric() {
370		use crate::lite::test_transport::{SinkSession, SinkStats};
371
372		let rtt_only = SinkStats::default().with_rtt(std::time::Duration::from_millis(40));
373		let session = SinkSession::new(Default::default()).with_stats(rtt_only);
374		assert_eq!(ProbeLevel::detect(&session), ProbeLevel::Report);
375
376		let rate_only = SinkStats::default().with_send_rate(1_000_000);
377		let session = SinkSession::new(Default::default()).with_stats(rate_only);
378		assert_eq!(ProbeLevel::detect(&session), ProbeLevel::Report);
379	}
380
381	#[test]
382	fn probe_levels_round_trip() {
383		for probe in [ProbeLevel::None, ProbeLevel::Report, ProbeLevel::Increase] {
384			let msg = Setup {
385				probe,
386				..Default::default()
387			};
388			assert_eq!(round_trip(&msg), msg);
389		}
390	}
391
392	#[test]
393	fn cost_round_trip() {
394		// Zero is a meaningful price (a free same-datacenter link), so it must survive
395		// the round trip as `Some(0)` rather than collapsing into "unpriced".
396		for cost in [None, Some(0), Some(1), Some(7)] {
397			let msg = Setup {
398				cost,
399				..Default::default()
400			};
401			assert_eq!(round_trip(&msg), msg);
402		}
403	}
404
405	/// Parameter values follow the session's varint codec, not a fixed one: a value that
406	/// takes the two-byte QUIC form on lite-06 fits one leading-ones byte on lite-07.
407	#[test]
408	fn parameter_values_use_the_version_codec() {
409		let msg = Setup {
410			cost: Some(100),
411			..Default::default()
412		};
413		for (version, wire) in [
414			(Version::Lite06, &[0x05, 0x01, 0x04, 0x02, 0x40, 0x64][..]),
415			(Version::Lite07, &[0x04, 0x01, 0x04, 0x01, 0x64][..]),
416		] {
417			let mut buf = Vec::new();
418			msg.encode(&mut buf, version).unwrap();
419			assert_eq!(buf, wire, "{version}");
420			assert_eq!(Setup::decode(&mut &buf[..], version).unwrap(), msg, "{version}");
421		}
422	}
423
424	/// Never emit a SETUP our own receiver would refuse.
425	#[test]
426	fn encode_enforces_the_setup_limit() {
427		// Count, id, and a 4-byte length varint precede the path.
428		let at_limit = crate::setup::MAX_SETUP_SIZE - 6;
429		let msg = Setup {
430			path: Some("a".repeat(at_limit)),
431			..Default::default()
432		};
433		assert_eq!(round_trip(&msg), msg);
434
435		let msg = Setup {
436			path: Some("a".repeat(at_limit + 1)),
437			..Default::default()
438		};
439		let mut buf = bytes::BytesMut::new();
440		assert!(matches!(
441			msg.encode(&mut buf, Version::Lite05),
442			Err(EncodeError::TooLarge)
443		));
444		assert!(buf.is_empty());
445	}
446
447	#[test]
448	fn path_round_trip() {
449		let msg = Setup {
450			probe: ProbeLevel::Report,
451			path: Some("/room/123".to_string()),
452			..Default::default()
453		};
454		assert_eq!(round_trip(&msg), msg);
455	}
456
457	#[test]
458	fn hop_round_trip() {
459		let msg = Setup {
460			hop: Some(crate::Hop::new(42).unwrap()),
461			..Default::default()
462		};
463		assert_eq!(round_trip(&msg), msg);
464	}
465
466	// A declared id of 0 carries no identity (it cannot be excluded), so it
467	// decodes as absent rather than erroring.
468	#[test]
469	fn hop_zero_decodes_as_none() {
470		use crate::coding::Encode;
471
472		let version = Version::Lite05;
473		let mut params = Parameters::default();
474		params.set_varint(super::PARAM_HOP, 0, version).unwrap();
475		let mut body = bytes::BytesMut::new();
476		params.encode(&mut body, version).unwrap();
477		// Frame the body with the Message Length prefix `Setup::decode` expects.
478		let mut buf = bytes::BytesMut::new();
479		(body.len() as u64).encode(&mut buf, version).unwrap();
480		buf.extend_from_slice(&body);
481		let mut slice = &buf[..];
482		let got = Setup::decode(&mut slice, version).unwrap();
483		assert_eq!(got.hop, None);
484	}
485
486	#[test]
487	fn empty_path_round_trips() {
488		// An empty path is valid and distinct from absent only on the wire; both mean
489		// the root, so a client doesn't have to special-case it.
490		let msg = Setup {
491			path: Some(String::new()),
492			..Default::default()
493		};
494		assert_eq!(round_trip(&msg), msg);
495	}
496
497	#[test]
498	fn roles_round_trip() {
499		for role in [Some(Role::Publisher), Some(Role::Subscriber), None] {
500			let msg = Setup {
501				path: Some("/room/123".to_string()),
502				role,
503				..Default::default()
504			};
505			assert_eq!(round_trip(&msg), msg);
506		}
507	}
508
509	#[test]
510	fn unknown_probe_level_saturates_to_increase() {
511		// Frame a SETUP message carrying an unknown probe level (99) by hand: the
512		// parameters body, prefixed with its length (the lite Message size prefix).
513		let mut params = Parameters::default();
514		params.set_varint(PARAM_PROBE, 99, Version::Lite05).unwrap();
515		let mut body = Vec::new();
516		params.encode(&mut body, Version::Lite05).unwrap();
517
518		let mut buf = bytes::BytesMut::new();
519		body.len().encode(&mut buf, Version::Lite05).unwrap();
520		buf.extend_from_slice(&body);
521
522		let mut slice = &buf[..];
523		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
524		assert_eq!(got.probe, ProbeLevel::Increase);
525	}
526
527	#[test]
528	fn role_wire_codes() {
529		// The draft pins Publisher=1 / Subscriber=2. A swap here would still round-trip
530		// against our own decoder, but break every other implementation.
531		for (role, code) in [(Role::Publisher, 1u64), (Role::Subscriber, 2)] {
532			assert_eq!(role.to_code(), code);
533			assert_eq!(Role::from_code(code), Some(role));
534		}
535	}
536
537	#[test]
538	fn unknown_role_decodes_as_bidirectional() {
539		// A role value the receiver doesn't recognize (a future extension, or an explicit
540		// 0) decodes to `None` rather than failing, so a newer client can't break an older
541		// server. The draft mandates this fallback.
542		for code in [0u64, 9, 250] {
543			let mut params = Parameters::default();
544			params.set_varint(PARAM_ROLE, code, Version::Lite05).unwrap();
545			let mut body = Vec::new();
546			params.encode(&mut body, Version::Lite05).unwrap();
547
548			let mut buf = bytes::BytesMut::new();
549			body.len().encode(&mut buf, Version::Lite05).unwrap();
550			buf.extend_from_slice(&body);
551
552			let mut slice = &buf[..];
553			let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
554			assert_eq!(got.role, None, "role code {code} should decode as bidirectional");
555		}
556	}
557
558	#[test]
559	fn rejects_before_lite05() {
560		let msg = Setup::default();
561		let mut buf = bytes::BytesMut::new();
562		assert!(matches!(
563			msg.encode(&mut buf, Version::Lite04),
564			Err(EncodeError::Version)
565		));
566	}
567
568	#[test]
569	fn ignores_unknown_parameters() {
570		// Frame a SETUP carrying an unknown parameter ID alongside the path.
571		let mut params = Parameters::default();
572		params.set_bytes(PARAM_PATH, b"/foo".to_vec());
573		params.set_bytes(0xbeef, b"whatever".to_vec());
574
575		let mut body = Vec::new();
576		params.encode(&mut body, Version::Lite05).unwrap();
577
578		// Wrap with the message size prefix the Message impl expects.
579		let mut buf = bytes::BytesMut::new();
580		body.len().encode(&mut buf, Version::Lite05).unwrap();
581		buf.extend_from_slice(&body);
582
583		let mut slice = &buf[..];
584		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
585		assert_eq!(got.path.as_deref(), Some("/foo"));
586	}
587}