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 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}