Draft::select takes the most preferred draft a client offers (-22 first), as the relay's TLS does. The relay client takes extra Setup Options (AUTHORITY and PATH for a moqt:// URI), and the relay tests can run any session at -18 or -16. Co-Authored-By: Claude Opus 5.5 <[email protected]>
202 lines
8.2 KiB
Rust
202 lines
8.2 KiB
Rust
//! Which MOQT draft a session speaks.
|
|
//!
|
|
//! Since -15 a draft is negotiated before the first MOQT byte: by ALPN on
|
|
//! native QUIC and by `WT-Available-Protocols` on WebTransport, each draft
|
|
//! named `moqt-NN` (`draft-ietf-moq-transport-22` §6.2, `-18` §3.1, `-16` §3.1).
|
|
//! No two drafts interoperate, so a session is one draft from its first byte to
|
|
//! its last.
|
|
//!
|
|
//! This crate speaks three drafts, each a codec module of its own:
|
|
//!
|
|
//! | draft | ALPN | module | role |
|
|
//! |---|---|---|---|
|
|
//! | `draft-ietf-moq-transport-22` | `moqt-22` | [`crate::draft22`] | the default, and the model the rest of the crate is written in |
|
|
//! | `draft-ietf-moq-transport-18` | `moqt-18` | [`crate::draft18`] | the working group's interoperability target |
|
|
//! | `draft-ietf-moq-transport-16` | `moqt-16` | [`crate::draft16`] | deployed public relays |
|
|
//!
|
|
//! A session at -18 or -16 is carried by translating each message and object to
|
|
//! and from the -22 model at the session's edge (`draft18::bridge`,
|
|
//! `draft16::bridge`); every place where the drafts differ is named there, and
|
|
//! what one draft can say and the other cannot is refused there by name rather
|
|
//! than guessed at.
|
|
|
|
/// A MOQT draft this crate can speak.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
|
pub enum Draft {
|
|
/// `draft-ietf-moq-transport-16`, 13 January 2026.
|
|
D16,
|
|
/// `draft-ietf-moq-transport-18`, 12 May 2026.
|
|
D18,
|
|
/// `draft-ietf-moq-transport-22`, 1 October 2026.
|
|
D22,
|
|
}
|
|
|
|
impl Draft {
|
|
/// Every draft this crate speaks, most preferred first: -22 is the default,
|
|
/// and an earlier draft is chosen only when the peer offers no later one.
|
|
pub const SUPPORTED: &'static [Draft] = &[Draft::D22, Draft::D18, Draft::D16];
|
|
|
|
/// The draft a session speaks when nothing else is asked for.
|
|
pub const DEFAULT: Draft = Draft::D22;
|
|
|
|
/// The draft number.
|
|
pub const fn number(self) -> u32 {
|
|
match self {
|
|
Draft::D16 => 16,
|
|
Draft::D18 => 18,
|
|
Draft::D22 => 22,
|
|
}
|
|
}
|
|
|
|
/// The ALPN token (and WebTransport protocol) that selects it.
|
|
pub const fn alpn(self) -> &'static [u8] {
|
|
match self {
|
|
Draft::D16 => crate::draft16::ALPN,
|
|
Draft::D18 => crate::draft18::ALPN,
|
|
Draft::D22 => crate::draft22::ALPN,
|
|
}
|
|
}
|
|
|
|
/// The ALPN token as text, the form `WT-Available-Protocols` carries.
|
|
pub fn alpn_str(self) -> &'static str {
|
|
std::str::from_utf8(self.alpn()).expect("ALPN tokens are ASCII")
|
|
}
|
|
|
|
/// The conventional 32-bit code of the draft, `0xff000000 + NN`.
|
|
pub const fn version_code(self) -> u32 {
|
|
0xff00_0000 + self.number()
|
|
}
|
|
|
|
pub fn from_alpn(alpn: &[u8]) -> Option<Draft> {
|
|
Self::SUPPORTED.iter().copied().find(|d| d.alpn() == alpn)
|
|
}
|
|
|
|
pub fn from_version_code(code: u32) -> Option<Draft> {
|
|
Self::SUPPORTED.iter().copied().find(|d| d.version_code() == code)
|
|
}
|
|
|
|
/// The draft of its number, when this crate speaks it.
|
|
pub fn from_number(n: u32) -> Option<Draft> {
|
|
Self::SUPPORTED.iter().copied().find(|d| d.number() == n)
|
|
}
|
|
|
|
/// The draft a server chooses among a client's ALPNs: the one this crate prefers
|
|
/// most ([`Draft::SUPPORTED`]'s order) of those offered. The relay's TLS
|
|
/// configuration chooses the same way (the server's list first).
|
|
pub fn select<'a>(offered: impl IntoIterator<Item = &'a [u8]>) -> Option<Draft> {
|
|
let offered: Vec<Draft> = offered.into_iter().filter_map(Draft::from_alpn).collect();
|
|
Self::SUPPORTED.iter().copied().find(|d| offered.contains(d))
|
|
}
|
|
|
|
/// The ALPN list a client offers, most preferred first.
|
|
pub fn alpn_list() -> Vec<Vec<u8>> {
|
|
Self::SUPPORTED.iter().map(|d| d.alpn().to_vec()).collect()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Draft {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "draft-ietf-moq-transport-{}", self.number())
|
|
}
|
|
}
|
|
|
|
/// Whether `v` follows the grease pattern `0x7f * N + 0x9D` (-22 §13, -18 §14).
|
|
pub fn is_grease(v: u64) -> bool {
|
|
v >= 0x9d && (v - 0x9d).is_multiple_of(0x7f)
|
|
}
|
|
|
|
/// A registry of status or error codes: a newtype over the wire value, so a code a
|
|
/// draft does not name still round-trips, with the names that draft registers.
|
|
macro_rules! code_registry {
|
|
(
|
|
$(#[$meta:meta])*
|
|
$name:ident, internal = $internal:expr, { $( $(#[$cm:meta])* $konst:ident = $val:expr ),* $(,)? }
|
|
) => {
|
|
$(#[$meta])*
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
|
pub struct $name(pub u64);
|
|
|
|
impl $name {
|
|
$( $(#[$cm])* pub const $konst: $name = $name($val); )*
|
|
|
|
/// Every code this draft registers, with its name.
|
|
pub const REGISTERED: &'static [($name, &'static str)] = &[ $( ($name($val), stringify!($konst)) ),* ];
|
|
|
|
/// The registered name, or `None` for a code this draft does not register.
|
|
pub fn name(self) -> Option<&'static str> {
|
|
Self::REGISTERED.iter().find(|(c, _)| *c == self).map(|(_, n)| *n)
|
|
}
|
|
|
|
/// The code read as the draft says an unknown one reads: `INTERNAL_ERROR`
|
|
/// for a code it does not register (-22 §13, -18 §14).
|
|
pub fn effective(self) -> $name {
|
|
if self.name().is_some() { self } else { $internal }
|
|
}
|
|
|
|
/// Whether the value is a grease value (`0x7f * N + 0x9D`).
|
|
pub fn is_grease(self) -> bool {
|
|
$crate::draft::is_grease(self.0)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for $name {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self.name() {
|
|
Some(n) => f.write_str(n),
|
|
None => write!(f, "{:#x}", self.0),
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
pub(crate) use code_registry;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn each_draft_by_alpn_and_code() {
|
|
assert_eq!(Draft::D22.alpn(), b"moqt-22");
|
|
assert_eq!(Draft::D18.alpn(), b"moqt-18");
|
|
assert_eq!(Draft::D16.alpn(), b"moqt-16");
|
|
assert_eq!(Draft::D22.version_code(), 0xff00_0016);
|
|
assert_eq!(Draft::D18.version_code(), 0xff00_0012);
|
|
assert_eq!(Draft::D16.version_code(), 0xff00_0010);
|
|
assert_eq!(Draft::from_alpn(b"moqt-22"), Some(Draft::D22));
|
|
assert_eq!(Draft::from_alpn(b"moqt-18"), Some(Draft::D18));
|
|
assert_eq!(Draft::from_alpn(b"moqt-16"), Some(Draft::D16));
|
|
// Drafts this crate does not speak, and the ALPN every draft before -15 used.
|
|
for other in [&b"moqt-17"[..], b"moqt-20", b"moqt-14", b"moq-00", b"h3", b"moqt"] {
|
|
assert_eq!(Draft::from_alpn(other), None, "{}", String::from_utf8_lossy(other));
|
|
}
|
|
assert_eq!(Draft::from_version_code(crate::draft22::VERSION), Some(Draft::D22));
|
|
assert_eq!(Draft::from_version_code(crate::draft18::VERSION), Some(Draft::D18));
|
|
assert_eq!(Draft::from_version_code(crate::draft16::VERSION), Some(Draft::D16));
|
|
assert_eq!(Draft::from_number(18), Some(Draft::D18));
|
|
assert_eq!(Draft::D18.to_string(), "draft-ietf-moq-transport-18");
|
|
}
|
|
|
|
#[test]
|
|
fn negotiation_prefers_22() {
|
|
// A server takes the draft it prefers most of those the client offers.
|
|
let offered: [&[u8]; 3] = [b"moqt-18", b"h3", b"moqt-22"];
|
|
assert_eq!(Draft::select(offered), Some(Draft::D22));
|
|
let offered: [&[u8]; 3] = [b"moqt-20", b"moqt-16", b"moqt-18"];
|
|
assert_eq!(Draft::select(offered), Some(Draft::D18));
|
|
let offered: [&[u8]; 2] = [b"moqt-20", b"moqt-16"];
|
|
assert_eq!(Draft::select(offered), Some(Draft::D16));
|
|
let offered: [&[u8]; 2] = [b"moqt-14", b"moq-00"];
|
|
assert_eq!(Draft::select(offered), None);
|
|
// This crate offers -22 first.
|
|
assert_eq!(Draft::alpn_list(), vec![b"moqt-22".to_vec(), b"moqt-18".to_vec(), b"moqt-16".to_vec()]);
|
|
assert_eq!(Draft::DEFAULT, Draft::D22);
|
|
assert_eq!(Draft::SUPPORTED[0], Draft::DEFAULT);
|
|
}
|
|
|
|
#[test]
|
|
fn grease() {
|
|
assert!(is_grease(0x9d) && is_grease(0x11c));
|
|
assert!(!is_grease(0x9c));
|
|
}
|
|
}
|