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 origin (hop) id.
17const PARAM_ORIGIN: 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 its estimated bitrate.
38	Report,
39	/// The publisher can additionally pad the connection (or send redundant data).
40	Increase,
41}
42
43impl ProbeLevel {
44	/// Map the wire value to a level, saturating unknown values to [`Increase`](Self::Increase).
45	fn from_code(code: u64) -> Self {
46		match code {
47			0 => Self::None,
48			1 => Self::Report,
49			_ => Self::Increase,
50		}
51	}
52
53	/// The wire value for this level.
54	fn to_code(self) -> u64 {
55		match self {
56			Self::None => 0,
57			Self::Report => 1,
58			Self::Increase => 2,
59		}
60	}
61}
62
63/// The single direction a client intends to use the session for.
64///
65/// A client advertises this in its SETUP so the server can reject a token that lacks
66/// the matching scope during the handshake, instead of accepting a connection that
67/// then silently carries no media (a subscribe-only token used to publish, or vice
68/// versa). It only ever narrows what the server grants, so it is not a security
69/// boundary: the server still enforces the token's scope regardless.
70///
71/// A session is bidirectional by default, which the wire says by omitting the
72/// parameter. `Option<Role>` mirrors that: `None` is the default, and it's also what
73/// a client that predates the parameter decodes to.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum Role {
77	/// The client will publish tracks (ingest); the server must consume.
78	Publisher,
79	/// The client will subscribe to tracks (egress); the server must publish.
80	Subscriber,
81}
82
83impl Role {
84	/// Map the wire value to a role. `0` and any unrecognized future value are `None`
85	/// (bidirectional): the draft requires a receiver that does not recognize the value
86	/// to treat it as both directions, so a newer client can't break an older server (it
87	/// just loses the early reject and defers fully to the token's scope).
88	fn from_code(code: u64) -> Option<Self> {
89		match code {
90			1 => Some(Role::Publisher),
91			2 => Some(Role::Subscriber),
92			_ => None,
93		}
94	}
95
96	/// The wire value for this role.
97	fn to_code(self) -> u64 {
98		match self {
99			Role::Publisher => 1,
100			Role::Subscriber => 2,
101		}
102	}
103
104	/// Derive the advertised role from which origins a client wired up: publish-only is
105	/// a [`Publisher`](Role::Publisher), consume-only a [`Subscriber`](Role::Subscriber),
106	/// and both (or neither) advertises nothing. This keeps the advertised role from
107	/// drifting away from what the session actually does.
108	pub(crate) fn from_origins(publishes: bool, consumes: bool) -> Option<Self> {
109		match (publishes, consumes) {
110			(true, false) => Some(Role::Publisher),
111			(false, true) => Some(Role::Subscriber),
112			_ => None,
113		}
114	}
115
116	/// Lowercase label for this role (`"publisher"` / `"subscriber"`).
117	pub fn as_str(self) -> &'static str {
118		match self {
119			Role::Publisher => "publisher",
120			Role::Subscriber => "subscriber",
121		}
122	}
123}
124
125impl std::fmt::Display for Role {
126	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127		f.write_str(self.as_str())
128	}
129}
130
131/// The SETUP message, sent once per endpoint on the unidirectional Setup Stream.
132///
133/// lite-05+ only. The two endpoints' SETUP messages are independent: neither side
134/// blocks on the peer's before opening other streams, but a stream whose encoding
135/// depends on a negotiated capability (e.g. PROBE) must wait for it.
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
137pub struct Setup {
138	/// The probe capability this endpoint supports. [`ProbeLevel::None`] when absent.
139	pub probe: ProbeLevel,
140	/// The request path, for transports that carry no request URI (native QUIC,
141	/// qmux over TCP/TLS, unix sockets), with `?` and the URI query appended when
142	/// there is one. Sent only by the client; a server never sends one and a relay
143	/// never forwards it. `None` on URI-carrying bindings, where it would be a
144	/// protocol violation. An empty path means the same thing as `None`; both are
145	/// on the wire so a client need not special-case the root.
146	pub path: Option<String>,
147	/// The single direction the client intends to use, or `None` for a bidirectional
148	/// session. `None` is sent as the absence of the parameter, which is also how a
149	/// client that predates the parameter decodes.
150	pub role: Option<Role>,
151	/// What subscribing from this endpoint costs (lite-06+), added by the peer to the
152	/// route cost of every announcement we forward it.
153	///
154	/// Directional: it prices the sender's own egress, so both ends declare their own
155	/// and the two need not match. `None` means the default cost of 1.
156	pub cost: Option<u64>,
157	/// This endpoint's origin (hop) id, the identity it stamps onto forwarded
158	/// announcements. The peer uses it to serve this endpoint's subscriptions from
159	/// a route that does not flow through it (the same split horizon the announce
160	/// filter applies). `None` when the endpoint has no meaningful identity (a
161	/// leaf that never forwards); a wire value of 0 decodes as `None`.
162	pub origin: Option<crate::Origin>,
163}
164
165impl Message for Setup {
166	fn decode_msg<R: bytes::Buf>(r: &mut R, version: Version) -> Result<Self, DecodeError> {
167		if !version.has_setup_stream() {
168			return Err(DecodeError::Version);
169		}
170
171		let params = Parameters::decode(r, version)?;
172		let probe = params
173			.get_varint(PARAM_PROBE)?
174			.map(ProbeLevel::from_code)
175			.unwrap_or_default();
176		let path = match params.get_bytes(PARAM_PATH) {
177			Some(bytes) => Some(
178				std::str::from_utf8(bytes)
179					.map_err(|_| DecodeError::InvalidValue)?
180					.to_string(),
181			),
182			None => None,
183		};
184		let role = params.get_varint(PARAM_ROLE)?.and_then(Role::from_code);
185		let cost = params.get_varint(PARAM_COST)?;
186		// 0 is legal on the wire but carries no identity (it can't be excluded),
187		// so it decodes as "not declared" rather than an error.
188		let origin = params
189			.get_varint(PARAM_ORIGIN)?
190			.and_then(|id| crate::Origin::new(id).ok());
191
192		Ok(Self {
193			probe,
194			path,
195			role,
196			cost,
197			origin,
198		})
199	}
200
201	fn encode_msg<W: bytes::BufMut>(&self, w: &mut W, version: Version) -> Result<(), EncodeError> {
202		if !version.has_setup_stream() {
203			return Err(EncodeError::Version);
204		}
205
206		let mut params = Parameters::default();
207		// None is the wire default, so omit it to keep the message empty when nothing is set.
208		if self.probe != ProbeLevel::None {
209			params.set_varint(PARAM_PROBE, self.probe.to_code());
210		}
211		if let Some(path) = &self.path {
212			params.set_bytes(PARAM_PATH, path.as_bytes().to_vec());
213		}
214		// Bidirectional is the wire default (absence of the parameter), so only a
215		// directional role is encoded.
216		if let Some(role) = self.role {
217			params.set_varint(PARAM_ROLE, role.to_code());
218		}
219		if let Some(cost) = self.cost {
220			params.set_varint(PARAM_COST, cost);
221		}
222		if let Some(origin) = self.origin {
223			params.set_varint(PARAM_ORIGIN, origin.id());
224		}
225
226		params.encode(w, version)
227	}
228}
229
230/// Shared slot for the peer's SETUP, written once when its Setup stream is read.
231///
232/// Streams whose encoding depends on a negotiated capability (e.g. the PROBE
233/// stream) wait on this before deciding what to do. Cheap to clone: every handle
234/// shares the same slot.
235#[derive(Clone, Default)]
236pub(crate) struct PeerSetup(kio::Shared<Option<Setup>>);
237
238impl PeerSetup {
239	/// Record the peer's SETUP.
240	pub fn set(&self, setup: Setup) {
241		*self.0.lock() = Some(setup);
242	}
243
244	/// Await the peer's advertised probe level, blocking until its SETUP arrives.
245	pub async fn probe_level(&self) -> ProbeLevel {
246		self.wait(|setup| setup.probe).await
247	}
248
249	/// Await the link cost the peer (the dialing side) declared in its SETUP.
250	/// `None` when it declared none, meaning the default cost of 1.
251	pub async fn cost(&self) -> Option<u64> {
252		self.wait(|setup| setup.cost).await
253	}
254
255	/// Await the origin (hop) id the peer declared in its SETUP. `None` when it
256	/// declared none: a leaf with no identity worth excluding.
257	pub async fn origin(&self) -> Option<crate::Origin> {
258		self.wait(|setup| setup.origin).await
259	}
260
261	/// Await the peer's SETUP and read a field out of it.
262	///
263	/// The peer MUST send exactly one SETUP, so this resolves once that stream is read.
264	/// Waits forever if it never does; the caller is a session task, cancelled when the
265	/// driver drops.
266	async fn wait<T>(&self, f: impl FnOnce(&Setup) -> T) -> T {
267		let slot = self
268			.0
269			.wait(|setup| {
270				if setup.is_some() {
271					std::task::Poll::Ready(())
272				} else {
273					std::task::Poll::Pending
274				}
275			})
276			.await;
277		f(slot.as_ref().expect("waited for Some"))
278	}
279}
280
281#[cfg(test)]
282mod tests {
283	use super::*;
284
285	fn round_trip(msg: &Setup) -> Setup {
286		let mut buf = bytes::BytesMut::new();
287		msg.encode(&mut buf, Version::Lite05).unwrap();
288		let mut slice = &buf[..];
289		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
290		assert!(bytes::Buf::remaining(&slice) == 0, "trailing bytes after decode");
291		got
292	}
293
294	#[test]
295	fn empty_round_trip() {
296		let msg = Setup::default();
297		assert_eq!(round_trip(&msg), msg);
298	}
299
300	#[test]
301	fn probe_levels_round_trip() {
302		for probe in [ProbeLevel::None, ProbeLevel::Report, ProbeLevel::Increase] {
303			let msg = Setup {
304				probe,
305				..Default::default()
306			};
307			assert_eq!(round_trip(&msg), msg);
308		}
309	}
310
311	#[test]
312	fn cost_round_trip() {
313		// Zero is a meaningful price (a free same-datacenter link), so it must survive
314		// the round trip as `Some(0)` rather than collapsing into "unpriced".
315		for cost in [None, Some(0), Some(1), Some(7)] {
316			let msg = Setup {
317				cost,
318				..Default::default()
319			};
320			assert_eq!(round_trip(&msg), msg);
321		}
322	}
323
324	#[test]
325	fn path_round_trip() {
326		let msg = Setup {
327			probe: ProbeLevel::Report,
328			path: Some("/room/123".to_string()),
329			..Default::default()
330		};
331		assert_eq!(round_trip(&msg), msg);
332	}
333
334	#[test]
335	fn origin_round_trip() {
336		let msg = Setup {
337			origin: Some(crate::Origin::new(42).unwrap()),
338			..Default::default()
339		};
340		assert_eq!(round_trip(&msg), msg);
341	}
342
343	// A declared id of 0 carries no identity (it cannot be excluded), so it
344	// decodes as absent rather than erroring.
345	#[test]
346	fn origin_zero_decodes_as_none() {
347		use crate::coding::Encode;
348
349		let version = Version::Lite05;
350		let mut params = Parameters::default();
351		params.set_varint(super::PARAM_ORIGIN, 0);
352		let mut body = bytes::BytesMut::new();
353		params.encode(&mut body, version).unwrap();
354		// Frame the body with the Message Length prefix `Setup::decode` expects.
355		let mut buf = bytes::BytesMut::new();
356		(body.len() as u64).encode(&mut buf, version).unwrap();
357		buf.extend_from_slice(&body);
358		let mut slice = &buf[..];
359		let got = Setup::decode(&mut slice, version).unwrap();
360		assert_eq!(got.origin, None);
361	}
362
363	#[test]
364	fn empty_path_round_trips() {
365		// An empty path is valid and distinct from absent only on the wire; both mean
366		// the root, so a client doesn't have to special-case it.
367		let msg = Setup {
368			path: Some(String::new()),
369			..Default::default()
370		};
371		assert_eq!(round_trip(&msg), msg);
372	}
373
374	#[test]
375	fn roles_round_trip() {
376		for role in [Some(Role::Publisher), Some(Role::Subscriber), None] {
377			let msg = Setup {
378				path: Some("/room/123".to_string()),
379				role,
380				..Default::default()
381			};
382			assert_eq!(round_trip(&msg), msg);
383		}
384	}
385
386	#[test]
387	fn unknown_probe_level_saturates_to_increase() {
388		// Frame a SETUP message carrying an unknown probe level (99) by hand: the
389		// parameters body, prefixed with its length (the lite Message size prefix).
390		let mut params = Parameters::default();
391		params.set_varint(PARAM_PROBE, 99);
392		let mut body = Vec::new();
393		params.encode(&mut body, Version::Lite05).unwrap();
394
395		let mut buf = bytes::BytesMut::new();
396		body.len().encode(&mut buf, Version::Lite05).unwrap();
397		buf.extend_from_slice(&body);
398
399		let mut slice = &buf[..];
400		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
401		assert_eq!(got.probe, ProbeLevel::Increase);
402	}
403
404	#[test]
405	fn role_wire_codes() {
406		// The draft pins Publisher=1 / Subscriber=2. A swap here would still round-trip
407		// against our own decoder, but break every other implementation.
408		for (role, code) in [(Role::Publisher, 1u64), (Role::Subscriber, 2)] {
409			assert_eq!(role.to_code(), code);
410			assert_eq!(Role::from_code(code), Some(role));
411		}
412	}
413
414	#[test]
415	fn unknown_role_decodes_as_bidirectional() {
416		// A role value the receiver doesn't recognize (a future extension, or an explicit
417		// 0) decodes to `None` rather than failing, so a newer client can't break an older
418		// server. The draft mandates this fallback.
419		for code in [0u64, 9, 250] {
420			let mut params = Parameters::default();
421			params.set_varint(PARAM_ROLE, code);
422			let mut body = Vec::new();
423			params.encode(&mut body, Version::Lite05).unwrap();
424
425			let mut buf = bytes::BytesMut::new();
426			body.len().encode(&mut buf, Version::Lite05).unwrap();
427			buf.extend_from_slice(&body);
428
429			let mut slice = &buf[..];
430			let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
431			assert_eq!(got.role, None, "role code {code} should decode as bidirectional");
432		}
433	}
434
435	#[test]
436	fn rejects_before_lite05() {
437		let msg = Setup::default();
438		let mut buf = bytes::BytesMut::new();
439		assert!(matches!(
440			msg.encode(&mut buf, Version::Lite04),
441			Err(EncodeError::Version)
442		));
443	}
444
445	#[test]
446	fn ignores_unknown_parameters() {
447		// Frame a SETUP carrying an unknown parameter ID alongside the path.
448		let mut params = Parameters::default();
449		params.set_bytes(PARAM_PATH, b"/foo".to_vec());
450		params.set_bytes(0xbeef, b"whatever".to_vec());
451
452		let mut body = Vec::new();
453		params.encode(&mut body, Version::Lite05).unwrap();
454
455		// Wrap with the message size prefix the Message impl expects.
456		let mut buf = bytes::BytesMut::new();
457		body.len().encode(&mut buf, Version::Lite05).unwrap();
458		buf.extend_from_slice(&body);
459
460		let mut slice = &buf[..];
461		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
462		assert_eq!(got.path.as_deref(), Some("/foo"));
463	}
464}