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