Skip to main content

moqtap_client/transport/
quic.rs

1//! QUIC transport implementation wrapping quinn.
2
3use bytes::Bytes;
4
5use super::{RecvStream, SendStream, TransportError};
6
7/// QUIC transport wrapping a `quinn::Connection`.
8pub struct QuicTransport {
9    conn: quinn::Connection,
10}
11
12impl QuicTransport {
13    /// Create a new QUIC transport from a quinn connection.
14    pub fn new(conn: quinn::Connection) -> Self {
15        Self { conn }
16    }
17
18    /// Open a bidirectional stream.
19    pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), TransportError> {
20        let (send, recv) = self.conn.open_bi().await.map_err(conn_err)?;
21        Ok((SendStream::Quic(send), RecvStream::Quic(recv)))
22    }
23
24    /// Accept an incoming bidirectional stream.
25    pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), TransportError> {
26        let (send, recv) = self.conn.accept_bi().await.map_err(conn_err)?;
27        Ok((SendStream::Quic(send), RecvStream::Quic(recv)))
28    }
29
30    /// Open a unidirectional send stream.
31    pub async fn open_uni(&self) -> Result<SendStream, TransportError> {
32        let send = self.conn.open_uni().await.map_err(conn_err)?;
33        Ok(SendStream::Quic(send))
34    }
35
36    /// Accept an incoming unidirectional stream.
37    pub async fn accept_uni(&self) -> Result<RecvStream, TransportError> {
38        let recv = self.conn.accept_uni().await.map_err(conn_err)?;
39        Ok(RecvStream::Quic(recv))
40    }
41
42    /// Send a datagram.
43    pub fn send_datagram(&self, data: Bytes) -> Result<(), TransportError> {
44        self.conn.send_datagram(data).map_err(|e| TransportError::SendDatagram(e.to_string()))
45    }
46
47    /// Receive a datagram.
48    pub async fn recv_datagram(&self) -> Result<Bytes, TransportError> {
49        self.conn.read_datagram().await.map_err(conn_err)
50    }
51
52    /// Close the connection.
53    pub fn close(&self, code: u32, reason: &[u8]) {
54        self.conn.close(quinn::VarInt::from_u32(code), reason);
55    }
56}
57
58/// Convert a quinn connection error to a TransportError.
59fn conn_err(e: quinn::ConnectionError) -> TransportError {
60    TransportError::Connection(e.to_string())
61}
62
63// ── From impls for quinn error types ────────────────────────
64
65impl From<quinn::ConnectionError> for TransportError {
66    fn from(e: quinn::ConnectionError) -> Self {
67        TransportError::Connection(e.to_string())
68    }
69}
70
71impl From<quinn::WriteError> for TransportError {
72    /// `Stopped` keeps the peer's application error code as a typed
73    /// [`TransportError::Stopped`] so a forwarder can mirror it; every
74    /// other cause collapses to a message.
75    fn from(e: quinn::WriteError) -> Self {
76        match e {
77            quinn::WriteError::Stopped(code) => TransportError::Stopped(code.into_inner()),
78            other => TransportError::Write(other.to_string()),
79        }
80    }
81}
82
83impl From<quinn::ReadError> for TransportError {
84    /// `Reset` keeps the peer's application error code as a typed
85    /// [`TransportError::StreamReset`] so a forwarder can mirror it;
86    /// every other cause collapses to a message.
87    fn from(e: quinn::ReadError) -> Self {
88        match e {
89            quinn::ReadError::Reset(code) => TransportError::StreamReset(code.into_inner()),
90            other => TransportError::Read(other.to_string()),
91        }
92    }
93}
94
95impl From<quinn::ReadExactError> for TransportError {
96    fn from(e: quinn::ReadExactError) -> Self {
97        match e {
98            quinn::ReadExactError::ReadError(inner) => inner.into(),
99            other => TransportError::Read(other.to_string()),
100        }
101    }
102}
103
104impl From<quinn::ConnectError> for TransportError {
105    fn from(e: quinn::ConnectError) -> Self {
106        TransportError::Connect(e.to_string())
107    }
108}
109
110impl From<quinn::ClosedStream> for TransportError {
111    fn from(_e: quinn::ClosedStream) -> Self {
112        TransportError::StreamClosed
113    }
114}
115
116impl From<quinn::SendDatagramError> for TransportError {
117    fn from(e: quinn::SendDatagramError) -> Self {
118        TransportError::SendDatagram(e.to_string())
119    }
120}
121
122// ---------------------------------------------------------------------------
123// Dialling
124// ---------------------------------------------------------------------------
125
126/// How a QUIC dial is configured, independent of any draft.
127///
128/// `alpn` is a list because ALPN is: one handshake offers several protocols and
129/// the server picks, which is how a caller that does not know a peer's draft
130/// finds out without dialling once per candidate.
131pub struct QuicDialOptions {
132    /// Skip TLS certificate verification. Testing only.
133    pub skip_cert_verification: bool,
134    /// Additional CA certificates to trust, DER-encoded.
135    pub ca_certs: Vec<Vec<u8>>,
136    /// ALPN protocols to offer, in preference order.
137    ///
138    /// [`dial_quic`] returns the one the server selected. An empty list offers
139    /// nothing and is refused by any peer that requires ALPN, which every MoQT
140    /// relay does.
141    pub alpn: Vec<Vec<u8>>,
142}
143
144/// Why a QUIC dial did not produce a connection.
145///
146/// Separate from [`TransportError`] so the two failures that happen before any
147/// packet is sent — a bad address, a TLS config this machine rejects — stay
148/// distinguishable from a peer that would not talk to us.
149#[derive(Debug, thiserror::Error)]
150pub enum DialError {
151    /// `addr` is not a `host:port` this machine can resolve to a socket address.
152    #[error("invalid address: {0}")]
153    InvalidAddress(String),
154    /// The TLS client configuration could not be built.
155    #[error("TLS configuration error: {0}")]
156    TlsConfig(String),
157    /// The dial itself failed.
158    #[error(transparent)]
159    Transport(#[from] TransportError),
160}
161
162/// Dial a QUIC server, and report which ALPN it chose.
163///
164/// The second return value is the protocol the server chose from `options.alpn`,
165/// `None` if it selected none.
166/// [`DraftVersion::from_alpn`](moqtap_codec::version::DraftVersion::from_alpn)
167/// names a draft for five of the six; drafts 07-14 share `moq-00` and settle
168/// their version in CLIENT_SETUP.
169///
170/// The endpoint is dropped when the dial returns — quinn keeps the connection's
171/// driver alive independently.
172pub async fn dial_quic(
173    addr: &str,
174    options: &QuicDialOptions,
175) -> Result<(super::Transport, Option<Vec<u8>>), DialError> {
176    use std::sync::Arc;
177
178    let server_addr = addr
179        .parse()
180        .map_err(|e: std::net::AddrParseError| DialError::InvalidAddress(e.to_string()))?;
181
182    let mut tls_config = if options.skip_cert_verification {
183        rustls::ClientConfig::builder()
184            .dangerous()
185            .with_custom_certificate_verifier(Arc::new(SkipVerification))
186            .with_no_client_auth()
187    } else {
188        let mut roots = rustls::RootCertStore::empty();
189        roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
190        for der in &options.ca_certs {
191            roots
192                .add(rustls::pki_types::CertificateDer::from(der.clone()))
193                .map_err(|e| DialError::TlsConfig(format!("bad CA cert: {e}")))?;
194        }
195        rustls::ClientConfig::builder().with_root_certificates(roots).with_no_client_auth()
196    };
197
198    tls_config.alpn_protocols = options.alpn.clone();
199
200    let quic_config: quinn::crypto::rustls::QuicClientConfig =
201        tls_config.try_into().map_err(|e| DialError::TlsConfig(format!("{e}")))?;
202    let client_config = quinn::ClientConfig::new(Arc::new(quic_config));
203
204    let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap())
205        .map_err(|e| DialError::InvalidAddress(e.to_string()))?;
206    endpoint.set_default_client_config(client_config);
207
208    let server_name = addr.split(':').next().unwrap_or("localhost").to_string();
209
210    let quic = endpoint
211        .connect(server_addr, &server_name)
212        .map_err(TransportError::from)?
213        .await
214        .map_err(TransportError::from)?;
215
216    let negotiated = negotiated_alpn(&quic);
217    Ok((super::Transport::Quic(QuicTransport::new(quic)), negotiated))
218}
219
220/// The ALPN the server selected, if the handshake recorded one.
221fn negotiated_alpn(conn: &quinn::Connection) -> Option<Vec<u8>> {
222    conn.handshake_data()?.downcast::<quinn::crypto::rustls::HandshakeData>().ok()?.protocol
223}
224
225/// TLS certificate verifier that skips all verification. Testing only.
226#[derive(Debug)]
227struct SkipVerification;
228
229impl rustls::client::danger::ServerCertVerifier for SkipVerification {
230    fn verify_server_cert(
231        &self,
232        _end_entity: &rustls::pki_types::CertificateDer<'_>,
233        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
234        _server_name: &rustls::pki_types::ServerName<'_>,
235        _ocsp_response: &[u8],
236        _now: rustls::pki_types::UnixTime,
237    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
238        Ok(rustls::client::danger::ServerCertVerified::assertion())
239    }
240
241    fn verify_tls12_signature(
242        &self,
243        _message: &[u8],
244        _cert: &rustls::pki_types::CertificateDer<'_>,
245        _dcs: &rustls::DigitallySignedStruct,
246    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
247        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
248    }
249
250    fn verify_tls13_signature(
251        &self,
252        _message: &[u8],
253        _cert: &rustls::pki_types::CertificateDer<'_>,
254        _dcs: &rustls::DigitallySignedStruct,
255    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
256        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
257    }
258
259    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
260        vec![
261            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
262            rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
263            rustls::SignatureScheme::ED25519,
264            rustls::SignatureScheme::RSA_PSS_SHA256,
265            rustls::SignatureScheme::RSA_PSS_SHA384,
266            rustls::SignatureScheme::RSA_PSS_SHA512,
267        ]
268    }
269}