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). Sent only by the client; a server never
142	/// sends one and a relay never forwards it. `None` on URI-carrying bindings,
143	/// where it would be a protocol violation. An empty path means the same thing
144	/// as `None`; both are on the wire so a client need not special-case the root.
145	pub path: Option<String>,
146	/// The single direction the client intends to use, or `None` for a bidirectional
147	/// session. `None` is sent as the absence of the parameter, which is also how a
148	/// client that predates the parameter decodes.
149	pub role: Option<Role>,
150	/// What crossing this link costs (lite-06+), added to the route cost of every
151	/// announcement forwarded over it. Sent only by the dialing side, since the link
152	/// cost lives in its connect config; the accepting side reads it here so both
153	/// ends price the same link identically. `None` means the default cost of 1.
154	pub cost: Option<u64>,
155	/// This endpoint's origin (hop) id, the identity it stamps onto forwarded
156	/// announcements. The peer uses it to serve this endpoint's subscriptions from
157	/// a route that does not flow through it (the same split horizon the announce
158	/// filter applies). `None` when the endpoint has no meaningful identity (a
159	/// leaf that never forwards); a wire value of 0 decodes as `None`.
160	pub origin: Option<crate::Origin>,
161}
162
163impl Message for Setup {
164	fn decode_msg<R: bytes::Buf>(r: &mut R, version: Version) -> Result<Self, DecodeError> {
165		if !version.has_setup_stream() {
166			return Err(DecodeError::Version);
167		}
168
169		let params = Parameters::decode(r, version)?;
170		let probe = params
171			.get_varint(PARAM_PROBE)?
172			.map(ProbeLevel::from_code)
173			.unwrap_or_default();
174		let path = match params.get_bytes(PARAM_PATH) {
175			Some(bytes) => Some(
176				std::str::from_utf8(bytes)
177					.map_err(|_| DecodeError::InvalidValue)?
178					.to_string(),
179			),
180			None => None,
181		};
182		let role = params.get_varint(PARAM_ROLE)?.and_then(Role::from_code);
183		let cost = params.get_varint(PARAM_COST)?;
184		// 0 is legal on the wire but carries no identity (it can't be excluded),
185		// so it decodes as "not declared" rather than an error.
186		let origin = params
187			.get_varint(PARAM_ORIGIN)?
188			.and_then(|id| crate::Origin::new(id).ok());
189
190		Ok(Self {
191			probe,
192			path,
193			role,
194			cost,
195			origin,
196		})
197	}
198
199	fn encode_msg<W: bytes::BufMut>(&self, w: &mut W, version: Version) -> Result<(), EncodeError> {
200		if !version.has_setup_stream() {
201			return Err(EncodeError::Version);
202		}
203
204		let mut params = Parameters::default();
205		// None is the wire default, so omit it to keep the message empty when nothing is set.
206		if self.probe != ProbeLevel::None {
207			params.set_varint(PARAM_PROBE, self.probe.to_code());
208		}
209		if let Some(path) = &self.path {
210			params.set_bytes(PARAM_PATH, path.as_bytes().to_vec());
211		}
212		// Bidirectional is the wire default (absence of the parameter), so only a
213		// directional role is encoded.
214		if let Some(role) = self.role {
215			params.set_varint(PARAM_ROLE, role.to_code());
216		}
217		if let Some(cost) = self.cost {
218			params.set_varint(PARAM_COST, cost);
219		}
220		if let Some(origin) = self.origin {
221			params.set_varint(PARAM_ORIGIN, origin.id());
222		}
223
224		params.encode(w, version)
225	}
226}
227
228/// Shared slot for the peer's SETUP, written once when its Setup stream is read.
229///
230/// Streams whose encoding depends on a negotiated capability (e.g. the PROBE
231/// stream) wait on this before deciding what to do. Cheap to clone: every handle
232/// shares the same slot.
233#[derive(Clone, Default)]
234pub(crate) struct PeerSetup(kio::Shared<Option<Setup>>);
235
236impl PeerSetup {
237	/// Record the peer's SETUP.
238	pub fn set(&self, setup: Setup) {
239		*self.0.lock() = Some(setup);
240	}
241
242	/// Await the peer's advertised probe level, blocking until its SETUP arrives.
243	pub async fn probe_level(&self) -> ProbeLevel {
244		self.wait(|setup| setup.probe).await
245	}
246
247	/// Await the link cost the peer (the dialing side) declared in its SETUP.
248	/// `None` when it declared none, meaning the default cost of 1.
249	pub async fn cost(&self) -> Option<u64> {
250		self.wait(|setup| setup.cost).await
251	}
252
253	/// Await the origin (hop) id the peer declared in its SETUP. `None` when it
254	/// declared none: a leaf with no identity worth excluding.
255	pub async fn origin(&self) -> Option<crate::Origin> {
256		self.wait(|setup| setup.origin).await
257	}
258
259	/// Await the peer's SETUP and read a field out of it.
260	///
261	/// The peer MUST send exactly one SETUP, so this resolves once that stream is read.
262	/// Waits forever if it never does; the caller is a session task, cancelled when the
263	/// driver drops.
264	async fn wait<T>(&self, f: impl FnOnce(&Setup) -> T) -> T {
265		let slot = self
266			.0
267			.wait(|setup| {
268				if setup.is_some() {
269					std::task::Poll::Ready(())
270				} else {
271					std::task::Poll::Pending
272				}
273			})
274			.await;
275		f(slot.as_ref().expect("waited for Some"))
276	}
277}
278
279#[cfg(test)]
280mod tests {
281	use super::*;
282
283	fn round_trip(msg: &Setup) -> Setup {
284		let mut buf = bytes::BytesMut::new();
285		msg.encode(&mut buf, Version::Lite05).unwrap();
286		let mut slice = &buf[..];
287		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
288		assert!(bytes::Buf::remaining(&slice) == 0, "trailing bytes after decode");
289		got
290	}
291
292	#[test]
293	fn empty_round_trip() {
294		let msg = Setup::default();
295		assert_eq!(round_trip(&msg), msg);
296	}
297
298	#[test]
299	fn probe_levels_round_trip() {
300		for probe in [ProbeLevel::None, ProbeLevel::Report, ProbeLevel::Increase] {
301			let msg = Setup {
302				probe,
303				..Default::default()
304			};
305			assert_eq!(round_trip(&msg), msg);
306		}
307	}
308
309	#[test]
310	fn cost_round_trip() {
311		// Zero is a meaningful price (a free same-datacenter link), so it must survive
312		// the round trip as `Some(0)` rather than collapsing into "unpriced".
313		for cost in [None, Some(0), Some(1), Some(7)] {
314			let msg = Setup {
315				cost,
316				..Default::default()
317			};
318			assert_eq!(round_trip(&msg), msg);
319		}
320	}
321
322	#[test]
323	fn path_round_trip() {
324		let msg = Setup {
325			probe: ProbeLevel::Report,
326			path: Some("/room/123".to_string()),
327			..Default::default()
328		};
329		assert_eq!(round_trip(&msg), msg);
330	}
331
332	#[test]
333	fn origin_round_trip() {
334		let msg = Setup {
335			origin: Some(crate::Origin::new(42).unwrap()),
336			..Default::default()
337		};
338		assert_eq!(round_trip(&msg), msg);
339	}
340
341	// A declared id of 0 carries no identity (it cannot be excluded), so it
342	// decodes as absent rather than erroring.
343	#[test]
344	fn origin_zero_decodes_as_none() {
345		use crate::coding::Encode;
346
347		let version = Version::Lite05;
348		let mut params = Parameters::default();
349		params.set_varint(super::PARAM_ORIGIN, 0);
350		let mut body = bytes::BytesMut::new();
351		params.encode(&mut body, version).unwrap();
352		// Frame the body with the Message Length prefix `Setup::decode` expects.
353		let mut buf = bytes::BytesMut::new();
354		(body.len() as u64).encode(&mut buf, version).unwrap();
355		buf.extend_from_slice(&body);
356		let mut slice = &buf[..];
357		let got = Setup::decode(&mut slice, version).unwrap();
358		assert_eq!(got.origin, None);
359	}
360
361	#[test]
362	fn empty_path_round_trips() {
363		// An empty path is valid and distinct from absent only on the wire; both mean
364		// the root, so a client doesn't have to special-case it.
365		let msg = Setup {
366			path: Some(String::new()),
367			..Default::default()
368		};
369		assert_eq!(round_trip(&msg), msg);
370	}
371
372	#[test]
373	fn roles_round_trip() {
374		for role in [Some(Role::Publisher), Some(Role::Subscriber), None] {
375			let msg = Setup {
376				path: Some("/room/123".to_string()),
377				role,
378				..Default::default()
379			};
380			assert_eq!(round_trip(&msg), msg);
381		}
382	}
383
384	#[test]
385	fn unknown_probe_level_saturates_to_increase() {
386		// Frame a SETUP message carrying an unknown probe level (99) by hand: the
387		// parameters body, prefixed with its length (the lite Message size prefix).
388		let mut params = Parameters::default();
389		params.set_varint(PARAM_PROBE, 99);
390		let mut body = Vec::new();
391		params.encode(&mut body, Version::Lite05).unwrap();
392
393		let mut buf = bytes::BytesMut::new();
394		body.len().encode(&mut buf, Version::Lite05).unwrap();
395		buf.extend_from_slice(&body);
396
397		let mut slice = &buf[..];
398		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
399		assert_eq!(got.probe, ProbeLevel::Increase);
400	}
401
402	#[test]
403	fn role_wire_codes() {
404		// The draft pins Publisher=1 / Subscriber=2. A swap here would still round-trip
405		// against our own decoder, but break every other implementation.
406		for (role, code) in [(Role::Publisher, 1u64), (Role::Subscriber, 2)] {
407			assert_eq!(role.to_code(), code);
408			assert_eq!(Role::from_code(code), Some(role));
409		}
410	}
411
412	#[test]
413	fn unknown_role_decodes_as_bidirectional() {
414		// A role value the receiver doesn't recognize (a future extension, or an explicit
415		// 0) decodes to `None` rather than failing, so a newer client can't break an older
416		// server. The draft mandates this fallback.
417		for code in [0u64, 9, 250] {
418			let mut params = Parameters::default();
419			params.set_varint(PARAM_ROLE, code);
420			let mut body = Vec::new();
421			params.encode(&mut body, Version::Lite05).unwrap();
422
423			let mut buf = bytes::BytesMut::new();
424			body.len().encode(&mut buf, Version::Lite05).unwrap();
425			buf.extend_from_slice(&body);
426
427			let mut slice = &buf[..];
428			let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
429			assert_eq!(got.role, None, "role code {code} should decode as bidirectional");
430		}
431	}
432
433	#[test]
434	fn rejects_before_lite05() {
435		let msg = Setup::default();
436		let mut buf = bytes::BytesMut::new();
437		assert!(matches!(
438			msg.encode(&mut buf, Version::Lite04),
439			Err(EncodeError::Version)
440		));
441	}
442
443	#[test]
444	fn ignores_unknown_parameters() {
445		// Frame a SETUP carrying an unknown parameter ID alongside the path.
446		let mut params = Parameters::default();
447		params.set_bytes(PARAM_PATH, b"/foo".to_vec());
448		params.set_bytes(0xbeef, b"whatever".to_vec());
449
450		let mut body = Vec::new();
451		params.encode(&mut body, Version::Lite05).unwrap();
452
453		// Wrap with the message size prefix the Message impl expects.
454		let mut buf = bytes::BytesMut::new();
455		body.len().encode(&mut buf, Version::Lite05).unwrap();
456		buf.extend_from_slice(&body);
457
458		let mut slice = &buf[..];
459		let got = Setup::decode(&mut slice, Version::Lite05).unwrap();
460		assert_eq!(got.path.as_deref(), Some("/foo"));
461	}
462}