moqtap_client/transport/quic.rs
1//! QUIC transport implementation wrapping quinn.
2
3use std::future::Future;
4use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
5
6use bytes::Bytes;
7
8use super::{RecvStream, SendStream, TransportError};
9
10/// QUIC transport wrapping a `quinn::Connection`.
11pub struct QuicTransport {
12 conn: quinn::Connection,
13}
14
15impl QuicTransport {
16 /// Create a new QUIC transport from a quinn connection.
17 pub fn new(conn: quinn::Connection) -> Self {
18 Self { conn }
19 }
20
21 /// Open a bidirectional stream.
22 pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), TransportError> {
23 let (send, recv) = self.conn.open_bi().await.map_err(conn_err)?;
24 Ok((SendStream::Quic(send), RecvStream::Quic(recv)))
25 }
26
27 /// Accept an incoming bidirectional stream.
28 pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), TransportError> {
29 let (send, recv) = self.conn.accept_bi().await.map_err(conn_err)?;
30 Ok((SendStream::Quic(send), RecvStream::Quic(recv)))
31 }
32
33 /// Open a unidirectional send stream.
34 pub async fn open_uni(&self) -> Result<SendStream, TransportError> {
35 let send = self.conn.open_uni().await.map_err(conn_err)?;
36 Ok(SendStream::Quic(send))
37 }
38
39 /// Accept an incoming unidirectional stream.
40 pub async fn accept_uni(&self) -> Result<RecvStream, TransportError> {
41 let recv = self.conn.accept_uni().await.map_err(conn_err)?;
42 Ok(RecvStream::Quic(recv))
43 }
44
45 /// Send a datagram.
46 pub fn send_datagram(&self, data: Bytes) -> Result<(), TransportError> {
47 self.conn.send_datagram(data).map_err(|e| TransportError::SendDatagram(e.to_string()))
48 }
49
50 /// Receive a datagram.
51 pub async fn recv_datagram(&self) -> Result<Bytes, TransportError> {
52 self.conn.read_datagram().await.map_err(conn_err)
53 }
54
55 /// Close the connection.
56 pub fn close(&self, code: u32, reason: &[u8]) {
57 self.conn.close(quinn::VarInt::from_u32(code), reason);
58 }
59
60 /// A future resolving when the session ends, carrying the peer's close.
61 ///
62 /// Borrows nothing and outlives this transport, for the same reason
63 /// [`SendStream::stopped`] does: the answer arrives after the call that
64 /// wanted it has already returned. A peer refusing a handshake commonly
65 /// finishes the control stream first and closes the session a round trip
66 /// later, so the caller sees an end-of-stream, hands back an error, and
67 /// drops the transport before the code it was refused with ever lands.
68 /// Taking this handle *before* the transport is given away is what makes
69 /// that code readable at all.
70 ///
71 /// Holding the future keeps the connection alive; dropping it lets quinn
72 /// close as usual.
73 pub fn closed(&self) -> impl Future<Output = TransportError> + Send + 'static {
74 let conn = self.conn.clone();
75 async move { connection_lost(conn.closed().await) }
76 }
77
78 /// The certificate chain the peer presented, DER-encoded, leaf first.
79 ///
80 /// Bytes, and no opinion about them: nothing here parses a certificate,
81 /// checks a date, or decides whether a chain is trustworthy, because the
82 /// verdict depends on what the caller is measuring. Empty is not an error —
83 /// a chain is absent for ordinary reasons, including a handshake that has
84 /// not completed.
85 ///
86 /// A chain that a handshake *failed over* never reaches here, since there
87 /// is no connection left to read it off; that is what
88 /// [`QuicDialOptions::observing`] is for.
89 pub fn peer_certificates(&self) -> Vec<Vec<u8>> {
90 peer_certificates(&self.conn)
91 }
92}
93
94/// Read a quinn connection's peer certificate chain as DER, leaf first.
95///
96/// Shared with the WebTransport arm, which reaches the same
97/// `quinn::Connection` through `wtransport`'s `quic_connection()`. One copy
98/// because there is exactly one fragile step here and it should not exist
99/// twice: `Connection::peer_identity` is `Option<Box<dyn Any>>`, since quinn is
100/// generic over its crypto backend and has no type it could name for every
101/// one. The rustls backend documents the concrete type as
102/// `Vec<rustls::pki_types::CertificateDer>` and quinn-proto builds exactly
103/// that, so the downcast is correct — and it is checked by nothing at compile
104/// time, so a quinn release that changed the type would turn this into a
105/// permanently empty chain rather than a build failure. That is what
106/// `tests/the_peer_certificate_chain_reaches_the_caller.rs` is for: it asserts
107/// the bytes handed back are the server's own certificate, byte for byte, so
108/// the silent version of that break cannot pass.
109///
110/// **Bytes, and no opinion about them.** Nothing here parses a certificate,
111/// checks a date, or decides whether a chain is trustworthy. A verdict depends
112/// on what the caller is measuring — a conformance probe grading a public relay
113/// wants "does this chain reach a public root", an operator on a private CA
114/// wants the opposite — and a library that guessed would be wrong for one of
115/// them while looking authoritative to both. The rule that keeps this honest:
116/// the transport reports what the peer sent, the caller decides what it means.
117///
118/// Empty is not an error and is never reported as one. A chain is absent for
119/// ordinary reasons — the handshake has not completed, the peer authenticated
120/// by some other means, the session was resumed without one — and none of them
121/// are faults this connection can do anything about. A caller that needs the
122/// distinction between "no chain" and "a chain we could not read" is asking a
123/// question the `Any` boundary above cannot answer anyway.
124///
125/// The returned `Vec<Vec<u8>>` owns its bytes rather than borrowing the
126/// connection's, which costs a copy per certificate and buys the thing callers
127/// actually need: a chain that outlives the connection it came from. A probe
128/// records the certificate and then closes the connection immediately, so a
129/// borrowed chain would have to be interpreted before the peer is released —
130/// exactly the ordering constraint this API exists to avoid imposing.
131pub(crate) fn peer_certificates(conn: &quinn::Connection) -> Vec<Vec<u8>> {
132 conn.peer_identity()
133 .and_then(|identity| {
134 identity.downcast::<Vec<rustls::pki_types::CertificateDer<'static>>>().ok()
135 })
136 .map(|chain| chain.iter().map(|der| der.as_ref().to_vec()).collect())
137 .unwrap_or_default()
138}
139
140/// Convert a quinn connection error to a TransportError.
141fn conn_err(e: quinn::ConnectionError) -> TransportError {
142 TransportError::Connection(e.to_string())
143}
144
145// ── From impls for quinn error types ────────────────────────
146
147impl From<quinn::ConnectionError> for TransportError {
148 fn from(e: quinn::ConnectionError) -> Self {
149 TransportError::Connection(e.to_string())
150 }
151}
152
153/// Take a handshake failure apart into codes plus prose.
154///
155/// The prose is quinn's own `Display`, unchanged, so nothing that reads the
156/// message loses anything by this; the codes are otherwise recoverable only by
157/// finding digits inside that message.
158///
159/// The three variants that carry a number are the three that matter to a
160/// conformance report: a peer's `CONNECTION_CLOSE`, our own stack's transport
161/// error (which is where a locally-detected certificate failure lands), and an
162/// application close. The rest — a timeout, a reset, a version mismatch — are
163/// findings with genuinely no code in them, and inventing one would be worse
164/// than `None`.
165pub(crate) fn handshake_failure(e: &quinn::ConnectionError) -> super::HandshakeFailure {
166 let reason = e.to_string();
167 match e {
168 quinn::ConnectionError::TransportError(inner) => {
169 super::HandshakeFailure::transport(u64::from(inner.code), reason)
170 }
171 quinn::ConnectionError::ConnectionClosed(close) => {
172 super::HandshakeFailure::transport(u64::from(close.error_code), reason)
173 }
174 quinn::ConnectionError::ApplicationClosed(close) => {
175 super::HandshakeFailure::application(close.error_code.into_inner(), reason)
176 }
177 _ => super::HandshakeFailure::bare(reason),
178 }
179}
180
181/// A lost connection, keeping the peer's close code where it named one.
182///
183/// Both stream error enums render `ConnectionLost` as the two words "connection
184/// lost" and drop the cause, so a session a relay ended deliberately — with a
185/// MoQT error code, and often a reason phrase saying why — reaches a caller as
186/// a sentence carrying neither. This reads them off the value instead.
187///
188/// Only `ApplicationClosed` carries an application code. A timeout, a stateless
189/// reset or a local close have genuinely no such number, and those keep
190/// quinn's own message.
191fn connection_lost(e: quinn::ConnectionError) -> TransportError {
192 match &e {
193 quinn::ConnectionError::ApplicationClosed(close) => TransportError::SessionClosed {
194 code: close.error_code.into_inner(),
195 reason: e.to_string(),
196 },
197 _ => TransportError::Connection(e.to_string()),
198 }
199}
200
201impl From<quinn::WriteError> for TransportError {
202 /// `Stopped` keeps the peer's application error code as a typed
203 /// [`TransportError::Stopped`] so a forwarder can mirror it; every
204 /// other cause collapses to a message.
205 fn from(e: quinn::WriteError) -> Self {
206 match e {
207 quinn::WriteError::Stopped(code) => TransportError::Stopped(code.into_inner()),
208 quinn::WriteError::ConnectionLost(lost) => connection_lost(lost),
209 other => TransportError::Write(other.to_string()),
210 }
211 }
212}
213
214impl From<quinn::ReadError> for TransportError {
215 /// `Reset` keeps the peer's application error code as a typed
216 /// [`TransportError::StreamReset`] so a forwarder can mirror it;
217 /// every other cause collapses to a message.
218 fn from(e: quinn::ReadError) -> Self {
219 match e {
220 quinn::ReadError::Reset(code) => TransportError::StreamReset(code.into_inner()),
221 quinn::ReadError::ConnectionLost(lost) => connection_lost(lost),
222 other => TransportError::Read(other.to_string()),
223 }
224 }
225}
226
227impl From<quinn::ReadExactError> for TransportError {
228 fn from(e: quinn::ReadExactError) -> Self {
229 match e {
230 quinn::ReadExactError::ReadError(inner) => inner.into(),
231 other => TransportError::Read(other.to_string()),
232 }
233 }
234}
235
236impl From<quinn::ConnectError> for TransportError {
237 fn from(e: quinn::ConnectError) -> Self {
238 TransportError::Connect(e.to_string())
239 }
240}
241
242impl From<quinn::ClosedStream> for TransportError {
243 fn from(_e: quinn::ClosedStream) -> Self {
244 TransportError::StreamClosed
245 }
246}
247
248impl From<quinn::SendDatagramError> for TransportError {
249 fn from(e: quinn::SendDatagramError) -> Self {
250 TransportError::SendDatagram(e.to_string())
251 }
252}
253
254// ---------------------------------------------------------------------------
255// Dialling
256// ---------------------------------------------------------------------------
257
258/// How a QUIC dial is configured, independent of any draft.
259///
260/// `alpn` is a list because ALPN is: one handshake offers several protocols and
261/// the server picks, which is how a caller that does not know a peer's draft
262/// finds out without dialling once per candidate.
263pub struct QuicDialOptions {
264 /// Skip TLS certificate verification. Testing only.
265 pub skip_cert_verification: bool,
266 /// Additional CA certificates to trust, DER-encoded, on top of the bundled
267 /// Mozilla roots every dial starts from, on either transport.
268 pub ca_certs: Vec<Vec<u8>>,
269 /// ALPN protocols to offer, in preference order.
270 ///
271 /// [`dial_quic`] returns the one the server selected. An empty list offers
272 /// nothing and is refused by any peer that requires ALPN, which every MoQT
273 /// relay does.
274 ///
275 /// A WebTransport dial ignores this field: that session is HTTP/3 by
276 /// definition and offers `h3` alone, so the protocol name a WebTransport
277 /// session negotiates is `WT-Available-Protocols` and not this. See
278 /// [`wt_protocols`](Self::wt_protocols). Everything else here applies to
279 /// both transports.
280 pub alpn: Vec<Vec<u8>>,
281 /// MOQT protocol identifiers to offer in the `WT-Available-Protocols`
282 /// header of a WebTransport dial, in preference order.
283 ///
284 /// WebTransport's answer to ALPN, and the reason a MoQT draft can be
285 /// negotiated over it at all. Drafts 15 and later state it in one sentence:
286 /// "MOQT uses ALPN in QUIC and `WT-Available-Protocols` in WebTransport
287 /// (\[WebTransport\], Section 3.3) to perform version negotiation" —
288 /// draft-15 cites Section 3.4 of the same document and is otherwise
289 /// word-for-word. Drafts 18 through 20 add the client's half of it: "The
290 /// client includes MOQT protocol identifiers in the WT-Available-Protocols
291 /// header". The identifiers are the ALPN names: `moqt-15` … `moqt-20`.
292 ///
293 /// Empty for drafts 07 through 14, which predate the header and settle
294 /// their version in CLIENT_SETUP instead. Empty is not the same as absent
295 /// by accident: a server that implements the negotiation and receives no
296 /// offer has nothing to select, and may reject the session outright.
297 /// imquic does, in as many words — "No WebTransport protocol offered".
298 ///
299 /// Ignored by a QUIC dial, where [`alpn`](Self::alpn) carries the same
300 /// names.
301 ///
302 /// # Reading the answer needs a patched `wtransport`
303 ///
304 /// A server names its choice in a `WT-Protocol` response header, and
305 /// upstream `wtransport` 0.7 drops the CONNECT response once it has judged
306 /// the status code. `Transport::wt_protocol` reads it — spelled as code
307 /// because it exists only behind this crate's `wt-protocol` feature, and a
308 /// link to it would be broken in every build without that feature. The
309 /// feature requires the patch in `moqtap/vendor/wtransport` and does not
310 /// build without it.
311 ///
312 /// Without that feature an accepted session says only that the server took
313 /// one of the offers or ignored the header, and only a *rejected* one is
314 /// conclusive — conclusive, then, about every identifier offered.
315 pub wt_protocols: Vec<Vec<u8>>,
316 /// Called with the peer's certificate chain during the handshake, **before
317 /// it is judged** — so it runs even for a chain that is about to be
318 /// rejected, which is the case it exists for. See [`CertificateHook`].
319 pub on_peer_certificates: Option<CertificateHook>,
320 /// Restrict the TLS 1.3 cipher suites offered, by IANA codepoint.
321 ///
322 /// `None` offers the crypto provider's full set, which is what an ordinary
323 /// client does and what every caller but a measuring one wants.
324 ///
325 /// `Some` exists because **the negotiated suite cannot be read back**.
326 /// quinn's `HandshakeData` carries the ALPN and the server name and nothing
327 /// else, and rustls does not surface the suite through it — so the only way
328 /// to learn which suite a peer accepts is to offer exactly one and see
329 /// whether the handshake completes. A successful dial *is* the measurement.
330 ///
331 /// Codepoints rather than a rustls enum so that a rustls upgrade cannot
332 /// change this crate's public API. The three TLS 1.3 suites are `0x1301`
333 /// AES-128-GCM-SHA256, `0x1302` AES-256-GCM-SHA384 and `0x1303`
334 /// CHACHA20-POLY1305-SHA256. A codepoint the provider does not have is
335 /// [`DialError::TlsConfig`] rather than a silent omission, because silently
336 /// offering fewer suites than asked would make every answer a false
337 /// negative.
338 ///
339 /// Excluding `0x1301` is allowed, and is the interesting case. QUIC's
340 /// Initial packets must use AES-128-GCM and normally that makes such an
341 /// offer unbuildable; the initial keys are taken from the default provider
342 /// separately so that only the *traffic* suites are restricted.
343 ///
344 /// Not honoured by `webtransport::dial_webtransport_to`, which reaches
345 /// `wtransport`'s builder and cannot supply a separate initial suite
346 /// through it. Spelled as code and not as a link on purpose: that module is
347 /// behind the `webtransport` feature, and `just doc-check` builds these
348 /// docs with the feature off.
349 pub cipher_suites: Option<Vec<u16>>,
350}
351
352impl QuicDialOptions {
353 /// Options offering `alpn`, verifying certificates against the bundled
354 /// Mozilla roots, observing nothing.
355 ///
356 /// A constructor and not a `Default` because there is no sensible default
357 /// ALPN: an empty list is refused by every MoQT relay, so a
358 /// `QuicDialOptions::default()` would be a value whose only outcome is TLS
359 /// alert 120 — which in a conformance report reads as a defect in the
360 /// relay rather than in the caller. Naming the offer is the one thing a
361 /// dial cannot do without.
362 ///
363 /// It is also the base for functional update, which is how the fields
364 /// below stay additive:
365 ///
366 /// ```ignore
367 /// QuicDialOptions { skip_cert_verification: true, ..QuicDialOptions::new(alpn) }
368 /// ```
369 pub fn new(alpn: Vec<Vec<u8>>) -> Self {
370 Self {
371 skip_cert_verification: false,
372 ca_certs: Vec::new(),
373 alpn,
374 wt_protocols: Vec::new(),
375 on_peer_certificates: None,
376 cipher_suites: None,
377 }
378 }
379
380 /// Accept any certificate. Testing only — see
381 /// [`skip_cert_verification`](Self::skip_cert_verification).
382 pub fn insecure(mut self, yes: bool) -> Self {
383 self.skip_cert_verification = yes;
384 self
385 }
386
387 /// Trust these DER-encoded CAs on top of the bundled roots.
388 pub fn ca_certs(mut self, certs: Vec<Vec<u8>>) -> Self {
389 self.ca_certs = certs;
390 self
391 }
392
393 /// Observe the peer's certificate chain, accepted or not.
394 ///
395 /// [`CertificateLog`] is the collector most callers want:
396 ///
397 /// ```ignore
398 /// let log = CertificateLog::new();
399 /// let result = dial_quic_to(&target, &QuicDialOptions::new(alpn).observing(log.hook())).await;
400 /// let chain = log.chain();
401 /// ```
402 pub fn observing(mut self, hook: CertificateHook) -> Self {
403 self.on_peer_certificates = Some(hook);
404 self
405 }
406
407 /// Offer only these cipher suites, by IANA codepoint. See
408 /// [`cipher_suites`](Self::cipher_suites).
409 pub fn offering_cipher_suites(mut self, suites: Vec<u16>) -> Self {
410 self.cipher_suites = Some(suites);
411 self
412 }
413
414 /// Offer these MOQT protocol identifiers to a WebTransport dial. See
415 /// [`wt_protocols`](Self::wt_protocols).
416 pub fn offering_wt_protocols(mut self, protocols: Vec<Vec<u8>>) -> Self {
417 self.wt_protocols = protocols;
418 self
419 }
420}
421
422/// The TLS 1.3 cipher suites, as IANA codepoints.
423///
424/// The whole set: TLS 1.3 defines five and rustls implements the three that
425/// QUIC can use. Named here so a caller enumerating support does not have to
426/// hardcode the numbers, and so [`show_cipher_suite`] and this list cannot
427/// drift apart.
428pub const TLS13_CIPHER_SUITES: [u16; 3] = [0x1301, 0x1302, 0x1303];
429
430/// The IANA name of a TLS 1.3 cipher suite codepoint.
431///
432/// Returns `None` for anything outside [`TLS13_CIPHER_SUITES`] rather than
433/// inventing a label, so an unknown number is reported as the number.
434pub fn show_cipher_suite(code: u16) -> Option<&'static str> {
435 match code {
436 0x1301 => Some("TLS_AES_128_GCM_SHA256"),
437 0x1302 => Some("TLS_AES_256_GCM_SHA384"),
438 0x1303 => Some("TLS_CHACHA20_POLY1305_SHA256"),
439 _ => None,
440 }
441}
442
443/// Where packets go, and whose certificate to expect when they arrive.
444///
445/// Two fields and not one `host:port` string, because they are two different
446/// things and a public relay is where that stops being pedantry. The socket
447/// layer needs an address it can send to; the TLS layer needs the name a
448/// certificate was issued for. Deriving the second from the first — which is
449/// what a single string forces — leaves only bad options: dial the hostname
450/// and `parse::<SocketAddr>()` rejects it, or resolve first and then validate
451/// a public relay's certificate against an IP literal that no CA ever put in
452/// a SAN. [`dial_quic`] is where both are avoided.
453pub struct QuicTarget {
454 /// The address packets are sent to. Already resolved — nothing in this
455 /// module does DNS on it.
456 pub addr: SocketAddr,
457 /// The name offered in SNI and validated against the server's certificate.
458 ///
459 /// An IP literal is legal and is what a loopback peer holding an IP-SAN
460 /// certificate wants; a hostname is what a public relay wants. Keeping it
461 /// separate is what lets one caller ask for both against one address.
462 pub server_name: String,
463}
464
465/// Why a QUIC dial did not produce a connection.
466///
467/// Separate from [`TransportError`] so the failures that happen before any
468/// packet is sent — a bad address, a socket this machine would not open, a TLS
469/// config it rejects — stay distinguishable from a peer that would not talk
470/// to us. [`DialError::phase`] is that distinction as a value.
471///
472/// # Why there is a variant for a socket that would not open
473///
474/// A bind this machine refused is this side's failure, and the type is what has
475/// to say so. Folding it into [`InvalidAddress`](Self::InvalidAddress) leaves
476/// the message as the only thing telling the two apart — a substring test
477/// wearing a type's clothes. A consumer reading it that way recovers "this
478/// machine has no IPv6 stack" only where the prose happens to contain `could
479/// not bind`, and files the result at whichever phase the *caller* named, which
480/// for a single call to [`dial_quic_to`] is one word for four stages: a failed
481/// v6 bind then publishes as a relay that failed a QUIC handshake, on a dial
482/// where no packet left the machine.
483///
484/// The WebTransport arm offers no such substring to find. `wtransport`'s
485/// endpoint constructor binds the socket too and its failure carries no `could
486/// not bind` in the message, so a `TransportError` there reaches
487/// [`ErrorCause::Transport`](crate::dispatch::ErrorCause::Transport) through a
488/// draft's `ConnectionError`, and `is_local` answers **false** for it — a
489/// socket this machine could not open, filed against the peer. Both arms raise
490/// this variant.
491///
492/// # Why it goes no further than this type
493///
494/// The per-draft `From<DialError> for ConnectionError` impls map this variant
495/// onto `ConnectionError::InvalidAddress`, which is one of the variants the
496/// facade reads as [`ErrorCause::Facade`](crate::dispatch::ErrorCause::Facade),
497/// and with it `is_local() == true` — the right answer for a failure this side
498/// decided. Splitting it further there would add public variants for a
499/// distinction no caller on that path reads: a draft `Connection` is built by a
500/// caller who named a `host:port`, not by one measuring which stage of a dial
501/// died. The reader of the distinction is a caller of [`dial_quic_to`], and it
502/// reads it off this type directly.
503#[derive(Debug, thiserror::Error)]
504pub enum DialError {
505 /// `addr` is not a `host:port` this machine can resolve to a socket address.
506 #[error("invalid address: {0}")]
507 InvalidAddress(String),
508 /// A local socket could not be opened to send from.
509 ///
510 /// Nothing about the target: the address is fine and this machine would not
511 /// give us a socket to reach it with. The common cause by a distance is
512 /// dialling a v6 address from a host or container with no IPv6 stack, which
513 /// is a routine outcome for anything that enumerates both families of a
514 /// dual-stack name — and a fact about the dialler, never about the peer.
515 #[error("local socket error: {0}")]
516 LocalSocket(String),
517 /// The TLS client configuration could not be built.
518 #[error("TLS configuration error: {0}")]
519 TlsConfig(String),
520 /// The dial itself failed.
521 #[error(transparent)]
522 Transport(#[from] TransportError),
523}
524
525/// Which stage of a dial a [`DialError`] came from.
526///
527/// A dial is not one event. [`dial_quic_to`] builds a TLS configuration, opens a
528/// local socket, asks quinn to start a connection, and only then sends a packet;
529/// the four fail for unrelated reasons and only the last of them involves the
530/// peer at all. This is that sequence as a value, so a caller can switch on it
531/// instead of reading the message a variant renders.
532///
533/// # The caller cannot supply this itself
534///
535/// It knows which *call* it made, not which stage inside the call died — and
536/// the four stages above live inside one call. The one thing it does know that
537/// this cannot is which transport it was dialling, which is why
538/// [`Handshake`](Self::Handshake) does not distinguish a QUIC handshake from a
539/// WebTransport CONNECT: both are a peer answering, and only the caller knows
540/// which it asked.
541///
542/// # Exactly one of these means the peer was involved
543///
544/// [`Handshake`](Self::Handshake), and [`DialError::is_local`] is that sentence
545/// as a predicate. The other four are decided on this machine with nothing on
546/// the wire, so a failure carrying one of them can never be evidence about the
547/// peer — which is the direction this distinction is load-bearing in. The
548/// converse is weaker and deliberately not claimed: a handshake that timed out
549/// is not proof of anything the peer did either, only that the machine got as
550/// far as sending.
551#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
552pub enum DialPhase {
553 /// Turning what the caller named into somewhere to send — parsing a
554 /// `host:port`, resolving it, finding it resolved to nothing.
555 Address,
556 /// Opening a local socket to send from. See [`DialError::LocalSocket`].
557 LocalSocket,
558 /// Building the TLS client configuration: a `ca_certs` entry that is not a
559 /// certificate, a cipher suite the crypto provider does not implement.
560 TlsConfig,
561 /// The stack refusing to start the connection — a server name rustls will
562 /// not accept, a remote address quinn will not dial. Nothing was sent.
563 Connect,
564 /// The handshake, and the only phase in which the peer is involved.
565 Handshake,
566}
567
568impl DialError {
569 /// Which stage of the dial this failure came from.
570 ///
571 /// Total and structural: every variant answers, and no arm reads a message.
572 /// [`DialError::Transport`] splits on whether the transport error is the
573 /// typed [`TransportError::Handshake`] — which by construction exists only
574 /// where a peer answered — so everything else under it is the stack
575 /// declining to start, which is [`DialPhase::Connect`].
576 pub fn phase(&self) -> DialPhase {
577 match self {
578 DialError::InvalidAddress(_) => DialPhase::Address,
579 DialError::LocalSocket(_) => DialPhase::LocalSocket,
580 DialError::TlsConfig(_) => DialPhase::TlsConfig,
581 DialError::Transport(TransportError::Handshake(_)) => DialPhase::Handshake,
582 DialError::Transport(_) => DialPhase::Connect,
583 }
584 }
585
586 /// Whether the dial failed **before a packet left this machine**.
587 ///
588 /// True for everything but [`DialPhase::Handshake`]. The name matches
589 /// [`AnyConnectionError::is_local`](crate::dispatch::AnyConnectionError::is_local),
590 /// which answers the same question for everything after the dial, and it is
591 /// worth being exact about what each half of the answer buys:
592 ///
593 /// - **`true` is a guarantee.** Nothing was sent, so the failure cannot be
594 /// evidence about the peer, and anything that publishes it as such is
595 /// publishing a finding that never happened.
596 /// - **`false` is not the opposite guarantee.** It says the machine got as
597 /// far as sending, and a handshake can still fail for reasons that are
598 /// nobody's fault in particular — a timeout, a network that dropped the
599 /// packets. Read [`TransportError::Handshake`]'s codes for what the peer
600 /// actually said; this only says whether there was a peer to ask.
601 pub fn is_local(&self) -> bool {
602 self.phase() != DialPhase::Handshake
603 }
604}
605
606/// The one place either transport decides what a server certificate is checked
607/// against.
608///
609/// Shared, and that is the whole point of it. Two transports answering this
610/// question apart from each other answer it differently: a `RootCertStore`
611/// seeded from the `webpki-roots` bundle with `ca_certs` added to it on one
612/// side, `wtransport`'s `with_native_certs()` — the *OS* trust store, which
613/// never sees `ca_certs` — on the other. Two consequences, both of which a
614/// conformance run publishes as facts about the relay:
615///
616/// - The bundled Mozilla set and a machine's own set are not the same set. They
617/// diverge on newly-added roots, on roots a distribution has retired early,
618/// and on whatever a corporate MITM appliance installed. A relay would
619/// therefore pass over QUIC and fail over WebTransport with nothing about the
620/// relay differing between the two dials.
621/// - A caller supplying a private CA would have it honoured on one transport
622/// and silently dropped on the other. Not an error, not a warning — a
623/// handshake failure that looks exactly like a relay presenting a bad chain.
624///
625/// Bundled roots for both, rather than native for both, because a published
626/// measurement has to be reproducible: `webpki-roots` is a fixed set compiled
627/// into the binary, so two runs on two machines validate against the same
628/// anchors and any difference in the outcome is a difference in the relay. The
629/// cost is that a peer whose chain the operator trusts only via the OS store is
630/// not trusted here — that operator passes the CA in `ca_certs`, which is what
631/// the field is for and why it reaches both transports.
632///
633/// `alpn` is a parameter and not read off `options` because it is the one part
634/// of the handshake the two transports do not share: a QUIC dial offers the
635/// draft ALPNs it wants the server to choose between, a WebTransport session
636/// offers `h3` and nothing else. Everything a certificate is judged by comes
637/// from `options`.
638///
639/// Returns [`DialError::TlsConfig`] for a `ca_certs` entry that is not a
640/// parseable certificate, and for a crypto provider without the AES-128-GCM
641/// suite QUIC's initial packets are obliged to use.
642///
643/// # The initial suite is separate from the offered suites
644///
645/// QUIC encrypts its Initial packets with AES-128-GCM and has no say in the
646/// matter (RFC 9001 §5.2), which normally makes that suite impossible to leave
647/// out of an offer — and therefore makes "does this peer accept *only*
648/// ChaCha20" unaskable. `QuicClientConfig::with_initial` exists for exactly
649/// this: it takes the initial keys from one suite and lets the TLS config offer
650/// another. So when [`QuicDialOptions::cipher_suites`] excludes AES-128-GCM,
651/// the initial suite is taken from the process default provider and only the
652/// *traffic* suites are restricted.
653pub(crate) fn client_config(
654 options: &QuicDialOptions,
655 alpn: Vec<Vec<u8>>,
656) -> Result<quinn::ClientConfig, DialError> {
657 use std::sync::Arc;
658
659 let tls_config = Arc::new(rustls_client_config(options, alpn)?);
660
661 let has_initial = tls_config
662 .crypto_provider()
663 .cipher_suites
664 .iter()
665 .any(|cs| cs.suite() == rustls::CipherSuite::TLS13_AES_128_GCM_SHA256);
666
667 let quic_config = if has_initial {
668 quinn::crypto::rustls::QuicClientConfig::try_from(tls_config)
669 .map_err(|e| DialError::TlsConfig(format!("{e}")))?
670 } else {
671 quinn::crypto::rustls::QuicClientConfig::with_initial(tls_config, initial_suite()?)
672 .map_err(|e| DialError::TlsConfig(format!("{e}")))?
673 };
674 Ok(quinn::ClientConfig::new(Arc::new(quic_config)))
675}
676
677/// The AES-128-GCM keys QUIC's Initial packets require, from the default
678/// provider.
679///
680/// Read from the process default rather than from the dial's own (possibly
681/// restricted) provider, because the whole point of reaching this function is
682/// that the dial's provider deliberately does not have it.
683fn initial_suite() -> Result<rustls::quic::Suite, DialError> {
684 default_provider()
685 .cipher_suites
686 .iter()
687 .find(|cs| cs.suite() == rustls::CipherSuite::TLS13_AES_128_GCM_SHA256)
688 .and_then(|cs| cs.tls13())
689 .and_then(|cs| cs.quic_suite())
690 .ok_or_else(|| {
691 DialError::TlsConfig(
692 "the crypto provider has no TLS13_AES_128_GCM_SHA256, which QUIC's initial \
693 packets require"
694 .to_string(),
695 )
696 })
697}
698
699/// The process-wide crypto provider, or `ring` if none was installed.
700fn default_provider() -> std::sync::Arc<rustls::crypto::CryptoProvider> {
701 rustls::crypto::CryptoProvider::get_default()
702 .cloned()
703 .unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::ring::default_provider()))
704}
705
706/// The rustls half of [`client_config`], before quinn wraps it.
707///
708/// Split out for the WebTransport dial, which cannot use the quinn form: the
709/// only `wtransport` builder state carrying `dns_resolver` — the hook that lets
710/// a caller choose which address a session goes to — is reached through
711/// `with_custom_tls`, which takes a `rustls::ClientConfig`. The state that
712/// accepts a ready-made quinn config has no such hook.
713///
714/// Everything that decides a certificate verdict lives here, so both transports
715/// still get it from one place — including
716/// [`on_peer_certificates`](QuicDialOptions::on_peer_certificates), which is
717/// installed inside the verifier because that is the only place the chain
718/// exists when a handshake is going to fail. Reading it off the finished
719/// connection — what [`super::Transport::peer_certificates`] does — works only
720/// when there *is* a finished connection, and the certificates worth reporting
721/// on are disproportionately the ones that stopped a handshake from finishing.
722pub(crate) fn rustls_client_config(
723 options: &QuicDialOptions,
724 alpn: Vec<Vec<u8>>,
725) -> Result<rustls::ClientConfig, DialError> {
726 use std::sync::Arc;
727
728 // The verifier the caller's options ask for, before any recording.
729 let verifier: Arc<dyn rustls::client::danger::ServerCertVerifier> =
730 if options.skip_cert_verification {
731 Arc::new(SkipVerification::new())
732 } else {
733 let mut roots = rustls::RootCertStore::empty();
734 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
735 for der in &options.ca_certs {
736 roots
737 .add(rustls::pki_types::CertificateDer::from(der.clone()))
738 .map_err(|e| DialError::TlsConfig(format!("bad CA cert: {e}")))?;
739 }
740 rustls::client::WebPkiServerVerifier::builder(Arc::new(roots))
741 .build()
742 .map_err(|e| DialError::TlsConfig(format!("{e}")))?
743 };
744
745 // Wrapping is what keeps observing orthogonal to judging: the decorator
746 // copies the chain and then hands the same arguments to the verifier that
747 // would have run anyway, so an observed dial and an unobserved one reach
748 // identical verdicts. Anything else would make the act of measuring change
749 // the measurement.
750 let verifier = match &options.on_peer_certificates {
751 Some(hook) => Arc::new(ObservingVerifier { inner: verifier, hook: Arc::clone(hook) })
752 as Arc<dyn rustls::client::danger::ServerCertVerifier>,
753 None => verifier,
754 };
755
756 // `builder()` when nothing is restricted, so the default path is byte for
757 // byte what it always was; the provider form only when a caller is
758 // measuring. TLS 1.3 is pinned explicitly there because that builder does
759 // not default to it and QUIC permits nothing else.
760 let builder = match &options.cipher_suites {
761 None => rustls::ClientConfig::builder(),
762 Some(wanted) => {
763 rustls::ClientConfig::builder_with_provider(Arc::new(restricted_provider(wanted)?))
764 .with_protocol_versions(&[&rustls::version::TLS13])
765 .map_err(|e| DialError::TlsConfig(format!("{e}")))?
766 }
767 };
768
769 let mut tls_config =
770 builder.dangerous().with_custom_certificate_verifier(verifier).with_no_client_auth();
771
772 tls_config.alpn_protocols = alpn;
773 Ok(tls_config)
774}
775
776/// The default provider with its cipher suites narrowed to `wanted`.
777///
778/// A requested suite the provider does not implement is an error and not a
779/// silent omission. Offering fewer suites than asked would make a refusal
780/// indistinguishable from a suite that was never on the wire, which turns every
781/// negative result into a possible false one — and this field exists only to
782/// produce negative results that mean something.
783fn restricted_provider(wanted: &[u16]) -> Result<rustls::crypto::CryptoProvider, DialError> {
784 let base = default_provider();
785
786 let mut suites = Vec::with_capacity(wanted.len());
787 for code in wanted {
788 let found = base.cipher_suites.iter().find(|cs| u16::from(cs.suite()) == *code);
789 match found {
790 Some(cs) => suites.push(*cs),
791 None => {
792 let name = show_cipher_suite(*code).unwrap_or("unknown");
793 return Err(DialError::TlsConfig(format!(
794 "the crypto provider does not implement cipher suite {code:#06x} ({name})"
795 )));
796 }
797 }
798 }
799 if suites.is_empty() {
800 return Err(DialError::TlsConfig(
801 "an empty cipher suite list offers nothing and no peer can answer it".to_string(),
802 ));
803 }
804
805 Ok(rustls::crypto::CryptoProvider { cipher_suites: suites, ..(*base).clone() })
806}
807
808/// Dial one already-resolved address, and report which ALPN the server chose.
809///
810/// This is the whole dial with nothing decided for the caller: one address,
811/// one server name, one ALPN offer, one answer. [`dial_quic`] is the
812/// convenience wrapper that resolves a `host:port` and picks an address; a
813/// caller measuring *which* address or *which* name a relay answers on wants
814/// this one, so that each attempt fails on its own and is recorded on its own.
815///
816/// The second return value is the protocol the server chose from `options.alpn`,
817/// `None` if it selected none.
818/// [`DraftVersion::from_alpn`](moqtap_codec::version::DraftVersion::from_alpn)
819/// names a draft for five of the six; drafts 07-14 share `moq-00` and settle
820/// their version in CLIENT_SETUP.
821///
822/// The endpoint is dropped when the dial returns — quinn keeps the connection's
823/// driver alive independently.
824pub async fn dial_quic_to(
825 target: &QuicTarget,
826 options: &QuicDialOptions,
827) -> Result<(super::Transport, Option<Vec<u8>>), DialError> {
828 let server_addr = target.addr;
829
830 // Built before the socket, so a CA the caller cannot have meant fails
831 // without a packet leaving the machine. Through `client_config` and not
832 // inline, so that this dial and the WebTransport one agree about the
833 // initial cipher suite as well as about certificates.
834 let config = client_config(options, options.alpn.clone())?;
835
836 // Bind in the target's address family. A socket bound to `0.0.0.0` cannot
837 // send to a v6 peer, and quinn reports that as `invalid remote address` —
838 // which reads like the address was malformed when it was only unreachable
839 // from the socket we opened.
840 let bind: SocketAddr = match server_addr {
841 SocketAddr::V4(_) => (Ipv4Addr::UNSPECIFIED, 0).into(),
842 SocketAddr::V6(_) => (Ipv6Addr::UNSPECIFIED, 0).into(),
843 };
844 let mut endpoint = quinn::Endpoint::client(bind).map_err(|e| {
845 // Not `InvalidAddress` about the *target*: the target is fine and this
846 // machine could not open a local socket to reach it. `LocalSocket`
847 // keeps that distinction in the type rather than in the message.
848 DialError::LocalSocket(format!("could not bind a local {} socket: {e}", family(bind)))
849 })?;
850 endpoint.set_default_client_config(config);
851
852 let quic = endpoint
853 .connect(server_addr, &target.server_name)
854 .map_err(TransportError::from)?
855 .await
856 // Not the blanket `From`, which flattens to a message: this is the one
857 // place a peer's refusal is still typed, and a caller measuring *why* a
858 // relay refused needs the code rather than a sentence containing it.
859 .map_err(|e| TransportError::Handshake(handshake_failure(&e)))?;
860
861 let negotiated = negotiated_alpn(&quic);
862 Ok((super::Transport::Quic(QuicTransport::new(quic)), negotiated))
863}
864
865/// Resolve `addr` and dial the first address that answers.
866///
867/// Keeps the signature every draft module's `connect_quic` calls.
868///
869/// Why it resolves rather than parses: `addr.parse::<SocketAddr>()` accepts
870/// numeric literals and nothing else, so every hostname fails with `invalid
871/// address: invalid socket address syntax` before a packet is sent — a loopback
872/// interop suite passes while every public relay is unreachable. Resolving in
873/// the caller is not an answer either: the resolved IP becomes the SNI and the
874/// certificate check runs against it. `resolve` hands back the name and the
875/// addresses separately, which is what [`QuicTarget`] has two fields for.
876///
877/// One connection is what this returns, so one is what it looks for; a caller
878/// that needs to know an address failed, rather than that some address worked,
879/// wants [`dial_quic_to`] per address instead.
880pub async fn dial_quic(
881 addr: &str,
882 options: &QuicDialOptions,
883) -> Result<(super::Transport, Option<Vec<u8>>), DialError> {
884 let (server_name, addrs) = resolve(addr).await?;
885
886 let mut last: Option<(SocketAddr, DialError)> = None;
887 for candidate in addrs {
888 let target = QuicTarget { addr: candidate, server_name: server_name.clone() };
889 match dial_quic_to(&target, options).await {
890 Ok(connected) => return Ok(connected),
891 Err(e) => last = Some((candidate, e)),
892 }
893 }
894
895 match last {
896 Some((candidate, e)) => Err(e_at(candidate, e)),
897 // `resolve` refuses to return an empty list, so the loop always ran.
898 None => Err(DialError::InvalidAddress(format!("{addr} resolved to no addresses"))),
899 }
900}
901
902/// Name the address a flattened multi-address failure came from.
903///
904/// `dial_quic` collapses several attempts into one error, which is exactly the
905/// lossiness a probe must not have — and is fine here, because a caller of
906/// `dial_quic` asked for one connection and not for a measurement. Saying
907/// which address produced the surviving error is the least it can do.
908///
909/// # It must not relabel the phase while it does that
910///
911/// Every arm returns the variant it was given, and that is a correctness
912/// requirement rather than tidiness: [`DialError::phase`] reads the variant, so
913/// an arm that rewrote a peer's refusal into a `Connect` in order to prefix the
914/// address would make `is_local` answer **true** for a relay that answered a
915/// handshake, and would throw that handshake's codes away with it. Prefixing a
916/// peer's failure means going inside [`HandshakeFailure`] and prefixing the
917/// reason, which keeps its codes as well as its phase.
918///
919/// [`HandshakeFailure`]: super::HandshakeFailure
920fn e_at(addr: SocketAddr, e: DialError) -> DialError {
921 match e {
922 DialError::InvalidAddress(m) => DialError::InvalidAddress(format!("{addr}: {m}")),
923 DialError::LocalSocket(m) => DialError::LocalSocket(format!("{addr}: {m}")),
924 DialError::TlsConfig(m) => DialError::TlsConfig(format!("{addr}: {m}")),
925 DialError::Transport(TransportError::Handshake(failure)) => {
926 DialError::Transport(TransportError::Handshake(super::HandshakeFailure {
927 reason: format!("{addr}: {}", failure.reason),
928 ..failure
929 }))
930 }
931 DialError::Transport(inner) => {
932 DialError::Transport(TransportError::Connect(format!("{addr}: {inner}")))
933 }
934 }
935}
936
937/// Split `host:port` into the name to validate against and every address it
938/// resolves to, v4 first.
939///
940/// v4 first is a preference and not a correctness claim: a v6 attempt on a
941/// host without v6 connectivity burns the caller's entire timeout before v4
942/// is reached, and this path exists to return one working connection quickly.
943/// It is the wrong default for measuring a relay, which is why a probe should
944/// enumerate the list itself and dial each address through [`dial_quic_to`]
945/// rather than inherit this ordering.
946async fn resolve(addr: &str) -> Result<(String, Vec<SocketAddr>), DialError> {
947 // A literal is its own answer and its own server name. Loopback peers are
948 // dialled this way, and one holding an IP-SAN certificate has to keep
949 // validating against the IP — so this cannot be routed through the
950 // hostname path even though `lookup_host` would accept it.
951 if let Ok(sock) = addr.parse::<SocketAddr>() {
952 return Ok((sock.ip().to_string(), vec![sock]));
953 }
954
955 let host = host_of(addr)?;
956 let mut resolved: Vec<SocketAddr> = tokio::net::lookup_host(addr)
957 .await
958 .map_err(|e| DialError::InvalidAddress(format!("could not resolve {addr}: {e}")))?
959 .collect();
960
961 if resolved.is_empty() {
962 return Err(DialError::InvalidAddress(format!("{addr} resolved to no addresses")));
963 }
964 resolved.sort_by_key(|a| a.is_ipv6());
965 Ok((host, resolved))
966}
967
968/// The host part of `host:port`, with the port removed and brackets stripped.
969///
970/// `rsplit_once(':')` alone is wrong for `[::1]:443`: it would cut at the last
971/// colon inside the address and hand back `[::1]` including the bracket, which
972/// is not a name rustls will accept.
973fn host_of(addr: &str) -> Result<String, DialError> {
974 if let Some(rest) = addr.strip_prefix('[') {
975 return rest
976 .split_once(']')
977 .map(|(host, _)| host.to_string())
978 .ok_or_else(|| DialError::InvalidAddress(format!("unclosed '[' in {addr}")));
979 }
980 addr.rsplit_once(':')
981 .map(|(host, _)| host.to_string())
982 .filter(|host| !host.is_empty())
983 .ok_or_else(|| DialError::InvalidAddress(format!("{addr} is not host:port")))
984}
985
986/// `"IPv4"` or `"IPv6"`, for the one error message that needs to say which.
987fn family(addr: SocketAddr) -> &'static str {
988 if addr.is_ipv4() {
989 "IPv4"
990 } else {
991 "IPv6"
992 }
993}
994
995/// The ALPN the server selected, if the handshake recorded one.
996fn negotiated_alpn(conn: &quinn::Connection) -> Option<Vec<u8>> {
997 conn.handshake_data()?.downcast::<quinn::crypto::rustls::HandshakeData>().ok()?.protocol
998}
999
1000/// Called with a certificate chain observed during a handshake, DER, leaf
1001/// first.
1002///
1003/// A hook and not a return value because a verifier that is about to reject a
1004/// certificate has no return path that carries one — it answers with an error,
1005/// and the bytes it was judging go out of scope. The interesting certificates
1006/// are exactly the rejected ones, so the observation has to happen where the
1007/// judging does.
1008///
1009/// Two things about where it runs. It is called from inside the TLS handshake,
1010/// on quinn's connection driver task rather than on the task that called the
1011/// dial, so a hook that blocks stalls the connection and one that panics
1012/// unwinds into the driver — keep it to moving bytes somewhere. And
1013/// [`dial_quic`] calls it once per resolved address it tries, with nothing in
1014/// the arguments to say which; a caller that needs to attribute a chain to an
1015/// address should dial each one with [`dial_quic_to`].
1016pub type CertificateHook = std::sync::Arc<dyn Fn(&[Vec<u8>]) + Send + Sync>;
1017
1018/// The [`CertificateHook`] most callers want: keep the chain, read it after.
1019///
1020/// Shipped rather than left to each caller because the storage behind this hook
1021/// is the same shared, lock-guarded vector every time, and because it survives
1022/// cancellation — a dial abandoned by `tokio::time::timeout` still leaves
1023/// whatever chain it had already seen in the log, where a chain returned
1024/// alongside the dial's result would be dropped with the future.
1025#[derive(Clone, Default)]
1026pub struct CertificateLog(std::sync::Arc<std::sync::Mutex<Vec<Vec<u8>>>>);
1027
1028impl CertificateLog {
1029 /// An empty log.
1030 pub fn new() -> Self {
1031 Self::default()
1032 }
1033
1034 /// The hook to hand to
1035 /// [`QuicDialOptions::observing`](QuicDialOptions::observing).
1036 pub fn hook(&self) -> CertificateHook {
1037 let slot = std::sync::Arc::clone(&self.0);
1038 std::sync::Arc::new(move |chain: &[Vec<u8>]| {
1039 if let Ok(mut seen) = slot.lock() {
1040 *seen = chain.to_vec();
1041 }
1042 })
1043 }
1044
1045 /// The last chain observed, empty if the handshake never got as far as one.
1046 ///
1047 /// Empty therefore means *no certificate was offered* — a refused ALPN, a
1048 /// dead port, a timeout — and never *the certificate was unreadable*.
1049 pub fn chain(&self) -> Vec<Vec<u8>> {
1050 self.0.lock().map(|seen| seen.clone()).unwrap_or_default()
1051 }
1052}
1053
1054impl std::fmt::Debug for CertificateLog {
1055 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1056 f.debug_tuple("CertificateLog").field(&self.chain().len()).finish()
1057 }
1058}
1059
1060/// A verifier that writes down the chain it was shown, then defers to another.
1061///
1062/// It exists because the interesting certificates are the rejected ones. A
1063/// relay with an expired certificate, a private CA, or a name that does not
1064/// match never completes a handshake, so nothing is left afterwards to read the
1065/// chain off — and "the handshake failed for a certificate reason" without the
1066/// certificate is exactly the report a relay operator cannot act on.
1067///
1068/// It observes unconditionally and judges not at all. The inner verifier's
1069/// verdict is returned untouched, so wrapping cannot turn a rejection into an
1070/// acceptance however the observation goes.
1071struct ObservingVerifier {
1072 inner: std::sync::Arc<dyn rustls::client::danger::ServerCertVerifier>,
1073 hook: CertificateHook,
1074}
1075
1076// Hand-written because `rustls::client::danger::ServerCertVerifier` requires
1077// `Debug` of the verifier, and `#[derive]` would push that requirement onto the
1078// hook — which would make `CertificateHook` a trait with a `Debug` supertrait
1079// rather than a closure, for no benefit to anyone but this line.
1080impl std::fmt::Debug for ObservingVerifier {
1081 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1082 f.debug_struct("ObservingVerifier").field("inner", &self.inner).finish_non_exhaustive()
1083 }
1084}
1085
1086impl rustls::client::danger::ServerCertVerifier for ObservingVerifier {
1087 fn verify_server_cert(
1088 &self,
1089 end_entity: &rustls::pki_types::CertificateDer<'_>,
1090 intermediates: &[rustls::pki_types::CertificateDer<'_>],
1091 server_name: &rustls::pki_types::ServerName<'_>,
1092 ocsp_response: &[u8],
1093 now: rustls::pki_types::UnixTime,
1094 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
1095 // Observe before judging, so a rejection still leaves the evidence
1096 // behind. Leaf first, matching what a finished connection reports.
1097 let mut chain = Vec::with_capacity(1 + intermediates.len());
1098 chain.push(end_entity.as_ref().to_vec());
1099 chain.extend(intermediates.iter().map(|der| der.as_ref().to_vec()));
1100 (self.hook)(&chain);
1101
1102 self.inner.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
1103 }
1104
1105 fn verify_tls12_signature(
1106 &self,
1107 message: &[u8],
1108 cert: &rustls::pki_types::CertificateDer<'_>,
1109 dss: &rustls::DigitallySignedStruct,
1110 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1111 self.inner.verify_tls12_signature(message, cert, dss)
1112 }
1113
1114 fn verify_tls13_signature(
1115 &self,
1116 message: &[u8],
1117 cert: &rustls::pki_types::CertificateDer<'_>,
1118 dss: &rustls::DigitallySignedStruct,
1119 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1120 self.inner.verify_tls13_signature(message, cert, dss)
1121 }
1122
1123 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1124 self.inner.supported_verify_schemes()
1125 }
1126}
1127
1128/// TLS certificate verifier that skips all verification. Testing only.
1129#[derive(Debug)]
1130struct SkipVerification {
1131 /// The provider whose signature schemes this verifier advertises.
1132 ///
1133 /// Held rather than hardcoded because
1134 /// [`supported_verify_schemes`](SkipVerification::supported_verify_schemes)
1135 /// is not a claim about what this verifier checks — it checks nothing — but
1136 /// about what the *ClientHello* offers. See that method for why the
1137 /// distinction has teeth.
1138 provider: std::sync::Arc<rustls::crypto::CryptoProvider>,
1139}
1140
1141impl SkipVerification {
1142 /// Use whichever provider this process installed, falling back to the one
1143 /// this crate compiles with.
1144 ///
1145 /// Taking the installed provider rather than naming `ring` unconditionally
1146 /// keeps the verifier's advertised schemes in step with the schemes the
1147 /// rest of the handshake was actually built from, however the embedding
1148 /// binary configured rustls.
1149 fn new() -> Self {
1150 Self { provider: default_provider() }
1151 }
1152}
1153
1154impl rustls::client::danger::ServerCertVerifier for SkipVerification {
1155 fn verify_server_cert(
1156 &self,
1157 _end_entity: &rustls::pki_types::CertificateDer<'_>,
1158 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
1159 _server_name: &rustls::pki_types::ServerName<'_>,
1160 _ocsp_response: &[u8],
1161 _now: rustls::pki_types::UnixTime,
1162 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
1163 Ok(rustls::client::danger::ServerCertVerified::assertion())
1164 }
1165
1166 fn verify_tls12_signature(
1167 &self,
1168 _message: &[u8],
1169 _cert: &rustls::pki_types::CertificateDer<'_>,
1170 _dcs: &rustls::DigitallySignedStruct,
1171 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1172 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
1173 }
1174
1175 fn verify_tls13_signature(
1176 &self,
1177 _message: &[u8],
1178 _cert: &rustls::pki_types::CertificateDer<'_>,
1179 _dcs: &rustls::DigitallySignedStruct,
1180 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1181 Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
1182 }
1183
1184 /// Every scheme the active provider can verify.
1185 ///
1186 /// This looks like dead weight on a verifier that verifies nothing, and it
1187 /// is not: rustls sends this list as the ClientHello's
1188 /// `signature_algorithms` extension, so it decides which certificates a
1189 /// server is *willing to offer us* — before this verifier is consulted at
1190 /// all.
1191 ///
1192 /// Read off the provider rather than written out by hand, because a
1193 /// hand-written list omits whatever it forgets — `ECDSA_NISTP521_SHA512` is
1194 /// the easy one to miss. A relay with a P-521 leaf would then fail
1195 /// a verification-disabled connection because of what this client offered,
1196 /// not because of anything wrong with the relay — and for the conformance
1197 /// probe that consumes this crate, a failure it manufactured itself is the
1198 /// one result it must never record.
1199 ///
1200 /// The provider's own list is the floor, so the offer widens whenever the
1201 /// provider's does. It is not the ceiling, because it cannot be: `ring`
1202 /// does not implement P-521 at all, so deferring to it alone still leaves
1203 /// that certificate unreachable. That constraint does not apply *here* —
1204 /// this verifier accepts every certificate without looking at it, so a
1205 /// scheme it could not check is one it never needs to. Advertising a
1206 /// superset is exactly right for a verifier that verifies nothing, and
1207 /// would be wrong for any verifier that does.
1208 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1209 let mut schemes = self.provider.signature_verification_algorithms.supported_schemes();
1210
1211 // Schemes a server might legitimately sign with that the provider
1212 // cannot verify. Additive and deduplicated, so a provider that grows
1213 // support for one of these does not end up offering it twice. A named
1214 // list rather than a literal, so adding a scheme is adding a row.
1215 const EXTRA: &[rustls::SignatureScheme] = &[rustls::SignatureScheme::ECDSA_NISTP521_SHA512];
1216 for extra in EXTRA {
1217 if !schemes.contains(extra) {
1218 schemes.push(*extra);
1219 }
1220 }
1221 schemes
1222 }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227 use super::*;
1228 use rustls::client::danger::ServerCertVerifier;
1229
1230 /// The list was six schemes written out by hand, and `ECDSA_NISTP521_SHA512`
1231 /// was not among them. rustls sends it as the ClientHello's
1232 /// `signature_algorithms`, so the omission decided which certificates a
1233 /// server would offer — meaning a P-521 relay failed a
1234 /// verification-disabled dial because of this client rather than because of
1235 /// anything about the relay.
1236 #[test]
1237 fn skipping_verification_still_offers_every_scheme_the_provider_has() {
1238 let schemes = SkipVerification::new().supported_verify_schemes();
1239
1240 assert!(
1241 schemes.contains(&rustls::SignatureScheme::ECDSA_NISTP521_SHA512),
1242 "P-521 missing from the offer: {schemes:?}"
1243 );
1244 // The six that were hardcoded must all survive.
1245 for required in [
1246 rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
1247 rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
1248 rustls::SignatureScheme::ED25519,
1249 rustls::SignatureScheme::RSA_PSS_SHA256,
1250 rustls::SignatureScheme::RSA_PSS_SHA384,
1251 rustls::SignatureScheme::RSA_PSS_SHA512,
1252 ] {
1253 assert!(schemes.contains(&required), "{required:?} missing from {schemes:?}");
1254 }
1255 }
1256
1257 /// A TLS alert is a QUIC code with the top bit set, and both halves matter:
1258 /// the code is what the wire carried, the alert is what it means.
1259 #[test]
1260 fn a_crypto_code_yields_the_alert_it_encodes() {
1261 // 120 `no_application_protocol` — a relay refusing every draft offered.
1262 let refused = handshake_failure(&quinn::ConnectionError::TransportError(
1263 quinn::TransportErrorCode::crypto(120).into(),
1264 ));
1265 assert_eq!(refused.code, Some(0x178));
1266 assert_eq!(refused.tls_alert, Some(120));
1267
1268 // 45 `certificate_expired` — the live case across the seed fleet.
1269 let expired = handshake_failure(&quinn::ConnectionError::TransportError(
1270 quinn::TransportErrorCode::crypto(45).into(),
1271 ));
1272 assert_eq!(expired.code, Some(0x12d));
1273 assert_eq!(expired.tls_alert, Some(45));
1274 }
1275
1276 /// Codes outside `0x0100..=0x01ff` are not alerts and must not be reported
1277 /// as one. `0x178 & 0xff` is a valid alert; `0x10f & 0xff` would be too, and
1278 /// masking without checking the range is how an application close becomes a
1279 /// fictional alert in a conformance report.
1280 #[test]
1281 fn a_non_crypto_code_names_no_alert() {
1282 let closed = handshake_failure(&quinn::ConnectionError::ApplicationClosed(
1283 quinn::ApplicationClose {
1284 error_code: quinn::VarInt::from_u32(271),
1285 reason: (&[][..]).into(),
1286 },
1287 ));
1288 assert_eq!(closed.code, Some(271));
1289 assert_eq!(closed.code_space, Some(super::super::CodeSpace::Application));
1290 assert_eq!(closed.tls_alert, None, "271 is an application code, not alert 15");
1291
1292 assert_eq!(super::super::HandshakeFailure::alert_of(0x00ff), None);
1293 assert_eq!(super::super::HandshakeFailure::alert_of(0x0200), None);
1294 assert_eq!(super::super::HandshakeFailure::alert_of(0x0100), Some(0));
1295 assert_eq!(super::super::HandshakeFailure::alert_of(0x01ff), Some(255));
1296 }
1297
1298 /// A session a peer ended on purpose keeps the code it ended it with.
1299 ///
1300 /// quinn renders `ReadError::ConnectionLost` as the two words "connection
1301 /// lost", so without the mapping under test this reaches a caller as a
1302 /// sentence with no code, no reason phrase and nothing to distinguish a
1303 /// deliberate refusal from a dropped connection. `0x15` is
1304 /// `VERSION_NEGOTIATION_FAILED`.
1305 #[test]
1306 fn a_session_a_peer_closed_keeps_its_code_and_its_reason() {
1307 let read = TransportError::from(quinn::ReadError::ConnectionLost(
1308 quinn::ConnectionError::ApplicationClosed(quinn::ApplicationClose {
1309 error_code: quinn::VarInt::from_u32(0x15),
1310 reason: (&b"unsupported version"[..]).into(),
1311 }),
1312 ));
1313 let TransportError::SessionClosed { code, .. } = &read else {
1314 panic!("expected a session close, got {read}");
1315 };
1316 assert_eq!(*code, 0x15);
1317 // Spelled the way every other code this crate reports is spelled, so
1318 // that a reader parsing the message finds it where it expects to.
1319 let shown = read.to_string();
1320 assert!(shown.contains("(code 21)"), "{shown}");
1321 assert!(shown.contains("unsupported version"), "{shown}");
1322 }
1323
1324 /// A connection lost with nothing behind it invents no code.
1325 ///
1326 /// A timeout is not a refusal, and reporting one as a close with a code is
1327 /// the same class of error as reading a TLS alert out of an application
1328 /// code: a finding that never happened.
1329 #[test]
1330 fn a_timeout_is_not_a_close_and_names_no_code() {
1331 let lost = TransportError::from(quinn::ReadError::ConnectionLost(
1332 quinn::ConnectionError::TimedOut,
1333 ));
1334 assert!(
1335 matches!(lost, TransportError::Connection(_)),
1336 "a timeout carries no application code, got {lost}"
1337 );
1338 }
1339
1340 fn measuring(suites: Vec<u16>) -> QuicDialOptions {
1341 QuicDialOptions::new(vec![b"h3".to_vec()]).insecure(true).offering_cipher_suites(suites)
1342 }
1343
1344 /// The claim `cipher_suites` rests on, and the one that is not obvious.
1345 ///
1346 /// QUIC's Initial packets must use AES-128-GCM, so a config that does not
1347 /// offer it would normally be unbuildable — which would make "does this
1348 /// peer accept *only* ChaCha20" an unaskable question. `with_initial`
1349 /// separates the two, and this asserts it actually works rather than
1350 /// trusting the doc comment.
1351 #[test]
1352 fn a_suite_offer_excluding_aes128_still_builds() {
1353 for suite in [0x1303, 0x1302] {
1354 client_config(&measuring(vec![suite]), vec![b"h3".to_vec()])
1355 .unwrap_or_else(|e| panic!("offering only {suite:#06x} should build: {e}"));
1356 }
1357 }
1358
1359 /// Every suite this crate names must be one the provider actually has,
1360 /// or an enumeration built from the list reports false negatives.
1361 #[test]
1362 fn every_named_suite_is_offerable_on_its_own() {
1363 for suite in TLS13_CIPHER_SUITES {
1364 let name = show_cipher_suite(suite).expect("a named suite has a name");
1365 client_config(&measuring(vec![suite]), vec![b"h3".to_vec()])
1366 .unwrap_or_else(|e| panic!("{name} ({suite:#06x}) is not offerable: {e}"));
1367 }
1368 }
1369
1370 /// A suite the provider lacks must be an error, never a quiet omission:
1371 /// offering fewer suites than asked turns a refusal into a false negative.
1372 #[test]
1373 fn an_unavailable_suite_is_refused_rather_than_dropped() {
1374 // 0x1304 is TLS_AES_128_CCM_SHA256 — real, and not in `ring`.
1375 let err = client_config(&measuring(vec![0x1301, 0x1304]), vec![b"h3".to_vec()])
1376 .expect_err("an unimplemented suite must not be silently dropped");
1377 assert!(format!("{err}").contains("1304"), "the error should name the suite: {err}");
1378
1379 let empty = client_config(&measuring(Vec::new()), vec![b"h3".to_vec()])
1380 .expect_err("an empty offer cannot be answered by anyone");
1381 assert!(format!("{empty}").contains("empty"), "{empty}");
1382 }
1383
1384 /// The default path is unrestricted, and stays the path everything but a
1385 /// measurement takes.
1386 #[test]
1387 fn no_restriction_offers_the_whole_provider() {
1388 let options = QuicDialOptions::new(vec![b"h3".to_vec()]).insecure(true);
1389 assert!(options.cipher_suites.is_none());
1390 let config = rustls_client_config(&options, vec![b"h3".to_vec()]).expect("build");
1391 assert!(
1392 config.crypto_provider().cipher_suites.len() >= TLS13_CIPHER_SUITES.len(),
1393 "the unrestricted offer should carry at least the TLS 1.3 suites"
1394 );
1395 }
1396
1397 /// Every variant names a phase, and only one of them says a peer was there.
1398 ///
1399 /// The table is written out rather than derived so that a variant added to
1400 /// [`DialError`] arrives here as a missing row rather than as a silent
1401 /// answer — `phase` matches exhaustively, so the compiler catches the
1402 /// *variant*, and this catches the *claim* about which side it belongs to.
1403 #[test]
1404 fn every_dial_failure_names_its_phase_and_which_side_it_is() {
1405 let cases = [
1406 (DialError::InvalidAddress("x".into()), DialPhase::Address, true),
1407 (DialError::LocalSocket("x".into()), DialPhase::LocalSocket, true),
1408 (DialError::TlsConfig("x".into()), DialPhase::TlsConfig, true),
1409 (DialError::Transport(TransportError::Connect("x".into())), DialPhase::Connect, true),
1410 (
1411 DialError::Transport(TransportError::Handshake(
1412 super::super::HandshakeFailure::bare("x".into()),
1413 )),
1414 DialPhase::Handshake,
1415 false,
1416 ),
1417 ];
1418 for (err, phase, local) in cases {
1419 assert_eq!(err.phase(), phase, "{err}");
1420 assert_eq!(err.is_local(), local, "{err}");
1421 }
1422 }
1423
1424 /// A socket this machine would not open is not an invalid address.
1425 ///
1426 /// They are separate variants, so a consumer that needs the difference
1427 /// reads it off the type rather than by finding `could not bind` inside the
1428 /// prose. The message still carries those words for a human reader; it is
1429 /// not where the answer lives.
1430 #[test]
1431 fn a_failed_bind_is_this_machines_and_not_the_targets() {
1432 let bind = DialError::LocalSocket("could not bind a local IPv6 socket: oh no".into());
1433 assert_eq!(bind.phase(), DialPhase::LocalSocket);
1434 assert!(bind.is_local());
1435 assert!(bind.to_string().contains("could not bind a local IPv6 socket"), "{bind}");
1436
1437 // And the target being unusable is still its own answer.
1438 let target = DialError::InvalidAddress("relay.example:443 is not host:port".into());
1439 assert_eq!(target.phase(), DialPhase::Address);
1440 }
1441
1442 /// Flattening several addresses into one error must not relabel the phase.
1443 ///
1444 /// `e_at` prefixes the address onto the error it returns, and has to do
1445 /// that without rewriting a `Transport` failure into a `Connect`: the
1446 /// variant decides the phase, so flattening would throw the handshake's
1447 /// codes away and make a relay's refusal answer `is_local() == true`. A
1448 /// failure this machine decided and a failure a peer sent would then be the
1449 /// same value.
1450 #[test]
1451 fn collapsing_several_addresses_keeps_a_peers_refusal_a_peers() {
1452 let addr: SocketAddr = "203.0.113.7:443".parse().unwrap();
1453 let refused = DialError::Transport(TransportError::Handshake(
1454 super::super::HandshakeFailure::transport(0x178, "peer refused the ALPN".into()),
1455 ));
1456
1457 let named = e_at(addr, refused);
1458 assert_eq!(named.phase(), DialPhase::Handshake);
1459 assert!(!named.is_local(), "a peer answered this one");
1460
1461 let DialError::Transport(TransportError::Handshake(failure)) = &named else {
1462 panic!("the typed handshake failure did not survive: {named}");
1463 };
1464 assert_eq!(failure.code, Some(0x178), "the code the refusal carried");
1465 assert_eq!(failure.tls_alert, Some(120), "0x178 is no_application_protocol");
1466 assert!(failure.reason.contains("203.0.113.7:443"), "{}", failure.reason);
1467 assert!(failure.reason.contains("peer refused the ALPN"), "{}", failure.reason);
1468
1469 // The local phases keep their own variants through the same call.
1470 assert_eq!(
1471 e_at(addr, DialError::LocalSocket("no v6".into())).phase(),
1472 DialPhase::LocalSocket
1473 );
1474 assert_eq!(e_at(addr, DialError::TlsConfig("nope".into())).phase(), DialPhase::TlsConfig);
1475 }
1476
1477 /// A timeout is a real outcome with no code in it, and inventing one would
1478 /// be worse than reporting none.
1479 #[test]
1480 fn a_failure_with_no_code_reports_none() {
1481 let timed_out = handshake_failure(&quinn::ConnectionError::TimedOut);
1482 assert_eq!(timed_out.code, None);
1483 assert_eq!(timed_out.tls_alert, None);
1484 assert!(!timed_out.reason.is_empty(), "the prose is all this failure has");
1485 }
1486}