wt: patch web-transport-proto header interop for Cloudflare relay

This commit is contained in:
every.channel 2026-02-18 01:28:57 -08:00
parent aa4bddcba0
commit 523c601dc3
No known key found for this signature in database
17 changed files with 2567 additions and 2 deletions

View file

@ -0,0 +1,254 @@
use std::{
collections::HashMap,
fmt::Debug,
ops::{Deref, DerefMut},
sync::Arc,
};
use bytes::{Buf, BufMut, BytesMut};
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use super::{Frame, StreamUni, VarInt, VarIntUnexpectedEnd};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Setting(pub VarInt);
impl Setting {
pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, VarIntUnexpectedEnd> {
Ok(Setting(VarInt::decode(buf)?))
}
pub fn encode<B: BufMut>(&self, buf: &mut B) {
self.0.encode(buf)
}
// Reference : https://datatracker.ietf.org/doc/html/rfc9114#section-7.2.4.1
pub fn is_grease(&self) -> bool {
let val = self.0.into_inner();
if val < 0x21 {
return false;
}
#[allow(unknown_lints, clippy::manual_is_multiple_of)]
{
(val - 0x21) % 0x1f == 0
}
}
}
impl Debug for Setting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Setting::QPACK_MAX_TABLE_CAPACITY => write!(f, "QPACK_MAX_TABLE_CAPACITY"),
Setting::MAX_FIELD_SECTION_SIZE => write!(f, "MAX_FIELD_SECTION_SIZE"),
Setting::QPACK_BLOCKED_STREAMS => write!(f, "QPACK_BLOCKED_STREAMS"),
Setting::ENABLE_CONNECT_PROTOCOL => write!(f, "ENABLE_CONNECT_PROTOCOL"),
Setting::ENABLE_DATAGRAM => write!(f, "ENABLE_DATAGRAM"),
Setting::ENABLE_DATAGRAM_DEPRECATED => write!(f, "ENABLE_DATAGRAM_DEPRECATED"),
Setting::WEBTRANSPORT_ENABLE_DEPRECATED => write!(f, "WEBTRANSPORT_ENABLE_DEPRECATED"),
Setting::WEBTRANSPORT_MAX_SESSIONS_DEPRECATED => {
write!(f, "WEBTRANSPORT_MAX_SESSIONS_DEPRECATED")
}
Setting::WEBTRANSPORT_MAX_SESSIONS => write!(f, "WEBTRANSPORT_MAX_SESSIONS"),
x if x.is_grease() => write!(f, "GREASE SETTING [{:x?}]", x.0.into_inner()),
x => write!(f, "UNKNOWN_SETTING [{:x?}]", x.0.into_inner()),
}
}
}
macro_rules! settings {
{$($name:ident = $val:expr,)*} => {
impl Setting {
$(pub const $name: Setting = Setting(VarInt::from_u32($val));)*
}
}
}
settings! {
// These are for HTTP/3 and we can ignore them
QPACK_MAX_TABLE_CAPACITY = 0x1, // default is 0, which disables QPACK dynamic table
MAX_FIELD_SECTION_SIZE = 0x6,
QPACK_BLOCKED_STREAMS = 0x7,
// Both of these are required for WebTransport
ENABLE_CONNECT_PROTOCOL = 0x8,
ENABLE_DATAGRAM = 0x33,
ENABLE_DATAGRAM_DEPRECATED = 0xFFD277, // still used by Chrome
// Removed in draft 06
WEBTRANSPORT_ENABLE_DEPRECATED = 0x2b603742,
WEBTRANSPORT_MAX_SESSIONS_DEPRECATED = 0x2b603743,
// New way to enable WebTransport
WEBTRANSPORT_MAX_SESSIONS = 0xc671706a,
}
#[derive(Error, Debug, Clone)]
pub enum SettingsError {
#[error("unexpected end of input")]
UnexpectedEnd,
#[error("unexpected stream type {0:?}")]
UnexpectedStreamType(StreamUni),
#[error("unexpected frame {0:?}")]
UnexpectedFrame(Frame),
#[error("invalid size")]
InvalidSize,
#[error("io error: {0}")]
Io(Arc<std::io::Error>),
}
impl From<std::io::Error> for SettingsError {
fn from(err: std::io::Error) -> Self {
SettingsError::Io(Arc::new(err))
}
}
// A map of settings to values.
#[derive(Default, Debug)]
pub struct Settings(HashMap<Setting, VarInt>);
impl Settings {
pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, SettingsError> {
let typ = StreamUni::decode(buf).map_err(|_| SettingsError::UnexpectedEnd)?;
if typ != StreamUni::CONTROL {
return Err(SettingsError::UnexpectedStreamType(typ));
}
let (typ, mut data) = Frame::read(buf).map_err(|_| SettingsError::UnexpectedEnd)?;
if typ != Frame::SETTINGS {
return Err(SettingsError::UnexpectedFrame(typ));
}
let mut settings = Settings::default();
while data.has_remaining() {
// These return a different error because retrying won't help.
let id = Setting::decode(&mut data).map_err(|_| SettingsError::InvalidSize)?;
let value = VarInt::decode(&mut data).map_err(|_| SettingsError::InvalidSize)?;
// Only add if it is not grease
if !id.is_grease() {
settings.0.insert(id, value);
}
}
Ok(settings)
}
pub async fn read<S: AsyncRead + Unpin>(stream: &mut S) -> Result<Self, SettingsError> {
let mut buf = Vec::new();
loop {
if stream.read_buf(&mut buf).await? == 0 {
return Err(SettingsError::UnexpectedEnd);
}
// Look at the buffer we've already read.
let mut limit = std::io::Cursor::new(&buf);
match Settings::decode(&mut limit) {
Ok(settings) => return Ok(settings),
Err(SettingsError::UnexpectedEnd) => continue, // More data needed.
Err(e) => return Err(e),
};
}
}
pub fn encode<B: BufMut>(&self, buf: &mut B) {
StreamUni::CONTROL.encode(buf);
Frame::SETTINGS.encode(buf);
// Encode to a temporary buffer so we can learn the length.
// TODO avoid doing this, just use a fixed size varint.
let mut tmp = Vec::new();
for (id, value) in &self.0 {
id.encode(&mut tmp);
value.encode(&mut tmp);
}
VarInt::from_u32(tmp.len() as u32).encode(buf);
buf.put_slice(&tmp);
}
pub async fn write<S: AsyncWrite + Unpin>(&self, stream: &mut S) -> Result<(), SettingsError> {
// TODO avoid allocating to the heap
let mut buf = BytesMut::new();
self.encode(&mut buf);
stream.write_all_buf(&mut buf).await?;
Ok(())
}
pub fn enable_webtransport(&mut self, max_sessions: u32) {
let max = VarInt::from_u32(max_sessions);
self.insert(Setting::ENABLE_CONNECT_PROTOCOL, VarInt::from_u32(1));
self.insert(Setting::ENABLE_DATAGRAM, VarInt::from_u32(1));
self.insert(Setting::ENABLE_DATAGRAM_DEPRECATED, VarInt::from_u32(1));
self.insert(Setting::WEBTRANSPORT_MAX_SESSIONS, max);
// TODO remove when 07 is in the wild
self.insert(Setting::WEBTRANSPORT_MAX_SESSIONS_DEPRECATED, max);
self.insert(Setting::WEBTRANSPORT_ENABLE_DEPRECATED, VarInt::from_u32(1));
}
// Returns the maximum number of sessions supported.
pub fn supports_webtransport(&self) -> u64 {
// Sent by Chrome 114.0.5735.198 (July 19, 2023)
// Setting(1): 65536, // qpack_max_table_capacity
// Setting(6): 16384, // max_field_section_size
// Setting(7): 100, // qpack_blocked_streams
// Setting(51): 1, // enable_datagram
// Setting(16765559): 1 // enable_datagram_deprecated
// Setting(727725890): 1, // webtransport_max_sessions_deprecated
// Setting(4445614305): 454654587, // grease
// NOTE: The presence of ENABLE_WEBTRANSPORT implies ENABLE_CONNECT is supported.
let datagram = self
.get(&Setting::ENABLE_DATAGRAM)
.or(self.get(&Setting::ENABLE_DATAGRAM_DEPRECATED))
.map(|v| v.into_inner());
if datagram != Some(1) {
return 0;
}
// The deprecated (before draft-07) way of enabling WebTransport was to send two parameters.
// Both would send ENABLE=1 and the server would send MAX_SESSIONS=N to limit the sessions.
// Now both just send MAX_SESSIONS, and a non-zero value means WebTransport is enabled.
if let Some(max) = self.get(&Setting::WEBTRANSPORT_MAX_SESSIONS) {
return max.into_inner();
}
let enabled = self
.get(&Setting::WEBTRANSPORT_ENABLE_DEPRECATED)
.map(|v| v.into_inner());
if enabled != Some(1) {
return 0;
}
// Only the server is allowed to set this one, so if it's None we assume it's 1.
self.get(&Setting::WEBTRANSPORT_MAX_SESSIONS_DEPRECATED)
.map(|v| v.into_inner())
.unwrap_or(1)
}
}
impl Deref for Settings {
type Target = HashMap<Setting, VarInt>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Settings {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}