Skip to main content

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 fourteen `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. A probe recording "handshake"
537/// for every failure of `dial_quic_to` is not being careless; it has nothing
538/// finer to say until this exists. The one thing it does know that this cannot
539/// is which transport it was dialling, which is why
540/// [`Handshake`](Self::Handshake) does not distinguish a QUIC handshake from a
541/// WebTransport CONNECT: both are a peer answering, and only the caller knows
542/// which it asked.
543///
544/// # Exactly one of these means the peer was involved
545///
546/// [`Handshake`](Self::Handshake), and [`DialError::is_local`] is that sentence
547/// as a predicate. The other four are decided on this machine with nothing on
548/// the wire, so a failure carrying one of them can never be evidence about the
549/// peer — which is the direction this distinction is load-bearing in. The
550/// converse is weaker and deliberately not claimed: a handshake that timed out
551/// is not proof of anything the peer did either, only that the machine got as
552/// far as sending.
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
554pub enum DialPhase {
555    /// Turning what the caller named into somewhere to send — parsing a
556    /// `host:port`, resolving it, finding it resolved to nothing.
557    Address,
558    /// Opening a local socket to send from. See [`DialError::LocalSocket`].
559    LocalSocket,
560    /// Building the TLS client configuration: a `ca_certs` entry that is not a
561    /// certificate, a cipher suite the crypto provider does not implement.
562    TlsConfig,
563    /// The stack refusing to start the connection — a server name rustls will
564    /// not accept, a remote address quinn will not dial. Nothing was sent.
565    Connect,
566    /// The handshake, and the only phase in which the peer is involved.
567    Handshake,
568}
569
570impl DialError {
571    /// Which stage of the dial this failure came from.
572    ///
573    /// Total and structural: every variant answers, and no arm reads a message.
574    /// [`DialError::Transport`] splits on whether the transport error is the
575    /// typed [`TransportError::Handshake`] — which by construction exists only
576    /// where a peer answered — so everything else under it is the stack
577    /// declining to start, which is [`DialPhase::Connect`].
578    pub fn phase(&self) -> DialPhase {
579        match self {
580            DialError::InvalidAddress(_) => DialPhase::Address,
581            DialError::LocalSocket(_) => DialPhase::LocalSocket,
582            DialError::TlsConfig(_) => DialPhase::TlsConfig,
583            DialError::Transport(TransportError::Handshake(_)) => DialPhase::Handshake,
584            DialError::Transport(_) => DialPhase::Connect,
585        }
586    }
587
588    /// Whether the dial failed **before a packet left this machine**.
589    ///
590    /// True for everything but [`DialPhase::Handshake`]. The name matches
591    /// [`AnyConnectionError::is_local`](crate::dispatch::AnyConnectionError::is_local),
592    /// which answers the same question for everything after the dial, and it is
593    /// worth being exact about what each half of the answer buys:
594    ///
595    /// - **`true` is a guarantee.** Nothing was sent, so the failure cannot be
596    ///   evidence about the peer, and anything that publishes it as such is
597    ///   publishing a finding that never happened.
598    /// - **`false` is not the opposite guarantee.** It says the machine got as
599    ///   far as sending, and a handshake can still fail for reasons that are
600    ///   nobody's fault in particular — a timeout, a network that dropped the
601    ///   packets. Read [`TransportError::Handshake`]'s codes for what the peer
602    ///   actually said; this only says whether there was a peer to ask.
603    pub fn is_local(&self) -> bool {
604        self.phase() != DialPhase::Handshake
605    }
606}
607
608/// The one place either transport decides what a server certificate is checked
609/// against.
610///
611/// Shared, and that is the whole point of it. Two transports answering this
612/// question apart from each other answer it differently: a `RootCertStore`
613/// seeded from the `webpki-roots` bundle with `ca_certs` added to it on one
614/// side, `wtransport`'s `with_native_certs()` — the *OS* trust store, which
615/// never sees `ca_certs` — on the other. Two consequences, both of which a
616/// conformance run publishes as facts about the relay:
617///
618/// - The bundled Mozilla set and a machine's own set are not the same set. They
619///   diverge on newly-added roots, on roots a distribution has retired early,
620///   and on whatever a corporate MITM appliance installed. A relay would
621///   therefore pass over QUIC and fail over WebTransport with nothing about the
622///   relay differing between the two dials.
623/// - A caller supplying a private CA would have it honoured on one transport
624///   and silently dropped on the other. Not an error, not a warning — a
625///   handshake failure that looks exactly like a relay presenting a bad chain.
626///
627/// Bundled roots for both, rather than native for both, because a published
628/// measurement has to be reproducible: `webpki-roots` is a fixed set compiled
629/// into the binary, so two runs on two machines validate against the same
630/// anchors and any difference in the outcome is a difference in the relay. The
631/// cost is that a peer whose chain the operator trusts only via the OS store is
632/// not trusted here — that operator passes the CA in `ca_certs`, which is what
633/// the field is for and why it reaches both transports.
634///
635/// `alpn` is a parameter and not read off `options` because it is the one part
636/// of the handshake the two transports do not share: a QUIC dial offers the
637/// draft ALPNs it wants the server to choose between, a WebTransport session
638/// offers `h3` and nothing else. Everything a certificate is judged by comes
639/// from `options`.
640///
641/// Returns [`DialError::TlsConfig`] for a `ca_certs` entry that is not a
642/// parseable certificate, and for a crypto provider without the AES-128-GCM
643/// suite QUIC's initial packets are obliged to use.
644///
645/// # The initial suite is separate from the offered suites
646///
647/// QUIC encrypts its Initial packets with AES-128-GCM and has no say in the
648/// matter (RFC 9001 §5.2), which normally makes that suite impossible to leave
649/// out of an offer — and therefore makes "does this peer accept *only*
650/// ChaCha20" unaskable. `QuicClientConfig::with_initial` exists for exactly
651/// this: it takes the initial keys from one suite and lets the TLS config offer
652/// another. So when [`QuicDialOptions::cipher_suites`] excludes AES-128-GCM,
653/// the initial suite is taken from the process default provider and only the
654/// *traffic* suites are restricted.
655pub(crate) fn client_config(
656    options: &QuicDialOptions,
657    alpn: Vec<Vec<u8>>,
658) -> Result<quinn::ClientConfig, DialError> {
659    use std::sync::Arc;
660
661    let tls_config = Arc::new(rustls_client_config(options, alpn)?);
662
663    let has_initial = tls_config
664        .crypto_provider()
665        .cipher_suites
666        .iter()
667        .any(|cs| cs.suite() == rustls::CipherSuite::TLS13_AES_128_GCM_SHA256);
668
669    let quic_config = if has_initial {
670        quinn::crypto::rustls::QuicClientConfig::try_from(tls_config)
671            .map_err(|e| DialError::TlsConfig(format!("{e}")))?
672    } else {
673        quinn::crypto::rustls::QuicClientConfig::with_initial(tls_config, initial_suite()?)
674            .map_err(|e| DialError::TlsConfig(format!("{e}")))?
675    };
676    Ok(quinn::ClientConfig::new(Arc::new(quic_config)))
677}
678
679/// The AES-128-GCM keys QUIC's Initial packets require, from the default
680/// provider.
681///
682/// Read from the process default rather than from the dial's own (possibly
683/// restricted) provider, because the whole point of reaching this function is
684/// that the dial's provider deliberately does not have it.
685fn initial_suite() -> Result<rustls::quic::Suite, DialError> {
686    default_provider()
687        .cipher_suites
688        .iter()
689        .find(|cs| cs.suite() == rustls::CipherSuite::TLS13_AES_128_GCM_SHA256)
690        .and_then(|cs| cs.tls13())
691        .and_then(|cs| cs.quic_suite())
692        .ok_or_else(|| {
693            DialError::TlsConfig(
694                "the crypto provider has no TLS13_AES_128_GCM_SHA256, which QUIC's initial \
695                 packets require"
696                    .to_string(),
697            )
698        })
699}
700
701/// The process-wide crypto provider, or `ring` if none was installed.
702fn default_provider() -> std::sync::Arc<rustls::crypto::CryptoProvider> {
703    rustls::crypto::CryptoProvider::get_default()
704        .cloned()
705        .unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::ring::default_provider()))
706}
707
708/// The rustls half of [`client_config`], before quinn wraps it.
709///
710/// Split out for the WebTransport dial, which cannot use the quinn form: the
711/// only `wtransport` builder state carrying `dns_resolver` — the hook that lets
712/// a caller choose which address a session goes to — is reached through
713/// `with_custom_tls`, which takes a `rustls::ClientConfig`. The state that
714/// accepts a ready-made quinn config has no such hook.
715///
716/// Everything that decides a certificate verdict lives here, so both transports
717/// still get it from one place — including
718/// [`on_peer_certificates`](QuicDialOptions::on_peer_certificates), which is
719/// installed inside the verifier because that is the only place the chain
720/// exists when a handshake is going to fail. Reading it off the finished
721/// connection — what [`super::Transport::peer_certificates`] does — works only
722/// when there *is* a finished connection, and the certificates worth reporting
723/// on are disproportionately the ones that stopped a handshake from finishing.
724pub(crate) fn rustls_client_config(
725    options: &QuicDialOptions,
726    alpn: Vec<Vec<u8>>,
727) -> Result<rustls::ClientConfig, DialError> {
728    use std::sync::Arc;
729
730    // The verifier the caller's options ask for, before any recording.
731    let verifier: Arc<dyn rustls::client::danger::ServerCertVerifier> =
732        if options.skip_cert_verification {
733            Arc::new(SkipVerification::new())
734        } else {
735            let mut roots = rustls::RootCertStore::empty();
736            roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
737            for der in &options.ca_certs {
738                roots
739                    .add(rustls::pki_types::CertificateDer::from(der.clone()))
740                    .map_err(|e| DialError::TlsConfig(format!("bad CA cert: {e}")))?;
741            }
742            rustls::client::WebPkiServerVerifier::builder(Arc::new(roots))
743                .build()
744                .map_err(|e| DialError::TlsConfig(format!("{e}")))?
745        };
746
747    // Wrapping is what keeps observing orthogonal to judging: the decorator
748    // copies the chain and then hands the same arguments to the verifier that
749    // would have run anyway, so an observed dial and an unobserved one reach
750    // identical verdicts. Anything else would make the act of measuring change
751    // the measurement.
752    let verifier = match &options.on_peer_certificates {
753        Some(hook) => Arc::new(ObservingVerifier { inner: verifier, hook: Arc::clone(hook) })
754            as Arc<dyn rustls::client::danger::ServerCertVerifier>,
755        None => verifier,
756    };
757
758    // `builder()` when nothing is restricted, so the default path is byte for
759    // byte what it always was; the provider form only when a caller is
760    // measuring. TLS 1.3 is pinned explicitly there because that builder does
761    // not default to it and QUIC permits nothing else.
762    let builder = match &options.cipher_suites {
763        None => rustls::ClientConfig::builder(),
764        Some(wanted) => {
765            rustls::ClientConfig::builder_with_provider(Arc::new(restricted_provider(wanted)?))
766                .with_protocol_versions(&[&rustls::version::TLS13])
767                .map_err(|e| DialError::TlsConfig(format!("{e}")))?
768        }
769    };
770
771    let mut tls_config =
772        builder.dangerous().with_custom_certificate_verifier(verifier).with_no_client_auth();
773
774    tls_config.alpn_protocols = alpn;
775    Ok(tls_config)
776}
777
778/// The default provider with its cipher suites narrowed to `wanted`.
779///
780/// A requested suite the provider does not implement is an error and not a
781/// silent omission. Offering fewer suites than asked would make a refusal
782/// indistinguishable from a suite that was never on the wire, which turns every
783/// negative result into a possible false one — and this field exists only to
784/// produce negative results that mean something.
785fn restricted_provider(wanted: &[u16]) -> Result<rustls::crypto::CryptoProvider, DialError> {
786    let base = default_provider();
787
788    let mut suites = Vec::with_capacity(wanted.len());
789    for code in wanted {
790        let found = base.cipher_suites.iter().find(|cs| u16::from(cs.suite()) == *code);
791        match found {
792            Some(cs) => suites.push(*cs),
793            None => {
794                let name = show_cipher_suite(*code).unwrap_or("unknown");
795                return Err(DialError::TlsConfig(format!(
796                    "the crypto provider does not implement cipher suite {code:#06x} ({name})"
797                )));
798            }
799        }
800    }
801    if suites.is_empty() {
802        return Err(DialError::TlsConfig(
803            "an empty cipher suite list offers nothing and no peer can answer it".to_string(),
804        ));
805    }
806
807    Ok(rustls::crypto::CryptoProvider { cipher_suites: suites, ..(*base).clone() })
808}
809
810/// Dial one already-resolved address, and report which ALPN the server chose.
811///
812/// This is the whole dial with nothing decided for the caller: one address,
813/// one server name, one ALPN offer, one answer. [`dial_quic`] is the
814/// convenience wrapper that resolves a `host:port` and picks an address; a
815/// caller measuring *which* address or *which* name a relay answers on wants
816/// this one, so that each attempt fails on its own and is recorded on its own.
817///
818/// The second return value is the protocol the server chose from `options.alpn`,
819/// `None` if it selected none.
820/// [`DraftVersion::from_alpn`](moqtap_codec::version::DraftVersion::from_alpn)
821/// names a draft for five of the six; drafts 07-14 share `moq-00` and settle
822/// their version in CLIENT_SETUP.
823///
824/// The endpoint is dropped when the dial returns — quinn keeps the connection's
825/// driver alive independently.
826pub async fn dial_quic_to(
827    target: &QuicTarget,
828    options: &QuicDialOptions,
829) -> Result<(super::Transport, Option<Vec<u8>>), DialError> {
830    let server_addr = target.addr;
831
832    // Built before the socket, so a CA the caller cannot have meant fails
833    // without a packet leaving the machine. Through `client_config` and not
834    // inline, so that this dial and the WebTransport one agree about the
835    // initial cipher suite as well as about certificates.
836    let config = client_config(options, options.alpn.clone())?;
837
838    // Bind in the target's address family. A socket bound to `0.0.0.0` cannot
839    // send to a v6 peer, and quinn reports that as `invalid remote address` —
840    // which reads like the address was malformed when it was only unreachable
841    // from the socket we opened. Measured against a dual-stack relay whose
842    // first resolved address was v6.
843    let bind: SocketAddr = match server_addr {
844        SocketAddr::V4(_) => (Ipv4Addr::UNSPECIFIED, 0).into(),
845        SocketAddr::V6(_) => (Ipv6Addr::UNSPECIFIED, 0).into(),
846    };
847    let mut endpoint = quinn::Endpoint::client(bind).map_err(|e| {
848        // Not `InvalidAddress` about the *target*: the target is fine and this
849        // machine could not open a local socket to reach it. `LocalSocket`
850        // keeps that distinction in the type rather than in the message.
851        DialError::LocalSocket(format!("could not bind a local {} socket: {e}", family(bind)))
852    })?;
853    endpoint.set_default_client_config(config);
854
855    let quic = endpoint
856        .connect(server_addr, &target.server_name)
857        .map_err(TransportError::from)?
858        .await
859        // Not the blanket `From`, which flattens to a message: this is the one
860        // place a peer's refusal is still typed, and a caller measuring *why* a
861        // relay refused needs the code rather than a sentence containing it.
862        .map_err(|e| TransportError::Handshake(handshake_failure(&e)))?;
863
864    let negotiated = negotiated_alpn(&quic);
865    Ok((super::Transport::Quic(QuicTransport::new(quic)), negotiated))
866}
867
868/// Resolve `addr` and dial the first address that answers.
869///
870/// Keeps the signature every draft module's `connect_quic` already calls, so
871/// all fourteen reach this resolution without any of them being touched.
872///
873/// Why it resolves rather than parses: `addr.parse::<SocketAddr>()` accepts
874/// numeric literals and nothing else, so every hostname fails with `invalid
875/// address: invalid socket address syntax` before a packet is sent — a loopback
876/// interop suite passes while every public relay is unreachable. Resolving in
877/// the caller is not an answer either: the resolved IP becomes the SNI and the
878/// certificate check runs against it. `resolve` hands back the name and the
879/// addresses separately, which is what [`QuicTarget`] has two fields for.
880///
881/// One connection is what this returns, so one is what it looks for; a caller
882/// that needs to know an address failed, rather than that some address worked,
883/// wants [`dial_quic_to`] per address instead.
884pub async fn dial_quic(
885    addr: &str,
886    options: &QuicDialOptions,
887) -> Result<(super::Transport, Option<Vec<u8>>), DialError> {
888    let (server_name, addrs) = resolve(addr).await?;
889
890    let mut last: Option<(SocketAddr, DialError)> = None;
891    for candidate in addrs {
892        let target = QuicTarget { addr: candidate, server_name: server_name.clone() };
893        match dial_quic_to(&target, options).await {
894            Ok(connected) => return Ok(connected),
895            Err(e) => last = Some((candidate, e)),
896        }
897    }
898
899    match last {
900        Some((candidate, e)) => Err(e_at(candidate, e)),
901        // `resolve` refuses to return an empty list, so the loop always ran.
902        None => Err(DialError::InvalidAddress(format!("{addr} resolved to no addresses"))),
903    }
904}
905
906/// Name the address a flattened multi-address failure came from.
907///
908/// `dial_quic` collapses several attempts into one error, which is exactly the
909/// lossiness a probe must not have — and is fine here, because a caller of
910/// `dial_quic` asked for one connection and not for a measurement. Saying
911/// which address produced the surviving error is the least it can do.
912///
913/// # It must not relabel the phase while it does that
914///
915/// Every arm returns the variant it was given, and that is a correctness
916/// requirement rather than tidiness: [`DialError::phase`] reads the variant, so
917/// an arm that rewrote a peer's refusal into a `Connect` in order to prefix the
918/// address would make `is_local` answer **true** for a relay that answered a
919/// handshake, and would throw that handshake's codes away with it. Prefixing a
920/// peer's failure means going inside [`HandshakeFailure`] and prefixing the
921/// reason, which keeps its codes as well as its phase.
922///
923/// [`HandshakeFailure`]: super::HandshakeFailure
924fn e_at(addr: SocketAddr, e: DialError) -> DialError {
925    match e {
926        DialError::InvalidAddress(m) => DialError::InvalidAddress(format!("{addr}: {m}")),
927        DialError::LocalSocket(m) => DialError::LocalSocket(format!("{addr}: {m}")),
928        DialError::TlsConfig(m) => DialError::TlsConfig(format!("{addr}: {m}")),
929        DialError::Transport(TransportError::Handshake(failure)) => {
930            DialError::Transport(TransportError::Handshake(super::HandshakeFailure {
931                reason: format!("{addr}: {}", failure.reason),
932                ..failure
933            }))
934        }
935        DialError::Transport(inner) => {
936            DialError::Transport(TransportError::Connect(format!("{addr}: {inner}")))
937        }
938    }
939}
940
941/// Split `host:port` into the name to validate against and every address it
942/// resolves to, v4 first.
943///
944/// v4 first is a preference and not a correctness claim: a v6 attempt on a
945/// host without v6 connectivity burns the caller's entire timeout before v4
946/// is reached, and this path exists to return one working connection quickly.
947/// It is the wrong default for measuring a relay, which is why a probe should
948/// enumerate the list itself and dial each address through [`dial_quic_to`]
949/// rather than inherit this ordering.
950async fn resolve(addr: &str) -> Result<(String, Vec<SocketAddr>), DialError> {
951    // A literal is its own answer and its own server name. Loopback peers are
952    // dialled this way, and one holding an IP-SAN certificate has to keep
953    // validating against the IP — so this cannot be routed through the
954    // hostname path even though `lookup_host` would accept it.
955    if let Ok(sock) = addr.parse::<SocketAddr>() {
956        return Ok((sock.ip().to_string(), vec![sock]));
957    }
958
959    let host = host_of(addr)?;
960    let mut resolved: Vec<SocketAddr> = tokio::net::lookup_host(addr)
961        .await
962        .map_err(|e| DialError::InvalidAddress(format!("could not resolve {addr}: {e}")))?
963        .collect();
964
965    if resolved.is_empty() {
966        return Err(DialError::InvalidAddress(format!("{addr} resolved to no addresses")));
967    }
968    resolved.sort_by_key(|a| a.is_ipv6());
969    Ok((host, resolved))
970}
971
972/// The host part of `host:port`, with the port removed and brackets stripped.
973///
974/// `rsplit_once(':')` alone is wrong for `[::1]:443`: it would cut at the last
975/// colon inside the address and hand back `[::1]` including the bracket, which
976/// is not a name rustls will accept.
977fn host_of(addr: &str) -> Result<String, DialError> {
978    if let Some(rest) = addr.strip_prefix('[') {
979        return rest
980            .split_once(']')
981            .map(|(host, _)| host.to_string())
982            .ok_or_else(|| DialError::InvalidAddress(format!("unclosed '[' in {addr}")));
983    }
984    addr.rsplit_once(':')
985        .map(|(host, _)| host.to_string())
986        .filter(|host| !host.is_empty())
987        .ok_or_else(|| DialError::InvalidAddress(format!("{addr} is not host:port")))
988}
989
990/// `"IPv4"` or `"IPv6"`, for the one error message that needs to say which.
991fn family(addr: SocketAddr) -> &'static str {
992    if addr.is_ipv4() {
993        "IPv4"
994    } else {
995        "IPv6"
996    }
997}
998
999/// The ALPN the server selected, if the handshake recorded one.
1000fn negotiated_alpn(conn: &quinn::Connection) -> Option<Vec<u8>> {
1001    conn.handshake_data()?.downcast::<quinn::crypto::rustls::HandshakeData>().ok()?.protocol
1002}
1003
1004/// Called with a certificate chain observed during a handshake, DER, leaf
1005/// first.
1006///
1007/// A hook and not a return value because a verifier that is about to reject a
1008/// certificate has no return path that carries one — it answers with an error,
1009/// and the bytes it was judging go out of scope. The interesting certificates
1010/// are exactly the rejected ones, so the observation has to happen where the
1011/// judging does.
1012///
1013/// Two things about where it runs. It is called from inside the TLS handshake,
1014/// on quinn's connection driver task rather than on the task that called the
1015/// dial, so a hook that blocks stalls the connection and one that panics
1016/// unwinds into the driver — keep it to moving bytes somewhere. And
1017/// [`dial_quic`] calls it once per resolved address it tries, with nothing in
1018/// the arguments to say which; a caller that needs to attribute a chain to an
1019/// address should dial each one with [`dial_quic_to`].
1020pub type CertificateHook = std::sync::Arc<dyn Fn(&[Vec<u8>]) + Send + Sync>;
1021
1022/// The [`CertificateHook`] most callers want: keep the chain, read it after.
1023///
1024/// Shipped rather than left to each caller because the storage behind this hook
1025/// is the same shared, lock-guarded vector every time, and because it survives
1026/// cancellation — a dial abandoned by `tokio::time::timeout` still leaves
1027/// whatever chain it had already seen in the log, where a chain returned
1028/// alongside the dial's result would be dropped with the future.
1029#[derive(Clone, Default)]
1030pub struct CertificateLog(std::sync::Arc<std::sync::Mutex<Vec<Vec<u8>>>>);
1031
1032impl CertificateLog {
1033    /// An empty log.
1034    pub fn new() -> Self {
1035        Self::default()
1036    }
1037
1038    /// The hook to hand to
1039    /// [`QuicDialOptions::observing`](QuicDialOptions::observing).
1040    pub fn hook(&self) -> CertificateHook {
1041        let slot = std::sync::Arc::clone(&self.0);
1042        std::sync::Arc::new(move |chain: &[Vec<u8>]| {
1043            if let Ok(mut seen) = slot.lock() {
1044                *seen = chain.to_vec();
1045            }
1046        })
1047    }
1048
1049    /// The last chain observed, empty if the handshake never got as far as one.
1050    ///
1051    /// Empty therefore means *no certificate was offered* — a refused ALPN, a
1052    /// dead port, a timeout — and never *the certificate was unreadable*.
1053    pub fn chain(&self) -> Vec<Vec<u8>> {
1054        self.0.lock().map(|seen| seen.clone()).unwrap_or_default()
1055    }
1056}
1057
1058impl std::fmt::Debug for CertificateLog {
1059    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1060        f.debug_tuple("CertificateLog").field(&self.chain().len()).finish()
1061    }
1062}
1063
1064/// A verifier that writes down the chain it was shown, then defers to another.
1065///
1066/// It exists because the interesting certificates are the rejected ones. A
1067/// relay with an expired certificate, a private CA, or a name that does not
1068/// match never completes a handshake, so nothing is left afterwards to read the
1069/// chain off — and "the handshake failed for a certificate reason" without the
1070/// certificate is exactly the report a relay operator cannot act on.
1071///
1072/// It observes unconditionally and judges not at all. The inner verifier's
1073/// verdict is returned untouched, so wrapping cannot turn a rejection into an
1074/// acceptance however the observation goes.
1075struct ObservingVerifier {
1076    inner: std::sync::Arc<dyn rustls::client::danger::ServerCertVerifier>,
1077    hook: CertificateHook,
1078}
1079
1080// Hand-written because `rustls::client::danger::ServerCertVerifier` requires
1081// `Debug` of the verifier, and `#[derive]` would push that requirement onto the
1082// hook — which would make `CertificateHook` a trait with a `Debug` supertrait
1083// rather than a closure, for no benefit to anyone but this line.
1084impl std::fmt::Debug for ObservingVerifier {
1085    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1086        f.debug_struct("ObservingVerifier").field("inner", &self.inner).finish_non_exhaustive()
1087    }
1088}
1089
1090impl rustls::client::danger::ServerCertVerifier for ObservingVerifier {
1091    fn verify_server_cert(
1092        &self,
1093        end_entity: &rustls::pki_types::CertificateDer<'_>,
1094        intermediates: &[rustls::pki_types::CertificateDer<'_>],
1095        server_name: &rustls::pki_types::ServerName<'_>,
1096        ocsp_response: &[u8],
1097        now: rustls::pki_types::UnixTime,
1098    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
1099        // Observe before judging, so a rejection still leaves the evidence
1100        // behind. Leaf first, matching what a finished connection reports.
1101        let mut chain = Vec::with_capacity(1 + intermediates.len());
1102        chain.push(end_entity.as_ref().to_vec());
1103        chain.extend(intermediates.iter().map(|der| der.as_ref().to_vec()));
1104        (self.hook)(&chain);
1105
1106        self.inner.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
1107    }
1108
1109    fn verify_tls12_signature(
1110        &self,
1111        message: &[u8],
1112        cert: &rustls::pki_types::CertificateDer<'_>,
1113        dss: &rustls::DigitallySignedStruct,
1114    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1115        self.inner.verify_tls12_signature(message, cert, dss)
1116    }
1117
1118    fn verify_tls13_signature(
1119        &self,
1120        message: &[u8],
1121        cert: &rustls::pki_types::CertificateDer<'_>,
1122        dss: &rustls::DigitallySignedStruct,
1123    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1124        self.inner.verify_tls13_signature(message, cert, dss)
1125    }
1126
1127    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1128        self.inner.supported_verify_schemes()
1129    }
1130}
1131
1132/// TLS certificate verifier that skips all verification. Testing only.
1133#[derive(Debug)]
1134struct SkipVerification {
1135    /// The provider whose signature schemes this verifier advertises.
1136    ///
1137    /// Held rather than hardcoded because
1138    /// [`supported_verify_schemes`](SkipVerification::supported_verify_schemes)
1139    /// is not a claim about what this verifier checks — it checks nothing — but
1140    /// about what the *ClientHello* offers. See that method for why the
1141    /// distinction has teeth.
1142    provider: std::sync::Arc<rustls::crypto::CryptoProvider>,
1143}
1144
1145impl SkipVerification {
1146    /// Use whichever provider this process installed, falling back to the one
1147    /// this crate compiles with.
1148    ///
1149    /// Taking the installed provider rather than naming `ring` unconditionally
1150    /// keeps the verifier's advertised schemes in step with the schemes the
1151    /// rest of the handshake was actually built from, however the embedding
1152    /// binary configured rustls.
1153    fn new() -> Self {
1154        Self { provider: default_provider() }
1155    }
1156}
1157
1158impl rustls::client::danger::ServerCertVerifier for SkipVerification {
1159    fn verify_server_cert(
1160        &self,
1161        _end_entity: &rustls::pki_types::CertificateDer<'_>,
1162        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
1163        _server_name: &rustls::pki_types::ServerName<'_>,
1164        _ocsp_response: &[u8],
1165        _now: rustls::pki_types::UnixTime,
1166    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
1167        Ok(rustls::client::danger::ServerCertVerified::assertion())
1168    }
1169
1170    fn verify_tls12_signature(
1171        &self,
1172        _message: &[u8],
1173        _cert: &rustls::pki_types::CertificateDer<'_>,
1174        _dcs: &rustls::DigitallySignedStruct,
1175    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1176        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
1177    }
1178
1179    fn verify_tls13_signature(
1180        &self,
1181        _message: &[u8],
1182        _cert: &rustls::pki_types::CertificateDer<'_>,
1183        _dcs: &rustls::DigitallySignedStruct,
1184    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1185        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
1186    }
1187
1188    /// Every scheme the active provider can verify.
1189    ///
1190    /// This looks like dead weight on a verifier that verifies nothing, and it
1191    /// is not: rustls sends this list as the ClientHello's
1192    /// `signature_algorithms` extension, so it decides which certificates a
1193    /// server is *willing to offer us* — before this verifier is consulted at
1194    /// all.
1195    ///
1196    /// Read off the provider rather than written out by hand, because a
1197    /// hand-written list omits whatever it forgets — `ECDSA_NISTP521_SHA512` is
1198    /// the easy one to miss. A relay with a P-521 leaf would then fail
1199    /// a verification-disabled connection because of what this client offered,
1200    /// not because of anything wrong with the relay — and for the conformance
1201    /// probe that consumes this crate, a failure it manufactured itself is the
1202    /// one result it must never record.
1203    ///
1204    /// The provider's own list is the floor, so the offer widens whenever the
1205    /// provider's does. It is not the ceiling, because it cannot be: `ring`
1206    /// does not implement P-521 at all, so deferring to it alone still leaves
1207    /// that certificate unreachable. That constraint does not apply *here* —
1208    /// this verifier accepts every certificate without looking at it, so a
1209    /// scheme it could not check is one it never needs to. Advertising a
1210    /// superset is exactly right for a verifier that verifies nothing, and
1211    /// would be wrong for any verifier that does.
1212    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1213        let mut schemes = self.provider.signature_verification_algorithms.supported_schemes();
1214
1215        // Schemes a server might legitimately sign with that the provider
1216        // cannot verify. Additive and deduplicated, so a provider that grows
1217        // support for one of these does not end up offering it twice.
1218        for extra in [rustls::SignatureScheme::ECDSA_NISTP521_SHA512] {
1219            if !schemes.contains(&extra) {
1220                schemes.push(extra);
1221            }
1222        }
1223        schemes
1224    }
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229    use super::*;
1230    use rustls::client::danger::ServerCertVerifier;
1231
1232    /// The list was six schemes written out by hand, and `ECDSA_NISTP521_SHA512`
1233    /// was not among them. rustls sends it as the ClientHello's
1234    /// `signature_algorithms`, so the omission decided which certificates a
1235    /// server would offer — meaning a P-521 relay failed a
1236    /// verification-disabled dial because of this client rather than because of
1237    /// anything about the relay.
1238    #[test]
1239    fn skipping_verification_still_offers_every_scheme_the_provider_has() {
1240        let schemes = SkipVerification::new().supported_verify_schemes();
1241
1242        assert!(
1243            schemes.contains(&rustls::SignatureScheme::ECDSA_NISTP521_SHA512),
1244            "P-521 missing from the offer: {schemes:?}"
1245        );
1246        // The six that were hardcoded must all survive.
1247        for required in [
1248            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
1249            rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
1250            rustls::SignatureScheme::ED25519,
1251            rustls::SignatureScheme::RSA_PSS_SHA256,
1252            rustls::SignatureScheme::RSA_PSS_SHA384,
1253            rustls::SignatureScheme::RSA_PSS_SHA512,
1254        ] {
1255            assert!(schemes.contains(&required), "{required:?} missing from {schemes:?}");
1256        }
1257    }
1258
1259    /// A TLS alert is a QUIC code with the top bit set, and both halves matter:
1260    /// the code is what the wire carried, the alert is what it means.
1261    #[test]
1262    fn a_crypto_code_yields_the_alert_it_encodes() {
1263        // 120 `no_application_protocol` — a relay refusing every draft offered.
1264        let refused = handshake_failure(&quinn::ConnectionError::TransportError(
1265            quinn::TransportErrorCode::crypto(120).into(),
1266        ));
1267        assert_eq!(refused.code, Some(0x178));
1268        assert_eq!(refused.tls_alert, Some(120));
1269
1270        // 45 `certificate_expired` — the live case across the seed fleet.
1271        let expired = handshake_failure(&quinn::ConnectionError::TransportError(
1272            quinn::TransportErrorCode::crypto(45).into(),
1273        ));
1274        assert_eq!(expired.code, Some(0x12d));
1275        assert_eq!(expired.tls_alert, Some(45));
1276    }
1277
1278    /// Codes outside `0x0100..=0x01ff` are not alerts and must not be reported
1279    /// as one. `0x178 & 0xff` is a valid alert; `0x10f & 0xff` would be too, and
1280    /// masking without checking the range is how an application close becomes a
1281    /// fictional alert in a conformance report.
1282    #[test]
1283    fn a_non_crypto_code_names_no_alert() {
1284        let closed = handshake_failure(&quinn::ConnectionError::ApplicationClosed(
1285            quinn::ApplicationClose {
1286                error_code: quinn::VarInt::from_u32(271),
1287                reason: (&[][..]).into(),
1288            },
1289        ));
1290        assert_eq!(closed.code, Some(271));
1291        assert_eq!(closed.code_space, Some(super::super::CodeSpace::Application));
1292        assert_eq!(closed.tls_alert, None, "271 is an application code, not alert 15");
1293
1294        assert_eq!(super::super::HandshakeFailure::alert_of(0x00ff), None);
1295        assert_eq!(super::super::HandshakeFailure::alert_of(0x0200), None);
1296        assert_eq!(super::super::HandshakeFailure::alert_of(0x0100), Some(0));
1297        assert_eq!(super::super::HandshakeFailure::alert_of(0x01ff), Some(255));
1298    }
1299
1300    /// A session a peer ended on purpose keeps the code it ended it with.
1301    ///
1302    /// quinn renders `ReadError::ConnectionLost` as the two words "connection
1303    /// lost", so without the mapping under test this reaches a caller as a
1304    /// sentence with no code, no reason phrase and nothing to distinguish a
1305    /// deliberate refusal from a dropped connection. `0x15` is
1306    /// `VERSION_NEGOTIATION_FAILED`.
1307    #[test]
1308    fn a_session_a_peer_closed_keeps_its_code_and_its_reason() {
1309        let read = TransportError::from(quinn::ReadError::ConnectionLost(
1310            quinn::ConnectionError::ApplicationClosed(quinn::ApplicationClose {
1311                error_code: quinn::VarInt::from_u32(0x15),
1312                reason: (&b"unsupported version"[..]).into(),
1313            }),
1314        ));
1315        let TransportError::SessionClosed { code, .. } = &read else {
1316            panic!("expected a session close, got {read}");
1317        };
1318        assert_eq!(*code, 0x15);
1319        // Spelled the way every other code this crate reports is spelled, so
1320        // that a reader parsing the message finds it where it expects to.
1321        let shown = read.to_string();
1322        assert!(shown.contains("(code 21)"), "{shown}");
1323        assert!(shown.contains("unsupported version"), "{shown}");
1324    }
1325
1326    /// A connection lost with nothing behind it invents no code.
1327    ///
1328    /// A timeout is not a refusal, and reporting one as a close with a code is
1329    /// the same class of error as reading a TLS alert out of an application
1330    /// code: a finding that never happened.
1331    #[test]
1332    fn a_timeout_is_not_a_close_and_names_no_code() {
1333        let lost = TransportError::from(quinn::ReadError::ConnectionLost(
1334            quinn::ConnectionError::TimedOut,
1335        ));
1336        assert!(
1337            matches!(lost, TransportError::Connection(_)),
1338            "a timeout carries no application code, got {lost}"
1339        );
1340    }
1341
1342    fn measuring(suites: Vec<u16>) -> QuicDialOptions {
1343        QuicDialOptions::new(vec![b"h3".to_vec()]).insecure(true).offering_cipher_suites(suites)
1344    }
1345
1346    /// The claim `cipher_suites` rests on, and the one that is not obvious.
1347    ///
1348    /// QUIC's Initial packets must use AES-128-GCM, so a config that does not
1349    /// offer it would normally be unbuildable — which would make "does this
1350    /// peer accept *only* ChaCha20" an unaskable question. `with_initial`
1351    /// separates the two, and this asserts it actually works rather than
1352    /// trusting the doc comment.
1353    #[test]
1354    fn a_suite_offer_excluding_aes128_still_builds() {
1355        for suite in [0x1303, 0x1302] {
1356            client_config(&measuring(vec![suite]), vec![b"h3".to_vec()])
1357                .unwrap_or_else(|e| panic!("offering only {suite:#06x} should build: {e}"));
1358        }
1359    }
1360
1361    /// Every suite this crate names must be one the provider actually has,
1362    /// or an enumeration built from the list reports false negatives.
1363    #[test]
1364    fn every_named_suite_is_offerable_on_its_own() {
1365        for suite in TLS13_CIPHER_SUITES {
1366            let name = show_cipher_suite(suite).expect("a named suite has a name");
1367            client_config(&measuring(vec![suite]), vec![b"h3".to_vec()])
1368                .unwrap_or_else(|e| panic!("{name} ({suite:#06x}) is not offerable: {e}"));
1369        }
1370    }
1371
1372    /// A suite the provider lacks must be an error, never a quiet omission:
1373    /// offering fewer suites than asked turns a refusal into a false negative.
1374    #[test]
1375    fn an_unavailable_suite_is_refused_rather_than_dropped() {
1376        // 0x1304 is TLS_AES_128_CCM_SHA256 — real, and not in `ring`.
1377        let err = client_config(&measuring(vec![0x1301, 0x1304]), vec![b"h3".to_vec()])
1378            .expect_err("an unimplemented suite must not be silently dropped");
1379        assert!(format!("{err}").contains("1304"), "the error should name the suite: {err}");
1380
1381        let empty = client_config(&measuring(Vec::new()), vec![b"h3".to_vec()])
1382            .expect_err("an empty offer cannot be answered by anyone");
1383        assert!(format!("{empty}").contains("empty"), "{empty}");
1384    }
1385
1386    /// The default path is unrestricted, and stays the path everything but a
1387    /// measurement takes.
1388    #[test]
1389    fn no_restriction_offers_the_whole_provider() {
1390        let options = QuicDialOptions::new(vec![b"h3".to_vec()]).insecure(true);
1391        assert!(options.cipher_suites.is_none());
1392        let config = rustls_client_config(&options, vec![b"h3".to_vec()]).expect("build");
1393        assert!(
1394            config.crypto_provider().cipher_suites.len() >= TLS13_CIPHER_SUITES.len(),
1395            "the unrestricted offer should carry at least the TLS 1.3 suites"
1396        );
1397    }
1398
1399    /// Every variant names a phase, and only one of them says a peer was there.
1400    ///
1401    /// The table is written out rather than derived so that a variant added to
1402    /// [`DialError`] arrives here as a missing row rather than as a silent
1403    /// answer — `phase` matches exhaustively, so the compiler catches the
1404    /// *variant*, and this catches the *claim* about which side it belongs to.
1405    #[test]
1406    fn every_dial_failure_names_its_phase_and_which_side_it_is() {
1407        let cases = [
1408            (DialError::InvalidAddress("x".into()), DialPhase::Address, true),
1409            (DialError::LocalSocket("x".into()), DialPhase::LocalSocket, true),
1410            (DialError::TlsConfig("x".into()), DialPhase::TlsConfig, true),
1411            (DialError::Transport(TransportError::Connect("x".into())), DialPhase::Connect, true),
1412            (
1413                DialError::Transport(TransportError::Handshake(
1414                    super::super::HandshakeFailure::bare("x".into()),
1415                )),
1416                DialPhase::Handshake,
1417                false,
1418            ),
1419        ];
1420        for (err, phase, local) in cases {
1421            assert_eq!(err.phase(), phase, "{err}");
1422            assert_eq!(err.is_local(), local, "{err}");
1423        }
1424    }
1425
1426    /// A socket this machine would not open is not an invalid address.
1427    ///
1428    /// They are separate variants, so a consumer that needs the difference
1429    /// reads it off the type rather than by finding `could not bind` inside the
1430    /// prose. The message still carries those words for a human reader; it is
1431    /// not where the answer lives.
1432    #[test]
1433    fn a_failed_bind_is_this_machines_and_not_the_targets() {
1434        let bind = DialError::LocalSocket("could not bind a local IPv6 socket: oh no".into());
1435        assert_eq!(bind.phase(), DialPhase::LocalSocket);
1436        assert!(bind.is_local());
1437        assert!(bind.to_string().contains("could not bind a local IPv6 socket"), "{bind}");
1438
1439        // And the target being unusable is still its own answer.
1440        let target = DialError::InvalidAddress("relay.example:443 is not host:port".into());
1441        assert_eq!(target.phase(), DialPhase::Address);
1442    }
1443
1444    /// Flattening several addresses into one error must not relabel the phase.
1445    ///
1446    /// `e_at` prefixes the address onto the error it returns, and has to do
1447    /// that without rewriting a `Transport` failure into a `Connect`: the
1448    /// variant decides the phase, so flattening would throw the handshake's
1449    /// codes away and make a relay's refusal answer `is_local() == true`. A
1450    /// failure this machine decided and a failure a peer sent would then be the
1451    /// same value.
1452    #[test]
1453    fn collapsing_several_addresses_keeps_a_peers_refusal_a_peers() {
1454        let addr: SocketAddr = "203.0.113.7:443".parse().unwrap();
1455        let refused = DialError::Transport(TransportError::Handshake(
1456            super::super::HandshakeFailure::transport(0x178, "peer refused the ALPN".into()),
1457        ));
1458
1459        let named = e_at(addr, refused);
1460        assert_eq!(named.phase(), DialPhase::Handshake);
1461        assert!(!named.is_local(), "a peer answered this one");
1462
1463        let DialError::Transport(TransportError::Handshake(failure)) = &named else {
1464            panic!("the typed handshake failure did not survive: {named}");
1465        };
1466        assert_eq!(failure.code, Some(0x178), "the code the refusal carried");
1467        assert_eq!(failure.tls_alert, Some(120), "0x178 is no_application_protocol");
1468        assert!(failure.reason.contains("203.0.113.7:443"), "{}", failure.reason);
1469        assert!(failure.reason.contains("peer refused the ALPN"), "{}", failure.reason);
1470
1471        // The local phases keep their own variants through the same call.
1472        assert_eq!(
1473            e_at(addr, DialError::LocalSocket("no v6".into())).phase(),
1474            DialPhase::LocalSocket
1475        );
1476        assert_eq!(e_at(addr, DialError::TlsConfig("nope".into())).phase(), DialPhase::TlsConfig);
1477    }
1478
1479    /// A timeout is a real outcome with no code in it, and inventing one would
1480    /// be worse than reporting none.
1481    #[test]
1482    fn a_failure_with_no_code_reports_none() {
1483        let timed_out = handshake_failure(&quinn::ConnectionError::TimedOut);
1484        assert_eq!(timed_out.code, None);
1485        assert_eq!(timed_out.tls_alert, None);
1486        assert!(!timed_out.reason.is_empty(), "the prose is all this failure has");
1487    }
1488}