every.channel: sanitized baseline

This commit is contained in:
every.channel 2026-02-15 16:17:27 -05:00
commit 897e556bea
No known key found for this signature in database
258 changed files with 74298 additions and 0 deletions

View file

@ -0,0 +1,71 @@
use std::sync::Arc;
use iroh::{EndpointAddr, endpoint::{ConnectOptions, QuicTransportConfig}};
use url::Url;
use crate::{ALPN_H3, ClientError, Session};
/// A client for connecting to a WebTransport server.
pub struct Client {
endpoint: iroh::Endpoint,
config: QuicTransportConfig,
}
impl Client {
pub fn new(endpoint: iroh::Endpoint) -> Self {
Self::with_transport_config(endpoint, Default::default())
}
/// Creates a client from an endpoint and a transport config.
pub fn with_transport_config(
endpoint: iroh::Endpoint,
config: QuicTransportConfig,
) -> Self {
Self { endpoint, config }
}
/// Connect to a server over QUIC without HTTP/3.
pub async fn connect_quic(
&self,
addr: impl Into<EndpointAddr>,
alpn: &[u8],
) -> Result<Session, ClientError> {
let conn = self.connect(addr, alpn).await?;
Ok(Session::raw(conn))
}
/// Connect with a full HTTP/3 handshake and WebTransport semantics.
///
/// Note that the url needs to have a `https:` scheme, otherwise the accepting side will
/// fail to accept the connection.
pub async fn connect_h3(
&self,
addr: impl Into<EndpointAddr>,
url: Url,
) -> Result<Session, ClientError> {
let conn = self.connect(addr, ALPN_H3.as_bytes()).await?;
// Connect with the connection we established.
Session::connect_h3(conn, url).await
}
async fn connect(
&self,
addr: impl Into<EndpointAddr>,
alpn: &[u8],
) -> Result<iroh::endpoint::Connection, ClientError> {
let opts = ConnectOptions::new().with_transport_config(self.config.clone());
let conn = self
.endpoint
.connect_with_opts(addr, alpn, opts)
.await
.map_err(|err| ClientError::Connect(Arc::new(err.into())))?;
let conn = conn
.await
.map_err(|err| ClientError::Connect(Arc::new(err.into())))?;
Ok(conn)
}
pub async fn close(&self) {
self.endpoint.close().await;
}
}

View file

@ -0,0 +1,125 @@
use web_transport_proto::{ConnectRequest, ConnectResponse, VarInt};
use thiserror::Error;
use url::Url;
#[derive(Error, Debug, Clone)]
pub enum ConnectError {
#[error("quic stream was closed early")]
UnexpectedEnd,
#[error("protocol error: {0}")]
ProtoError(#[from] web_transport_proto::ConnectError),
#[error("connection error")]
ConnectionError(#[from] iroh::endpoint::ConnectionError),
#[error("read error")]
ReadError(#[from] quinn::ReadError),
#[error("write error")]
WriteError(#[from] quinn::WriteError),
#[error("http error status: {0}")]
ErrorStatus(http::StatusCode),
}
pub struct Connect {
// The request that was sent by the client.
request: ConnectRequest,
// A reference to the send/recv stream, so we don't close it until dropped.
send: quinn::SendStream,
#[allow(dead_code)]
recv: quinn::RecvStream,
}
impl Connect {
pub async fn accept(conn: &iroh::endpoint::Connection) -> Result<Self, ConnectError> {
// Accept the stream that will be used to send the HTTP CONNECT request.
// If they try to send any other type of HTTP request, we will error out.
let (send, mut recv) = conn.accept_bi().await?;
let request = web_transport_proto::ConnectRequest::read(&mut recv).await?;
tracing::debug!("received CONNECT request: {request:?}");
// The request was successfully decoded, so we can send a response.
Ok(Self {
request,
send,
recv,
})
}
// Called by the server to send a response to the client.
pub async fn respond(&mut self, status: http::StatusCode) -> Result<(), ConnectError> {
let resp = ConnectResponse { status };
tracing::debug!("sending CONNECT response: {resp:?}");
resp.write(&mut self.send).await?;
Ok(())
}
pub async fn open(conn: &iroh::endpoint::Connection, url: Url) -> Result<Self, ConnectError> {
// Create a new stream that will be used to send the CONNECT frame.
let (mut send, mut recv) = conn.open_bi().await?;
// Create a new CONNECT request that we'll send using HTTP/3
let request = ConnectRequest { url };
tracing::debug!("sending CONNECT request: {request:?}");
request.write(&mut send).await?;
let response = web_transport_proto::ConnectResponse::read(&mut recv).await?;
tracing::debug!("received CONNECT response: {response:?}");
// Throw an error if we didn't get a 200 OK.
if response.status != http::StatusCode::OK {
return Err(ConnectError::ErrorStatus(response.status));
}
Ok(Self {
request,
send,
recv,
})
}
// The session ID is the stream ID of the CONNECT request.
pub fn session_id(&self) -> VarInt {
// We gotta convert from the Quinn VarInt to the (forked) WebTransport VarInt.
// We don't use the quinn::VarInt because that would mean a quinn dependency in web-transport-proto
let stream_id = quinn::VarInt::from(self.send.id());
VarInt::try_from(stream_id.into_inner()).unwrap()
}
// The URL in the CONNECT request.
pub fn url(&self) -> &Url {
&self.request.url
}
pub(super) fn into_inner(self) -> (quinn::SendStream, quinn::RecvStream) {
(self.send, self.recv)
}
// Keep reading from the control stream until it's closed.
pub(crate) async fn run_closed(self) -> (u32, String) {
let (_send, mut recv) = self.into_inner();
loop {
match web_transport_proto::Capsule::read(&mut recv).await {
Ok(web_transport_proto::Capsule::CloseWebTransportSession { code, reason }) => {
return (code, reason);
}
Ok(web_transport_proto::Capsule::Unknown { typ, payload }) => {
tracing::warn!("unknown capsule: type={typ} size={}", payload.len());
}
Err(_) => {
return (1, "capsule error".to_string());
}
}
}
}
}

View file

@ -0,0 +1,256 @@
use std::sync::Arc;
use n0_error::stack_error;
use thiserror::Error;
use crate::{ConnectError, SettingsError};
/// An error returned when connecting to a WebTransport endpoint.
#[stack_error(derive, from_sources)]
#[derive(Clone)]
pub enum ClientError {
#[error("unexpected end of stream")]
UnexpectedEnd,
#[error("failed to connect")]
Connect(#[error(source)] Arc<iroh::endpoint::ConnectError>),
#[error("connection failed")]
Connection(#[error(source, std_err)] iroh::endpoint::ConnectionError),
#[error("failed to write")]
WriteError(#[error(source, std_err)] quinn::WriteError),
#[error("failed to read")]
ReadError(#[error(source, std_err)] quinn::ReadError),
#[error("failed to exchange h3 settings")]
SettingsError(#[error(from, source, std_err)] SettingsError),
#[error("failed to exchange h3 connect")]
HttpError(#[error(from, source, std_err)] ConnectError),
#[error("invalid URL")]
InvalidUrl,
#[error("endpoint failed to bind")]
Bind(#[error(source)] Arc<iroh::endpoint::BindError>),
}
/// An errors returned by [`crate::Session`], split based on if they are underlying QUIC errors or WebTransport errors.
#[derive(Clone, Error, Debug)]
pub enum SessionError {
#[error("connection error: {0}")]
ConnectionError(#[from] iroh::endpoint::ConnectionError),
#[error("webtransport error: {0}")]
WebTransportError(#[from] WebTransportError),
#[error("send datagram error: {0}")]
SendDatagramError(#[from] quinn::SendDatagramError),
}
/// An error that can occur when reading/writing the WebTransport stream header.
#[derive(Clone, Error, Debug)]
pub enum WebTransportError {
#[error("closed: code={0} reason={1}")]
Closed(u32, String),
#[error("unknown session")]
UnknownSession,
#[error("read error: {0}")]
ReadError(#[from] quinn::ReadExactError),
#[error("write error: {0}")]
WriteError(#[from] quinn::WriteError),
}
/// An error when writing to [`crate::SendStream`]. Similar to [`quinn::WriteError`].
#[derive(Clone, Error, Debug)]
pub enum WriteError {
#[error("STOP_SENDING: {0}")]
Stopped(u32),
#[error("invalid STOP_SENDING: {0}")]
InvalidStopped(quinn::VarInt),
#[error("session error: {0}")]
SessionError(#[from] SessionError),
#[error("stream closed")]
ClosedStream,
}
impl From<quinn::WriteError> for WriteError {
fn from(e: quinn::WriteError) -> Self {
match e {
quinn::WriteError::Stopped(code) => {
match web_transport_proto::error_from_http3(code.into_inner()) {
Some(code) => WriteError::Stopped(code),
None => WriteError::InvalidStopped(code),
}
}
quinn::WriteError::ClosedStream => WriteError::ClosedStream,
quinn::WriteError::ConnectionLost(e) => WriteError::SessionError(e.into()),
quinn::WriteError::ZeroRttRejected => unreachable!("0-RTT not supported"),
}
}
}
/// An error when reading from [`crate::RecvStream`]. Similar to [`quinn::ReadError`].
#[derive(Clone, Error, Debug)]
pub enum ReadError {
#[error("session error: {0}")]
SessionError(#[from] SessionError),
#[error("RESET_STREAM: {0}")]
Reset(u32),
#[error("invalid RESET_STREAM: {0}")]
InvalidReset(quinn::VarInt),
#[error("stream already closed")]
ClosedStream,
}
impl From<quinn::ReadError> for ReadError {
fn from(value: quinn::ReadError) -> Self {
match value {
quinn::ReadError::Reset(code) => {
match web_transport_proto::error_from_http3(code.into_inner()) {
Some(code) => ReadError::Reset(code),
None => ReadError::InvalidReset(code),
}
}
quinn::ReadError::ConnectionLost(e) => ReadError::SessionError(e.into()),
quinn::ReadError::ClosedStream => ReadError::ClosedStream,
quinn::ReadError::ZeroRttRejected => unreachable!("0-RTT not supported"),
}
}
}
/// An error returned by [`crate::RecvStream::read_exact`]. Similar to [`quinn::ReadExactError`].
#[derive(Clone, Error, Debug)]
pub enum ReadExactError {
#[error("finished early")]
FinishedEarly(usize),
#[error("read error: {0}")]
ReadError(#[from] ReadError),
}
impl From<quinn::ReadExactError> for ReadExactError {
fn from(e: quinn::ReadExactError) -> Self {
match e {
quinn::ReadExactError::FinishedEarly(size) => ReadExactError::FinishedEarly(size),
quinn::ReadExactError::ReadError(e) => ReadExactError::ReadError(e.into()),
}
}
}
/// An error returned by [`crate::RecvStream::read_to_end`]. Similar to [`quinn::ReadToEndError`].
#[derive(Clone, Error, Debug)]
pub enum ReadToEndError {
#[error("too long")]
TooLong,
#[error("read error: {0}")]
ReadError(#[from] ReadError),
}
impl From<quinn::ReadToEndError> for ReadToEndError {
fn from(e: quinn::ReadToEndError) -> Self {
match e {
quinn::ReadToEndError::TooLong => ReadToEndError::TooLong,
quinn::ReadToEndError::Read(e) => ReadToEndError::ReadError(e.into()),
}
}
}
/// An error indicating the stream was already closed.
#[derive(Clone, Error, Debug)]
#[error("stream closed")]
pub struct ClosedStream;
impl From<quinn::ClosedStream> for ClosedStream {
fn from(_: quinn::ClosedStream) -> Self {
ClosedStream
}
}
/// An error returned when receiving a new WebTransport session.
#[stack_error(derive, from_sources)]
#[derive(Clone)]
pub enum ServerError {
#[error("unexpected end of stream")]
UnexpectedEnd,
#[error("connection failed")]
Connection(#[error(source, std_err)] iroh::endpoint::ConnectionError),
#[error("connection failed during handshake")]
Connecting(#[error(source)] Arc<iroh::endpoint::ConnectingError>),
#[error("failed to write")]
WriteError(#[error(source, std_err)] quinn::WriteError),
#[error("failed to read")]
ReadError(#[error(source, std_err)] quinn::ReadError),
#[error("io error")]
IoError(#[error(source)] Arc<std::io::Error>),
#[error("failed to bind endpoint")]
Bind(#[error(source)] Arc<iroh::endpoint::BindError>),
#[error("failed to exchange h3 connect")]
HttpError(#[error(source, from, std_err)] ConnectError),
#[error("failed to exchange h3 settings")]
SettingsError(#[error(source, from, std_err)] SettingsError),
}
impl web_transport_trait::Error for SessionError {
fn session_error(&self) -> Option<(u32, String)> {
if let SessionError::WebTransportError(WebTransportError::Closed(code, reason)) = self {
return Some((*code, reason.to_string()));
}
None
}
}
impl web_transport_trait::Error for WriteError {
fn session_error(&self) -> Option<(u32, String)> {
if let WriteError::SessionError(e) = self {
return e.session_error();
}
None
}
fn stream_error(&self) -> Option<u32> {
match self {
WriteError::Stopped(code) => Some(*code),
_ => None,
}
}
}
impl web_transport_trait::Error for ReadError {
fn session_error(&self) -> Option<(u32, String)> {
if let ReadError::SessionError(e) = self {
return e.session_error();
}
None
}
fn stream_error(&self) -> Option<u32> {
match self {
ReadError::Reset(code) => Some(*code),
_ => None,
}
}
}

View file

@ -0,0 +1,56 @@
//! WebTransport is a protocol for client-server communication over QUIC.
//! It's [available in the browser](https://caniuse.com/webtransport) as an alternative to HTTP and WebSockets.
//!
//! WebTransport is layered on top of HTTP/3 which is then layered on top of QUIC.
//! This library hides that detail and tries to expose only the QUIC API, delegating as much as possible to the underlying implementation.
//! See the [Quinn documentation](https://docs.rs/quinn/latest/quinn/) for more documentation.
//!
//! QUIC provides two primary APIs:
//!
//! # Streams
//! QUIC streams are ordered, reliable, flow-controlled, and optionally bidirectional.
//! Both endpoints can create and close streams (including an error code) with no overhead.
//! You can think of them as TCP connections, but shared over a single QUIC connection.
//!
//! # Datagrams
//! QUIC datagrams are unordered, unreliable, and not flow-controlled.
//! Both endpoints can send datagrams below the MTU size (~1.2kb minimum) and they might arrive out of order or not at all.
//! They are basically UDP packets, except they are encrypted and congestion controlled.
//!
//! # Limitations
//! WebTransport is able to be pooled with HTTP/3 and multiple WebTransport sessions.
//! This crate avoids that complexity, doing the bare minimum to support a single WebTransport session that owns the entire QUIC connection.
//! If you want to support HTTP/3 on the same host/port, you should use another crate (ex. `h3-webtransport`).
//! If you want to support multiple WebTransport sessions over the same QUIC connection... you should just dial a new QUIC connection instead.
// External
mod client;
mod connect;
mod error;
mod recv;
mod send;
mod server;
mod session;
mod settings;
#[cfg(test)]
mod tests;
pub use client::*;
pub use connect::*;
pub use error::*;
pub use recv::*;
pub use send::*;
pub use server::*;
pub use session::*;
pub use settings::*;
/// The HTTP/3 ALPN is required when negotiating a QUIC connection.
pub const ALPN_H3: &str = "h3";
/// Re-export the http crate because it's in the public API.
pub use http;
pub use iroh;
/// Re-export the underlying QUIC implementation.
pub use quinn;
/// Re-export the generic WebTransport implementation.
pub use web_transport_trait as generic;

View file

@ -0,0 +1,111 @@
use std::{
io,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use crate::{ReadError, ReadExactError, ReadToEndError, SessionError};
/// A stream that can be used to recieve bytes. See [`quinn::RecvStream`].
#[derive(Debug)]
pub struct RecvStream {
inner: quinn::RecvStream,
}
impl RecvStream {
pub(crate) fn new(stream: quinn::RecvStream) -> Self {
Self { inner: stream }
}
/// Tell the other end to stop sending data with the given error code. See [`quinn::RecvStream::stop`].
/// This is a u32 with WebTransport since it shares the error space with HTTP/3.
pub fn stop(&mut self, code: u32) -> Result<(), quinn::ClosedStream> {
let code = web_transport_proto::error_to_http3(code);
let code = quinn::VarInt::try_from(code).unwrap();
self.inner.stop(code)
}
// Unfortunately, we have to wrap ReadError for a bunch of functions.
/// Read some data into the buffer and return the amount read. See [`quinn::RecvStream::read`].
pub async fn read(&mut self, buf: &mut [u8]) -> Result<Option<usize>, ReadError> {
self.inner.read(buf).await.map_err(Into::into)
}
/// Fill the entire buffer with data. See [`quinn::RecvStream::read_exact`].
pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), ReadExactError> {
self.inner.read_exact(buf).await.map_err(Into::into)
}
/// Read a chunk of data from the stream. See [`quinn::RecvStream::read_chunk`].
pub async fn read_chunk(
&mut self,
max_length: usize,
) -> Result<Option<quinn::Chunk>, ReadError> {
self.inner
.read_chunk(max_length)
.await
.map_err(Into::into)
}
/// Read chunks of data from the stream. See [`quinn::RecvStream::read_chunks`].
pub async fn read_chunks(&mut self, bufs: &mut [Bytes]) -> Result<Option<usize>, ReadError> {
self.inner.read_chunks(bufs).await.map_err(Into::into)
}
/// Read until the end of the stream or the limit is hit. See [`quinn::RecvStream::read_to_end`].
pub async fn read_to_end(&mut self, size_limit: usize) -> Result<Vec<u8>, ReadToEndError> {
self.inner.read_to_end(size_limit).await.map_err(Into::into)
}
/// Block until the stream has been reset and return the error code. See [`quinn::RecvStream::received_reset`].
///
/// Unlike Quinn, this returns a SessionError, not a ResetError, because 0-RTT is not supported.
pub async fn received_reset(&mut self) -> Result<Option<u32>, SessionError> {
match self.inner.received_reset().await {
Ok(None) => Ok(None),
Ok(Some(code)) => Ok(Some(
web_transport_proto::error_from_http3(code.into_inner()).unwrap(),
)),
Err(quinn::ResetError::ConnectionLost(e)) => Err(e.into()),
Err(quinn::ResetError::ZeroRttRejected) => unreachable!("0-RTT not supported"),
}
}
// We purposely don't expose the stream ID or 0RTT because it's not valid with WebTransport
}
impl tokio::io::AsyncRead for RecvStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.inner).poll_read(cx, buf)
}
}
impl web_transport_trait::RecvStream for RecvStream {
type Error = ReadError;
fn stop(&mut self, code: u32) {
Self::stop(self, code).ok();
}
async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
self.read(dst).await
}
async fn read_chunk(&mut self, max: usize) -> Result<Option<Bytes>, Self::Error> {
self.read_chunk(max)
.await
.map(|r| r.map(|chunk| chunk.bytes))
}
async fn closed(&mut self) -> Result<(), Self::Error> {
self.received_reset().await?;
Ok(())
}
}

View file

@ -0,0 +1,142 @@
use std::{
io,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Buf, Bytes};
use crate::{ClosedStream, SessionError, WriteError};
/// A stream that can be used to send bytes. See [`quinn::SendStream`].
///
/// This wrapper is mainly needed for error codes, which is unfortunate.
/// WebTransport uses u32 error codes and they're mapped in a reserved HTTP/3 error space.
#[derive(Debug)]
pub struct SendStream {
stream: quinn::SendStream,
}
impl SendStream {
pub(crate) fn new(stream: quinn::SendStream) -> Self {
Self { stream }
}
/// Abruptly reset the stream with the provided error code. See [`quinn::SendStream::reset`].
/// This is a u32 with WebTransport because we share the error space with HTTP/3.
pub fn reset(&mut self, code: u32) -> Result<(), ClosedStream> {
let code = web_transport_proto::error_to_http3(code);
let code = quinn::VarInt::try_from(code).unwrap();
self.stream.reset(code).map_err(Into::into)
}
/// Wait until the stream has been stopped and return the error code. See [`quinn::SendStream::stopped`].
///
/// Unlike Quinn, this returns None if the code is not a valid WebTransport error code.
/// Also unlike Quinn, this returns a SessionError, not a StoppedError, because 0-RTT is not supported.
pub async fn stopped(&mut self) -> Result<Option<u32>, SessionError> {
match self.stream.stopped().await {
Ok(Some(code)) => Ok(web_transport_proto::error_from_http3(code.into_inner())),
Ok(None) => Ok(None),
Err(quinn::StoppedError::ConnectionLost(e)) => Err(e.into()),
Err(quinn::StoppedError::ZeroRttRejected) => unreachable!("0-RTT not supported"),
}
}
// Unfortunately, we have to wrap WriteError for a bunch of functions.
/// Write some data to the stream, returning the size written. See [`quinn::SendStream::write`].
pub async fn write(&mut self, buf: &[u8]) -> Result<usize, WriteError> {
self.stream.write(buf).await.map_err(Into::into)
}
/// Write all of the data to the stream. See [`quinn::SendStream::write_all`].
pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), WriteError> {
self.stream.write_all(buf).await.map_err(Into::into)
}
/// Write chunks of data to the stream. See [`quinn::SendStream::write_chunks`].
pub async fn write_chunks(&mut self, bufs: &mut [Bytes]) -> Result<quinn::Written, WriteError> {
self.stream.write_chunks(bufs).await.map_err(Into::into)
}
/// Write a chunk of data to the stream. See [`quinn::SendStream::write_chunk`].
pub async fn write_chunk(&mut self, buf: Bytes) -> Result<(), WriteError> {
self.stream.write_chunk(buf).await.map_err(Into::into)
}
/// Write all of the chunks of data to the stream. See [`quinn::SendStream::write_all_chunks`].
pub async fn write_all_chunks(&mut self, bufs: &mut [Bytes]) -> Result<(), WriteError> {
self.stream.write_all_chunks(bufs).await.map_err(Into::into)
}
/// Mark the stream as finished, such that no more data can be written. See [`quinn::SendStream::finish`].
pub fn finish(&mut self) -> Result<(), ClosedStream> {
self.stream.finish().map_err(Into::into)
}
pub fn set_priority(&self, order: i32) -> Result<(), ClosedStream> {
self.stream.set_priority(order).map_err(Into::into)
}
pub fn priority(&self) -> Result<i32, ClosedStream> {
self.stream.priority().map_err(Into::into)
}
}
impl tokio::io::AsyncWrite for SendStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
// We have to use this syntax because quinn added its own poll_write method.
tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.stream), cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream).poll_shutdown(cx)
}
}
impl web_transport_trait::SendStream for SendStream {
type Error = WriteError;
fn set_priority(&mut self, order: u8) {
self.stream.set_priority(order.into()).ok();
}
fn reset(&mut self, code: u32) {
Self::reset(self, code).ok();
}
fn finish(&mut self) -> Result<(), Self::Error> {
Self::finish(self).map_err(|_| WriteError::ClosedStream)?;
Ok(())
}
async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
Self::write(self, buf).await
}
async fn write_buf<B: Buf + Send>(&mut self, buf: &mut B) -> Result<usize, Self::Error> {
// This can avoid making a copy when Buf is Bytes, as Quinn will allocate anyway.
let size = buf.chunk().len();
let chunk = buf.copy_to_bytes(size);
self.write_chunk(chunk).await?;
Ok(size)
}
async fn write_chunk(&mut self, chunk: Bytes) -> Result<(), Self::Error> {
self.write_chunk(chunk).await
}
async fn closed(&mut self) -> Result<(), Self::Error> {
self.stopped().await?;
Ok(())
}
}

View file

@ -0,0 +1,76 @@
use url::Url;
use crate::{Connect, ServerError, Session, Settings};
/// A QUIC-only WebTransport handshake, awaiting server decision.
pub struct QuicRequest {
conn: iroh::endpoint::Connection,
}
/// An H3 WebTransport handshake, SETTINGS exchanged and CONNECT accepted,
/// awaiting server decision (respond OK / reject).
pub struct H3Request {
conn: iroh::endpoint::Connection,
settings: Settings,
connect: Connect,
}
impl QuicRequest {
/// Accept a new QUIC-only WebTransport session from a client.
pub fn accept(conn: iroh::endpoint::Connection) -> Self {
Self { conn }
}
pub fn conn(&self) -> &iroh::endpoint::Connection {
&self.conn
}
/// Accept the session.
pub fn ok(self) -> Session {
Session::raw(self.conn)
}
/// Reject the session.
pub fn close(self, status: http::StatusCode) {
self.conn
.close(status.as_u16().into(), status.as_str().as_bytes());
}
}
impl H3Request {
/// Accept a new H3 WebTransport session from a client.
pub async fn accept(conn: iroh::endpoint::Connection) -> Result<Self, ServerError> {
// Perform the H3 handshake by sending/receiving SETTINGS frames.
let settings = Settings::connect(&conn).await?;
// Accept the CONNECT request but don't send a response yet.
let connect = Connect::accept(&conn).await?;
Ok(Self {
conn,
settings,
connect,
})
}
/// Returns the URL provided by the client.
pub fn url(&self) -> &Url {
self.connect.url()
}
pub fn conn(&self) -> &iroh::endpoint::Connection {
&self.conn
}
/// Accept the session, returning a 200 OK.
pub async fn ok(mut self) -> Result<Session, ServerError> {
self.connect.respond(http::StatusCode::OK).await?;
Ok(Session::new_h3(self.conn, self.settings, self.connect))
}
/// Reject the session, returning your favorite HTTP status code.
pub async fn close(mut self, status: http::StatusCode) -> Result<(), ServerError> {
self.connect.respond(status).await?;
Ok(())
}
}

View file

@ -0,0 +1,540 @@
use std::{
fmt,
future::{Future, poll_fn},
io::Cursor,
ops::Deref,
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll, ready},
};
use bytes::{Bytes, BytesMut};
use iroh::endpoint::Connection;
use n0_future::{
FuturesUnordered,
stream::{Stream, StreamExt},
};
use url::Url;
use crate::{
ClientError, Connect, RecvStream, SendStream, SessionError, Settings, WebTransportError,
};
use web_transport_proto::{Frame, StreamUni, VarInt};
/// An established WebTransport session, acting like a full QUIC connection. See [`iroh::endpoint::Connection`].
///
/// It is important to remember that WebTransport is layered on top of QUIC:
/// 1. Each stream starts with a few bytes identifying the stream type and session ID.
/// 2. Errors codes are encoded with the session ID, so they aren't full QUIC error codes.
/// 3. Stream IDs may have gaps in them, used by HTTP/3 transparant to the application.
///
/// Deref is used to expose non-overloaded methods on [`iroh::endpoint::Connection`].
/// These should be safe to use with WebTransport, but file a PR if you find one that isn't.
#[derive(Clone)]
pub struct Session {
conn: Connection,
h3: Option<H3SessionState>,
}
impl Session {
/// Create a new session from a raw QUIC connection and a URL.
///
/// This is used to pretend like a QUIC connection is a WebTransport session.
/// It's a hack, but it makes it much easier to support WebTransport and raw QUIC simultaneously.
pub fn raw(conn: Connection) -> Self {
Self { conn, h3: None }
}
/// Connect using an established QUIC connection if you want to create the connection yourself.
/// This will only work with a brand new QUIC connection using the HTTP/3 ALPN.
pub async fn connect_h3(conn: Connection, url: Url) -> Result<Session, ClientError> {
// Perform the H3 handshake by sending/reciving SETTINGS frames.
let settings = Settings::connect(&conn).await?;
// Send the HTTP/3 CONNECT request.
let connect = Connect::open(&conn, url).await?;
Ok(Self::new_h3(conn, settings, connect))
}
pub fn new_h3(conn: Connection, settings: Settings, connect: Connect) -> Self {
let h3 = H3SessionState::connect(conn.clone(), settings, &connect);
let this = Session { conn, h3: Some(h3) };
// Run a background task to check if the connect stream is closed.
let this2 = this.clone();
tokio::spawn(async move {
let (code, reason) = connect.run_closed().await;
if this2.conn().close_reason().is_none() {
// TODO We shouldn't be closing the QUIC connection with the same error.
this2.close(code, reason.as_bytes());
}
});
this
}
pub fn conn(&self) -> &Connection {
&self.conn
}
pub fn url(&self) -> Option<&Url> {
self.h3.as_ref().map(|s| &s.url)
}
/// Accept a new unidirectional stream. See [`iroh::endpoint::Connection::accept_uni`].
pub async fn accept_uni(&self) -> Result<RecvStream, SessionError> {
if let Some(h3) = &self.h3 {
poll_fn(|cx| h3.accept.lock().unwrap().poll_accept_uni(cx)).await
} else {
self.conn
.accept_uni()
.await
.map(RecvStream::new)
.map_err(Into::into)
}
}
/// Accept a new bidirectional stream. See [`iroh::endpoint::Connection::accept_bi`].
pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), SessionError> {
if let Some(h3) = &self.h3 {
poll_fn(|cx| h3.accept.lock().unwrap().poll_accept_bi(cx)).await
} else {
self.conn
.accept_bi()
.await
.map(|(send, recv)| (SendStream::new(send), RecvStream::new(recv)))
.map_err(Into::into)
}
}
/// Open a new unidirectional stream. See [`iroh::endpoint::Connection::open_uni`].
pub async fn open_uni(&self) -> Result<SendStream, SessionError> {
let mut send = self.conn.open_uni().await?;
if let Some(h3) = self.h3.as_ref() {
write_full_with_max_prio(&mut send, &h3.header_uni).await?;
}
Ok(SendStream::new(send))
}
/// Open a new bidirectional stream. See [`iroh::endpoint::Connection::open_bi`].
pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), SessionError> {
let (mut send, recv) = self.conn.open_bi().await?;
if let Some(h3) = self.h3.as_ref() {
write_full_with_max_prio(&mut send, &h3.header_bi).await?;
}
Ok((SendStream::new(send), RecvStream::new(recv)))
}
/// Asynchronously receives an application datagram from the remote peer.
///
/// This method is used to receive an application datagram sent by the remote
/// peer over the connection.
/// It waits for a datagram to become available and returns the received bytes.
pub async fn read_datagram(&self) -> Result<Bytes, SessionError> {
let mut datagram = self
.conn
.read_datagram()
.await
.map_err(SessionError::from)?;
let datagram = if let Some(h3) = self.h3.as_ref() {
let mut cursor = Cursor::new(&datagram);
// We have to check and strip the session ID from the datagram.
let actual_id =
VarInt::decode(&mut cursor).map_err(|_| WebTransportError::UnknownSession)?;
if actual_id != h3.session_id {
return Err(WebTransportError::UnknownSession.into());
}
// Return the datagram without the session ID.
let datagram = datagram.split_off(cursor.position() as usize);
datagram
} else {
datagram
};
Ok(datagram)
}
/// Sends an application datagram to the remote peer.
///
/// Datagrams are unreliable and may be dropped or delivered out of order.
/// The data must be smaller than [`max_datagram_size`](Self::max_datagram_size).
pub fn send_datagram(&self, data: Bytes) -> Result<(), SessionError> {
let datagram = if let Some(h3) = self.h3.as_ref() {
// Unfortunately, we need to allocate/copy each datagram because of the Quinn API.
// Pls go +1 if you care: https://github.com/quinn-rs/quinn/issues/1724
let mut buf = BytesMut::with_capacity(h3.header_datagram.len() + data.len());
// Prepend the datagram with the header indicating the session ID.
buf.extend_from_slice(&h3.header_datagram);
buf.extend_from_slice(&data);
buf.into()
} else {
data
};
self.conn.send_datagram(datagram)?;
Ok(())
}
/// Computes the maximum size of datagrams that may be passed to
/// [`send_datagram`](Self::send_datagram).
pub fn max_datagram_size(&self) -> usize {
let mtu = self
.conn
.max_datagram_size()
.expect("datagram support is required");
if let Some(h3) = self.h3.as_ref() {
mtu.saturating_sub(h3.header_datagram.len())
} else {
mtu
}
}
/// Immediately close the connection with an error code and reason. See [`iroh::endpoint::Connection::close`].
pub fn close(&self, code: u32, reason: &[u8]) {
let code = if self.h3.is_some() {
web_transport_proto::error_to_http3(code)
.try_into()
.unwrap()
} else {
code.into()
};
self.conn.close(code, reason)
}
/// Wait until the session is closed, returning the error. See [`iroh::endpoint::Connection::closed`].
pub async fn closed(&self) -> SessionError {
self.conn.closed().await.into()
}
/// Return why the session was closed, or None if it's not closed. See [`iroh::endpoint::Connection::close_reason`].
pub fn close_reason(&self) -> Option<SessionError> {
self.conn.close_reason().map(Into::into)
}
}
async fn write_full_with_max_prio(
send: &mut quinn::SendStream,
buf: &[u8],
) -> Result<(), SessionError> {
// Set the stream priority to max and then write the stream header.
// Otherwise the application could write data with lower priority than the header, resulting in queuing.
// Also the header is very important for determining the session ID without reliable reset.
send.set_priority(i32::MAX).ok();
let res = match send.write_all(buf).await {
Ok(_) => Ok(()),
Err(quinn::WriteError::ConnectionLost(err)) => Err(err.into()),
Err(err) => Err(WebTransportError::WriteError(err).into()),
};
// Reset the stream priority back to the default of 0.
send.set_priority(0).ok();
res
}
impl Deref for Session {
type Target = Connection;
fn deref(&self) -> &Self::Target {
&self.conn
}
}
impl fmt::Debug for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.conn.fmt(f)
}
}
impl PartialEq for Session {
fn eq(&self, other: &Self) -> bool {
self.conn.stable_id() == other.conn.stable_id()
}
}
impl Eq for Session {}
#[derive(Clone)]
struct H3SessionState {
url: Url,
// The session ID, as determined by the stream ID of the connect request.
session_id: VarInt,
// Cache the headers in front of each stream we open.
header_uni: Vec<u8>,
header_bi: Vec<u8>,
header_datagram: Vec<u8>,
// Keep a reference to the settings and connect stream to avoid closing them until dropped.
#[allow(dead_code)]
settings: Arc<Settings>,
// The accept logic is stateful, so use an Arc<Mutex> to share it.
accept: Arc<Mutex<H3SessionAccept>>,
}
impl H3SessionState {
fn connect(conn: Connection, settings: Settings, connect: &Connect) -> Self {
// The session ID is the stream ID of the CONNECT request.
let session_id = connect.session_id();
// Cache the tiny header we write in front of each stream we open.
let mut header_uni = Vec::new();
StreamUni::WEBTRANSPORT.encode(&mut header_uni);
session_id.encode(&mut header_uni);
let mut header_bi = Vec::new();
Frame::WEBTRANSPORT.encode(&mut header_bi);
session_id.encode(&mut header_bi);
let mut header_datagram = Vec::new();
session_id.encode(&mut header_datagram);
// Accept logic is stateful, so use an Arc<Mutex> to share it.
let accept = H3SessionAccept::new(conn, session_id);
Self {
url: connect.url().clone(),
session_id,
header_uni,
header_bi,
header_datagram,
settings: Arc::new(settings),
accept: Arc::new(Mutex::new(accept)),
}
}
}
// Type aliases just so clippy doesn't complain about the complexity.
type AcceptUni =
dyn Stream<Item = Result<quinn::RecvStream, iroh::endpoint::ConnectionError>> + Send;
type AcceptBi = dyn Stream<Item = Result<(quinn::SendStream, quinn::RecvStream), iroh::endpoint::ConnectionError>>
+ Send;
type PendingUni = dyn Future<Output = Result<(StreamUni, quinn::RecvStream), SessionError>> + Send;
type PendingBi = dyn Future<Output = Result<Option<(quinn::SendStream, quinn::RecvStream)>, SessionError>>
+ Send;
// Logic just for accepting streams, which is annoying because of the stream header.
pub struct H3SessionAccept {
session_id: VarInt,
// We also need to keep a reference to the qpack streams if the endpoint (incorrectly) creates them.
// Again, this is just so they don't get closed until we drop the session.
qpack_encoder: Option<quinn::RecvStream>,
qpack_decoder: Option<quinn::RecvStream>,
accept_uni: Pin<Box<AcceptUni>>,
accept_bi: Pin<Box<AcceptBi>>,
// Keep track of work being done to read/write the WebTransport stream header.
pending_uni: FuturesUnordered<Pin<Box<PendingUni>>>,
pending_bi: FuturesUnordered<Pin<Box<PendingBi>>>,
}
impl H3SessionAccept {
pub(crate) fn new(conn: Connection, session_id: VarInt) -> Self {
// Create a stream that just outputs new streams, so it's easy to call from poll.
let accept_uni = Box::pin(n0_future::stream::unfold(conn.clone(), |conn| async {
Some((conn.accept_uni().await, conn))
}));
let accept_bi = Box::pin(n0_future::stream::unfold(conn, |conn| async {
Some((conn.accept_bi().await, conn))
}));
Self {
session_id,
qpack_decoder: None,
qpack_encoder: None,
accept_uni,
accept_bi,
pending_uni: FuturesUnordered::new(),
pending_bi: FuturesUnordered::new(),
}
}
// This is poll-based because we accept and decode streams in parallel.
// In async land I would use tokio::JoinSet, but that requires a runtime.
// It's better to use FuturesUnordered instead because it's agnostic.
pub fn poll_accept_uni(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<RecvStream, SessionError>> {
loop {
// Accept any new streams.
if let Poll::Ready(Some(res)) = self.accept_uni.poll_next(cx) {
// Start decoding the header and add the future to the list of pending streams.
let recv = res?;
let pending = Self::decode_uni(recv, self.session_id);
self.pending_uni.push(Box::pin(pending));
continue;
}
// Poll the list of pending streams.
let (typ, recv) = match ready!(self.pending_uni.poll_next(cx)) {
Some(Ok(res)) => res,
Some(Err(err)) => {
// Ignore the error, the stream was probably reset early.
tracing::warn!("failed to decode unidirectional stream: {err:?}");
continue;
}
None => return Poll::Pending,
};
// Decide if we keep looping based on the type.
match typ {
StreamUni::WEBTRANSPORT => {
let recv = RecvStream::new(recv);
return Poll::Ready(Ok(recv));
}
StreamUni::QPACK_DECODER => {
self.qpack_decoder = Some(recv);
}
StreamUni::QPACK_ENCODER => {
self.qpack_encoder = Some(recv);
}
_ => {
// ignore unknown streams
tracing::debug!("ignoring unknown unidirectional stream: {typ:?}");
}
}
}
}
// Reads the stream header, returning the stream type.
async fn decode_uni(
mut recv: quinn::RecvStream,
expected_session: VarInt,
) -> Result<(StreamUni, quinn::RecvStream), SessionError> {
// Read the VarInt at the start of the stream.
let typ = VarInt::read(&mut recv)
.await
.map_err(|_| WebTransportError::UnknownSession)?;
let typ = StreamUni(typ);
if typ == StreamUni::WEBTRANSPORT {
// Read the session_id and validate it
let session_id = VarInt::read(&mut recv)
.await
.map_err(|_| WebTransportError::UnknownSession)?;
if session_id != expected_session {
return Err(WebTransportError::UnknownSession.into());
}
}
// We need to keep a reference to the qpack streams if the endpoint (incorrectly) creates them, so return everything.
Ok((typ, recv))
}
pub fn poll_accept_bi(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<(SendStream, RecvStream), SessionError>> {
loop {
// Accept any new streams.
if let Poll::Ready(Some(res)) = self.accept_bi.poll_next(cx) {
// Start decoding the header and add the future to the list of pending streams.
let (send, recv) = res?;
let pending = Self::decode_bi(send, recv, self.session_id);
self.pending_bi.push(Box::pin(pending));
continue;
}
// Poll the list of pending streams.
let res = match ready!(self.pending_bi.poll_next(cx)) {
Some(Ok(res)) => res,
Some(Err(err)) => {
// Ignore the error, the stream was probably reset early.
tracing::warn!("failed to decode bidirectional stream: {err:?}");
continue;
}
None => return Poll::Pending,
};
if let Some((send, recv)) = res {
// Wrap the streams in our own types for correct error codes.
let send = SendStream::new(send);
let recv = RecvStream::new(recv);
return Poll::Ready(Ok((send, recv)));
}
// Keep looping if it's a stream we want to ignore.
}
}
// Reads the stream header, returning Some if it's a WebTransport stream.
async fn decode_bi(
send: quinn::SendStream,
mut recv: quinn::RecvStream,
expected_session: VarInt,
) -> Result<Option<(quinn::SendStream, quinn::RecvStream)>, SessionError> {
let typ = VarInt::read(&mut recv)
.await
.map_err(|_| WebTransportError::UnknownSession)?;
if Frame(typ) != Frame::WEBTRANSPORT {
tracing::debug!("ignoring unknown bidirectional stream: {typ:?}");
return Ok(None);
}
// Read the session ID and validate it.
let session_id = VarInt::read(&mut recv)
.await
.map_err(|_| WebTransportError::UnknownSession)?;
if session_id != expected_session {
return Err(WebTransportError::UnknownSession.into());
}
Ok(Some((send, recv)))
}
}
impl web_transport_trait::Session for Session {
type SendStream = SendStream;
type RecvStream = RecvStream;
type Error = SessionError;
async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
Self::accept_uni(self).await
}
async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
Self::accept_bi(self).await
}
async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
Self::open_bi(self).await
}
async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
Self::open_uni(self).await
}
fn close(&self, code: u32, reason: &str) {
Self::close(self, code, reason.as_bytes());
}
async fn closed(&self) -> Self::Error {
Self::closed(self).await
}
fn send_datagram(&self, data: Bytes) -> Result<(), Self::Error> {
Self::send_datagram(self, data)
}
async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
Self::read_datagram(self).await
}
fn max_datagram_size(&self) -> usize {
Self::max_datagram_size(self)
}
}

View file

@ -0,0 +1,69 @@
use thiserror::Error;
use tokio::try_join;
#[derive(Error, Debug, Clone)]
pub enum SettingsError {
#[error("quic stream was closed early")]
UnexpectedEnd,
#[error("protocol error: {0}")]
ProtoError(#[from] web_transport_proto::SettingsError),
#[error("WebTransport is not supported")]
WebTransportUnsupported,
#[error("connection error")]
ConnectionError(#[from] iroh::endpoint::ConnectionError),
#[error("read error")]
ReadError(#[from] quinn::ReadError),
#[error("write error")]
WriteError(#[from] quinn::WriteError),
}
pub struct Settings {
// A reference to the send/recv stream, so we don't close it until dropped.
#[allow(dead_code)]
send: quinn::SendStream,
#[allow(dead_code)]
recv: quinn::RecvStream,
}
impl Settings {
// Establish the H3 connection.
pub async fn connect(conn: &iroh::endpoint::Connection) -> Result<Self, SettingsError> {
let recv = Self::accept(conn);
let send = Self::open(conn);
// Run both tasks concurrently until one errors or they both complete.
let (send, recv) = try_join!(send, recv)?;
Ok(Self { send, recv })
}
async fn accept(conn: &iroh::endpoint::Connection) -> Result<quinn::RecvStream, SettingsError> {
let mut recv = conn.accept_uni().await?;
let settings = web_transport_proto::Settings::read(&mut recv).await?;
tracing::debug!("received SETTINGS frame: {settings:?}");
if settings.supports_webtransport() == 0 {
return Err(SettingsError::WebTransportUnsupported);
}
Ok(recv)
}
async fn open(conn: &iroh::endpoint::Connection) -> Result<quinn::SendStream, SettingsError> {
let mut settings = web_transport_proto::Settings::default();
settings.enable_webtransport(1);
tracing::debug!("sending SETTINGS frame: {settings:?}");
let mut send = conn.open_uni().await?;
settings.write(&mut send).await?;
Ok(send)
}
}

View file

@ -0,0 +1,128 @@
use iroh::{Endpoint, endpoint::ConnectionError};
use n0_tracing_test::traced_test;
use tracing::Instrument;
use url::Url;
use crate::{ALPN_H3, Client, H3Request, QuicRequest, SessionError};
#[tokio::test]
#[traced_test]
async fn h3_smoke() -> n0_error::Result<()> {
let client = Endpoint::bind()
.instrument(tracing::error_span!("client-ep"))
.await
.unwrap();
let client_id = client.id();
let client = Client::new(client);
let server = Endpoint::builder()
.alpns(vec![ALPN_H3.as_bytes().to_vec()])
.bind()
.instrument(tracing::error_span!("server-ep"))
.await
.unwrap();
let server_id = server.id();
let server_addr = server.addr();
let url: Url = format!("https://{}/foo", server_id).parse().unwrap();
let client_task = tokio::task::spawn({
let url = url.clone();
async move {
let session = client.connect_h3(server_addr, url.clone()).await.inspect_err(|err| println!("{err:#?}")).unwrap();
assert_eq!(session.remote_id(), server_id);
assert_eq!(session.url(), Some(&url));
let mut stream = session.open_uni().await.unwrap();
stream.write_all(b"hi").await.unwrap();
stream.finish().unwrap();
let reason = session.closed().await;
assert!(
matches!(reason, SessionError::ConnectionError(ConnectionError::ApplicationClosed(frame)) if web_transport_proto::error_from_http3(frame.error_code.into_inner()) == Some(23))
);
drop(session);
client.close().await;
}.instrument(tracing::error_span!("client"))
});
let server_task = tokio::task::spawn(
async move {
let conn = server.accept().await.unwrap().await.unwrap();
assert_eq!(conn.alpn(), ALPN_H3.as_bytes());
let request = H3Request::accept(conn)
.await
.inspect_err(|err| tracing::error!("accept failed: {err:?}"))
.unwrap();
assert_eq!(request.url(), &url);
assert_eq!(request.conn().remote_id(), client_id);
let session = request.ok().await.unwrap();
assert_eq!(session.url(), Some(&url));
assert_eq!(session.conn().remote_id(), client_id);
let mut stream = session.accept_uni().await.unwrap();
let buf = stream.read_to_end(2).await.unwrap();
assert_eq!(buf, b"hi");
session.close(23, b"bye");
server.close().await;
}
.instrument(tracing::error_span!("server")),
);
client_task.await.unwrap();
server_task.await.unwrap();
Ok(())
}
#[tokio::test]
#[traced_test]
async fn quic_smoke() -> n0_error::Result<()> {
const ALPN: &str = "moql";
let client = Endpoint::bind().await.unwrap();
let client_id = client.id();
let client = Client::new(client);
let server = Endpoint::builder()
.alpns(vec![ALPN.as_bytes().to_vec()])
.bind()
.await
.unwrap();
let server_id = server.id();
let server_addr = server.addr();
let client_task = tokio::task::spawn({
async move {
let session = client
.connect_quic(server_addr, ALPN.as_bytes())
.await
.unwrap();
println!("session established");
assert_eq!(session.remote_id(), server_id);
assert_eq!(session.url(), None);
let reason = session.closed().await;
assert!(
matches!(reason, SessionError::ConnectionError(ConnectionError::ApplicationClosed(frame)) if frame.error_code.into_inner() == 23)
)
}.instrument(tracing::error_span!("client"))
});
let server_task = tokio::task::spawn({
async move {
let conn = server.accept().await.unwrap().await.unwrap();
assert_eq!(conn.alpn(), ALPN.as_bytes());
let request = QuicRequest::accept(conn);
assert_eq!(request.conn().remote_id(), client_id);
let session = request.ok();
assert_eq!(session.url(), None);
assert_eq!(session.conn().remote_id(), client_id);
session.close(23, b"bye");
}
.instrument(tracing::error_span!("server"))
});
client_task.await.unwrap();
server_task.await.unwrap();
Ok(())
}