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