Repository: quinn-rs/quinn (rust)

Issue:
quinn-proto: `Chunks` panic-on-drop behavior is a footgun and makes control flow annoying
I was just bitten by this assert while trying to debug a silent failure of my application during testing: https://github.com/quinn-rs/quinn/blob/main/quinn-proto/src/connection/streams/recv.rs#L289-L292

This was in a spawned Tokio task ~which apparently suppresses the normal printing of panic backtraces to stderr~ and I was running this as a test which buffered the panic backtrace but not letting it complete. And I hadn't yet wired it up to check the task's exit status as it's supposed to be long-running anyway and I'm still working on the principal implementation here.

On `StreamEvent::Opened` or `StreamEvent::Readable` I essentially call into code that looks like this: 

```rust
impl<'a> StreamReadContext<'a> {
    fn read(mut self) -> anyhow::Result<()> {
        // `self.stream: quinn_proto::RecvStream`
        let mut chunks = self.stream.read(true)?;

        loop {
            match self.activity {
                StreamActivity::PendingHeader => {
                    if let Some(header) = self.reader.try_peek_header().context("invalid header")? {
                        Notification::validate_header(header).context("invalid header")?;
                        *self.activity = StreamActivity::ReadingNotification { header };
                    };
                }
                StreamActivity::ReadingNotification { header } => {
                    if self.reader.input_len() >= header.length {
                        let notification = Notification::decode(&mut *self.reader)
                            .context("invalid notification")?;

                        // Handle `Notification`
                    }
                }
            }

            // Lazily read chunks so we don't buffer a ton of data up-front.
            if let Some(chunk) = chunks.next(MAX_CHUNK_SIZE)? { // 8 KiB
                tracing::trace!("read {} bytes", chunk.bytes.len());
                self.reader.push(chunk.bytes)?;
            } else {
                break;
            }
        }

        // If we need to transmit it'll be checked when we return to the event loop.
        let _ = chunks.finalize();

        Ok(())
    }
}
```

As implied by the code here, `Notification` messages have a header that gives their length so we know how much data we need to buffer to fully decode them. `reader` is the type that buffers chunks and implements the right traits for `Notification::decode()`.

Due to some bug in my code, `.try_peek_header()` is returning an error which normally _would_ be logged when returned from this method. However, because the `impl Drop for Chunks` executes on the return, and because I hadn't called `.finalize()` yet, this is turned into a `panic!()`, which in the current state of my code just causes the task to silently exit.

The thing is, I don't _need_ the `ShouldTransmit` flag because this immediately returns to an event loop where I call all the appropriate `.handle*()` and `.poll*()` methods on the `quinn_proto::Connection`. So the implicit finalize triggered by the `Drop` impl would otherwise be sufficient for me.

Because the panic message was being suppressed, it took me an annoyingly long time to figure this out. It wasn't until I ran the code in a debugger (which helpfully automatically breaks on panics) to figure this out. While I understand the point of the `debug_assert!()` is to teach the user how to use this API correctly, in my case it was much more of a hindrance than anything.

(~Tokio's partly to blame here... why suppress backtraces in debug mode? That doesn't make any sense.~ However, the panic doesn't strictly _need_ to happen either. `quinn-proto` is by design a low-level API, so why is it assuming the user might not know what they're doing here?)

Pull request description:
remove panic-on-drop in `quinn_proto::Chunks`
closes #1703
closes #1531 

cc @djc 

Patch:
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 00000000..aa949327
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,7 @@
+# Do not notify until at least three results have been uploaded from the CI pipeline.
+# (Corresponds to the three platforms we check coverage on: Linux, macOS, and Windows.)
+codecov:
+    notify:
+        after_n_builds: 3
+comment:
+    after_n_builds: 3
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index c1884875..7fa7bcfd 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -6,7 +6,10 @@ on:
 
 jobs:
   coverage:
-    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+    runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v4
       - uses: dtolnay/rust-toolchain@stable
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 4b4e4b3d..8457d1ee 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -55,6 +55,16 @@ jobs:
       - run: cargo test --manifest-path fuzz/Cargo.toml
         if: ${{ matrix.rust }} == "stable"
 
+  test-aws-lc-rs:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@stable
+      - uses: Swatinem/rust-cache@v2
+      # Prevent feature unification from selecting *ring* as the crypto provider
+      - run: RUST_BACKTRACE=1 cargo test --manifest-path quinn-proto/Cargo.toml --no-default-features --features rustls-aws-lc-rs
+      - run: RUST_BACKTRACE=1 cargo test --manifest-path quinn/Cargo.toml --no-default-features --features rustls-aws-lc-rs,runtime-tokio
+
   msrv:
     runs-on: ubuntu-latest
     steps:
diff --git a/Cargo.toml b/Cargo.toml
index e76ebcc5..d9dc1cde 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ arbitrary = { version = "1.0.1", features = ["derive"] }
 async-io = "2"
 async-std = "1.11"
 assert_matches = "1.1"
+aws-lc-rs = { version = "1.9", default-features = false }
 bencher = "0.1.5"
 bytes = "1"
 clap = { version = "4", features = ["derive"] }
@@ -33,7 +34,7 @@ rand = "0.8"
 rcgen = "0.13"
 ring = "0.17"
 rustc-hash = "2"
-rustls = { version = "0.23.5", default-features = false, features = ["ring", "std"] }
+rustls = { version = "0.23.5", default-features = false, features = ["std"] }
 rustls-pemfile = "2"
 rustls-platform-verifier = "0.3"
 serde = { version = "1.0", features = ["derive"] }
diff --git a/deny.toml b/deny.toml
index 43038b9a..8998d624 100644
--- a/deny.toml
+++ b/deny.toml
@@ -6,9 +6,9 @@ allow = [
     "ISC",
     "MIT",
     "MPL-2.0",
+    "OpenSSL",
     "Unicode-DFS-2016",
 ]
-exceptions = [{ allow = ["ISC", "MIT", "OpenSSL"], name = "ring" }]
 private = { ignore = true }
 
 [[licenses.clarify]]
diff --git a/quinn-proto/Cargo.toml b/quinn-proto/Cargo.toml
index 1b757fa7..9a67f02c 100644
--- a/quinn-proto/Cargo.toml
+++ b/quinn-proto/Cargo.toml
@@ -14,8 +14,14 @@ workspace = ".."
 all-features = true
 
 [features]
-default = ["rustls", "log"]
-rustls = ["dep:rustls", "ring"]
+default = ["rustls-ring", "log"]
+aws-lc-rs = ["dep:aws-lc-rs", "aws-lc-rs/aws-lc-sys", "aws-lc-rs/prebuilt-nasm"]
+# For backwards compatibility, `rustls` forwards to `rustls-ring`
+rustls = ["rustls-ring"]
+# Enable rustls with the `aws-lc-rs` crypto provider
+rustls-aws-lc-rs = ["dep:rustls", "rustls/aws-lc-rs", "aws-lc-rs"]
+# Enable rustls with the `ring` crypto provider
+rustls-ring = ["dep:rustls", "rustls/ring", "ring"]
 ring = ["dep:ring"]
 # Enable rustls ring provider and direct ring usage
 # Provides `ClientConfig::with_platform_verifier()` convenience method
@@ -25,6 +31,7 @@ log = ["tracing/log"]
 
 [dependencies]
 arbitrary = { workspace = true, optional = true }
+aws-lc-rs = { workspace = true, optional = true }
 bytes = { workspace = true }
 rustc-hash = { workspace = true }
 rand = { workspace = true }
@@ -32,7 +39,7 @@ ring = { workspace = true, optional = true }
 rustls = { workspace = true, optional = true }
 rustls-platform-verifier = { workspace = true, optional = true }
 slab = { workspace = true }
-thiserror = { workspace= true }
+thiserror = { workspace = true }
 tinyvec = { workspace = true, features = ["alloc"] }
 tracing = { workspace = true }
 
diff --git a/quinn-proto/src/cid_generator.rs b/quinn-proto/src/cid_generator.rs
index 6173e96f..c59fb410 100644
--- a/quinn-proto/src/cid_generator.rs
+++ b/quinn-proto/src/cid_generator.rs
@@ -171,7 +171,6 @@ mod tests {
     use super::*;
 
     #[test]
-    #[cfg(feature = "ring")]
     fn validate_keyed_cid() {
         let mut generator = HashedConnectionIdGenerator::new();
         let cid = generator.generate_cid();
diff --git a/quinn-proto/src/config.rs b/quinn-proto/src/config.rs
index 557ae2cf..57f38e16 100644
--- a/quinn-proto/src/config.rs
+++ b/quinn-proto/src/config.rs
@@ -6,16 +6,14 @@ use std::{
     time::Duration,
 };
 
-#[cfg(feature = "ring")]
-use rand::RngCore;
-#[cfg(feature = "rustls")]
+#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 use rustls::client::WebPkiServerVerifier;
-#[cfg(feature = "rustls")]
+#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 use rustls::pki_types::{CertificateDer, PrivateKeyDer};
 use thiserror::Error;
 
-#[cfg(feature = "rustls")]
-use crate::crypto::rustls::QuicServerConfig;
+#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
+use crate::crypto::rustls::{configured_provider, QuicServerConfig};
 use crate::{
     cid_generator::{ConnectionIdGenerator, HashedConnectionIdGenerator},
     congestion,
@@ -759,16 +757,19 @@ impl fmt::Debug for EndpointConfig {
     }
 }
 
-#[cfg(feature = "ring")]
+#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
 impl Default for EndpointConfig {
     fn default() -> Self {
+        #[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
+        use aws_lc_rs::hmac;
+        use rand::RngCore;
+        #[cfg(feature = "ring")]
+        use ring::hmac;
+
         let mut reset_key = [0; 64];
         rand::thread_rng().fill_bytes(&mut reset_key);
 
-        Self::new(Arc::new(ring::hmac::Key::new(
-            ring::hmac::HMAC_SHA256,
-            &reset_key,
-        )))
+        Self::new(Arc::new(hmac::Key::new(hmac::HMAC_SHA256, &reset_key)))
     }
 }
 
@@ -919,7 +920,7 @@ impl ServerConfig {
     }
 }
 
-#[cfg(feature = "rustls")]
+#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 impl ServerConfig {
     /// Create a server config with the given certificate chain to be presented to clients
     ///
@@ -934,16 +935,22 @@ impl ServerConfig {
     }
 }
 
-#[cfg(feature = "ring")]
+#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
 impl ServerConfig {
     /// Create a server config with the given [`crypto::ServerConfig`]
     ///
     /// Uses a randomized handshake token key.
     pub fn with_crypto(crypto: Arc<dyn crypto::ServerConfig>) -> Self {
+        #[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
+        use aws_lc_rs::hkdf;
+        use rand::RngCore;
+        #[cfg(feature = "ring")]
+        use ring::hkdf;
+
         let rng = &mut rand::thread_rng();
         let mut master_key = [0u8; 64];
         rng.fill_bytes(&mut master_key);
-        let master_key = ring::hkdf::Salt::new(ring::hkdf::HKDF_SHA256, &[]).extract(&master_key);
+        let master_key = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key);
 
         Self::new(crypto, Arc::new(master_key))
     }
@@ -1030,7 +1037,7 @@ impl ClientConfig {
     }
 }
 
-#[cfg(feature = "rustls")]
+#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 impl ClientConfig {
     /// Create a client configuration that trusts the platform's native roots
     #[cfg(feature = "platform-verifier")]
@@ -1045,7 +1052,7 @@ impl ClientConfig {
         roots: Arc<rustls::RootCertStore>,
     ) -> Result<Self, rustls::client::VerifierBuilderError> {
         Ok(Self::new(Arc::new(crypto::rustls::QuicClientConfig::new(
-            WebPkiServerVerifier::builder(roots).build()?,
+            WebPkiServerVerifier::builder_with_provider(roots, configured_provider()).build()?,
         ))))
     }
 }
diff --git a/quinn-proto/src/connection/mod.rs b/quinn-proto/src/connection/mod.rs
index 2c0f92b5..65465fa1 100644
--- a/quinn-proto/src/connection/mod.rs
+++ b/quinn-proto/src/connection/mod.rs
@@ -82,7 +82,7 @@ pub use streams::StreamsState;
 use streams::StreamsState;
 pub use streams::{
     BytesSource, Chunks, ClosedStream, FinishError, ReadError, ReadableError, RecvStream,
-    SendStream, StreamEvent, Streams, WriteError, Written,
+    SendStream, ShouldTransmit, StreamEvent, Streams, WriteError, Written,
 };
 
 mod timer;
diff --git a/quinn-proto/src/connection/streams/recv.rs b/quinn-proto/src/connection/streams/recv.rs
index 0182bb43..93c7fedc 100644
--- a/quinn-proto/src/connection/streams/recv.rs
+++ b/quinn-proto/src/connection/streams/recv.rs
@@ -220,7 +220,17 @@ impl Recv {
     }
 }
 
-/// Chunks
+/// Chunks returned from [`RecvStream::read()`][crate::RecvStream::read].
+///
+/// ### Note: Finalization Needed
+/// Bytes read from the stream are not released from the congestion window until
+/// either [`Self::finalize()`] is called, or this type is dropped.
+///
+/// It is recommended that you call [`Self::finalize()`] because it returns a flag
+/// telling you whether reading from the stream has resulted in the need to transmit a packet.
+///
+/// If this type is leaked, the stream will remain blocked on the remote peer until
+/// another read from the stream is done.
 pub struct Chunks<'a> {
     id: StreamId,
     ordered: bool,
@@ -302,17 +312,21 @@ impl<'a> Chunks<'a> {
         }
     }
 
-    /// Finalize
+    /// Mark the read data as consumed from the stream.
+    ///
+    /// The number of read bytes will be released from the congestion window,
+    /// allowing the remote peer to send more data if it was previously blocked.
+    ///
+    /// If [`ShouldTransmit::should_transmit()`] returns `true`,
+    /// a packet needs to be sent to the peer informing them that the stream is unblocked.
+    /// This means that you should call [`Connection::poll_transmit()`][crate::Connection::poll_transmit]
+    /// and send the returned packet as soon as is reasonable, to unblock the remote peer.
     pub fn finalize(mut self) -> ShouldTransmit {
-        self.finalize_inner(false)
+        self.finalize_inner()
     }
 
-    fn finalize_inner(&mut self, drop: bool) -> ShouldTransmit {
+    fn finalize_inner(&mut self) -> ShouldTransmit {
         let state = mem::replace(&mut self.state, ChunksState::Finalized);
-        debug_assert!(
-            !drop || matches!(state, ChunksState::Finalized),
-            "finalize must be called before drop"
-        );
         if let ChunksState::Finalized = state {
             // Noop on repeated calls
             return ShouldTransmit(false);
@@ -344,7 +358,7 @@ impl<'a> Chunks<'a> {
 
 impl<'a> Drop for Chunks<'a> {
     fn drop(&mut self) {
-        let _ = self.finalize_inner(true);
+        let _ = self.finalize_inner();
     }
 }
 
diff --git a/quinn-proto/src/crypto.rs b/quinn-proto/src/crypto.rs
index 17229708..f15d740c 100644
--- a/quinn-proto/src/crypto.rs
+++ b/quinn-proto/src/crypto.rs
@@ -18,10 +18,10 @@ use crate::{
 };
 
 /// Cryptography interface based on *ring*
-#[cfg(feature = "ring")]
-pub(crate) mod ring;
+#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
+pub(crate) mod ring_like;
 /// TLS interface based on rustls
-#[cfg(feature = "rustls")]
+#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 pub mod rustls;
 
 /// A cryptographic session (commonly TLS)
diff --git a/quinn-proto/src/crypto/ring.rs b/quinn-proto/src/crypto/ring_like.rs
similarity index 71%
rename from quinn-proto/src/crypto/ring.rs
rename to quinn-proto/src/crypto/ring_like.rs
index 48561f45..1b5f301b 100644
--- a/quinn-proto/src/crypto/ring.rs
+++ b/quinn-proto/src/crypto/ring_like.rs
@@ -1,4 +1,7 @@
-use ring::{aead, hkdf, hmac};
+#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
+use aws_lc_rs::{aead, error, hkdf, hmac};
+#[cfg(feature = "ring")]
+use ring::{aead, error, hkdf, hmac};
 
 use crate::crypto::{self, CryptoError};
 
@@ -31,8 +34,8 @@ impl crypto::HandshakeTokenKey for hkdf::Prk {
 
 impl crypto::AeadKey for aead::LessSafeKey {
     fn seal(&self, data: &mut Vec<u8>, additional_data: &[u8]) -> Result<(), CryptoError> {
-        let aad = ring::aead::Aad::from(additional_data);
-        let zero_nonce = ring::aead::Nonce::assume_unique_for_key([0u8; 12]);
+        let aad = aead::Aad::from(additional_data);
+        let zero_nonce = aead::Nonce::assume_unique_for_key([0u8; 12]);
         Ok(self.seal_in_place_append_tag(zero_nonce, aad, data)?)
     }
 
@@ -41,14 +44,14 @@ impl crypto::AeadKey for aead::LessSafeKey {
         data: &'a mut [u8],
         additional_data: &[u8],
     ) -> Result<&'a mut [u8], CryptoError> {
-        let aad = ring::aead::Aad::from(additional_data);
-        let zero_nonce = ring::aead::Nonce::assume_unique_for_key([0u8; 12]);
+        let aad = aead::Aad::from(additional_data);
+        let zero_nonce = aead::Nonce::assume_unique_for_key([0u8; 12]);
         Ok(self.open_in_place(zero_nonce, aad, data)?)
     }
 }
 
-impl From<ring::error::Unspecified> for CryptoError {
-    fn from(_: ring::error::Unspecified) -> Self {
+impl From<error::Unspecified> for CryptoError {
+    fn from(_: error::Unspecified) -> Self {
         Self
     }
 }
diff --git a/quinn-proto/src/crypto/rustls.rs b/quinn-proto/src/crypto/rustls.rs
index 214ad83b..b8415181 100644
--- a/quinn-proto/src/crypto/rustls.rs
+++ b/quinn-proto/src/crypto/rustls.rs
@@ -1,6 +1,9 @@
 use std::{any::Any, io, str, sync::Arc};
 
+#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
+use aws_lc_rs::aead;
 use bytes::BytesMut;
+#[cfg(feature = "ring")]
 use ring::aead;
 pub use rustls::Error;
 use rustls::{
@@ -307,14 +310,12 @@ impl QuicClientConfig {
     }
 
     pub(crate) fn inner(verifier: Arc<dyn ServerCertVerifier>) -> rustls::ClientConfig {
-        let mut config = rustls::ClientConfig::builder_with_provider(
-            rustls::crypto::ring::default_provider().into(),
-        )
-        .with_protocol_versions(&[&rustls::version::TLS13])
-        .unwrap() // The *ring* default provider supports TLS 1.3
-        .dangerous()
-        .with_custom_certificate_verifier(verifier)
-        .with_no_client_auth();
+        let mut config = rustls::ClientConfig::builder_with_provider(configured_provider())
+            .with_protocol_versions(&[&rustls::version::TLS13])
+            .unwrap() // The default providers support TLS 1.3
+            .dangerous()
+            .with_custom_certificate_verifier(verifier)
+            .with_no_client_auth();
 
         config.enable_early_data = true;
         config
@@ -446,13 +447,11 @@ impl QuicServerConfig {
         cert_chain: Vec<CertificateDer<'static>>,
         key: PrivateKeyDer<'static>,
     ) -> Result<rustls::ServerConfig, rustls::Error> {
-        let mut inner = rustls::ServerConfig::builder_with_provider(
-            rustls::crypto::ring::default_provider().into(),
-        )
-        .with_protocol_versions(&[&rustls::version::TLS13])
-        .unwrap() // The *ring* default provider supports TLS 1.3
-        .with_no_client_auth()
-        .with_single_cert(cert_chain, key)?;
+        let mut inner = rustls::ServerConfig::builder_with_provider(configured_provider())
+            .with_protocol_versions(&[&rustls::version::TLS13])
+            .unwrap() // The *ring* default provider supports TLS 1.3
+            .with_no_client_auth()
+            .with_single_cert(cert_chain, key)?;
 
         inner.max_early_data_size = u32::MAX;
         Ok(inner)
@@ -549,6 +548,14 @@ pub(crate) fn initial_suite_from_provider(
         .flatten()
 }
 
+pub(crate) fn configured_provider() -> Arc<rustls::crypto::CryptoProvider> {
+    #[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
+    let provider = rustls::crypto::aws_lc_rs::default_provider();
+    #[cfg(feature = "ring")]
+    let provider = rustls::crypto::ring::default_provider();
+    Arc::new(provider)
+}
+
 fn to_vec(params: &TransportParameters) -> Vec<u8> {
     let mut bytes = Vec::new();
     params.write(&mut bytes);
diff --git a/quinn-proto/src/endpoint.rs b/quinn-proto/src/endpoint.rs
index 47b4394f..93e41963 100644
--- a/quinn-proto/src/endpoint.rs
+++ b/quinn-proto/src/endpoint.rs
@@ -1211,6 +1211,11 @@ impl Incoming {
     pub fn remote_address_validated(&self) -> bool {
         self.retry_src_cid.is_some()
     }
+
+    /// The original destination connection ID sent by the client
+    pub fn orig_dst_cid(&self) -> &ConnectionId {
+        &self.orig_dst_cid
+    }
 }
 
 impl fmt::Debug for Incoming {
diff --git a/quinn-proto/src/lib.rs b/quinn-proto/src/lib.rs
index bb39101b..ad9d937a 100644
--- a/quinn-proto/src/lib.rs
+++ b/quinn-proto/src/lib.rs
@@ -32,7 +32,7 @@ mod cid_queue;
 pub mod coding;
 mod constant_time;
 mod range_set;
-#[cfg(all(test, feature = "rustls"))]
+#[cfg(all(test, any(feature = "rustls-aws-lc-rs", feature = "rustls-ring")))]
 mod tests;
 pub mod transport_parameters;
 mod varint;
@@ -43,8 +43,8 @@ mod connection;
 pub use crate::connection::{
     BytesSource, Chunk, Chunks, ClosedStream, Connection, ConnectionError, ConnectionStats,
     Datagrams, Event, FinishError, FrameStats, PathStats, ReadError, ReadableError, RecvStream,
-    RttEstimator, SendDatagramError, SendStream, StreamEvent, Streams, UdpStats, WriteError,
-    Written,
+    RttEstimator, SendDatagramError, SendStream, ShouldTransmit, StreamEvent, Streams, UdpStats,
+    WriteError, Written,
 };
 
 mod config;
diff --git a/quinn-proto/src/packet.rs b/quinn-proto/src/packet.rs
index ee1babcc..9135bd65 100644
--- a/quinn-proto/src/packet.rs
+++ b/quinn-proto/src/packet.rs
@@ -898,8 +898,6 @@ impl SpaceId {
 #[cfg(test)]
 mod tests {
     use super::*;
-    #[cfg(feature = "rustls")]
-    use crate::DEFAULT_SUPPORTED_VERSIONS;
     use hex_literal::hex;
     use std::io;
 
@@ -936,7 +934,7 @@ mod tests {
         }
     }
 
-    #[cfg(feature = "rustls")]
+    #[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
     #[test]
     fn header_encoding() {
         use crate::crypto::rustls::{initial_keys, initial_suite_from_provider};
@@ -955,7 +953,7 @@ mod tests {
             src_cid: ConnectionId::new(&[]),
             dst_cid: dcid,
             token: Bytes::new(),
-            version: DEFAULT_SUPPORTED_VERSIONS[0],
+            version: crate::DEFAULT_SUPPORTED_VERSIONS[0],
         });
         let encode = header.encode(&mut buf);
         let header_len = buf.len();
@@ -979,7 +977,7 @@ mod tests {
         );
 
         let server = initial_keys(Version::V1, &dcid, Side::Server, &suite);
-        let supported_versions = DEFAULT_SUPPORTED_VERSIONS.to_vec();
+        let supported_versions = crate::DEFAULT_SUPPORTED_VERSIONS.to_vec();
         let decode = PartialDecode::new(
             buf.as_slice().into(),
             &FixedLengthConnectionIdParser::new(0),
diff --git a/quinn-proto/src/token.rs b/quinn-proto/src/token.rs
index 8b63d43e..4fa60329 100644
--- a/quinn-proto/src/token.rs
+++ b/quinn-proto/src/token.rs
@@ -164,9 +164,13 @@ impl fmt::Display for ResetToken {
     }
 }
 
-#[cfg(test)]
+#[cfg(all(test, any(feature = "aws-lc-rs", feature = "ring")))]
 mod test {
+    #[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
+    use aws_lc_rs::hkdf;
     #[cfg(feature = "ring")]
+    use ring::hkdf;
+
     #[test]
     fn token_sanity() {
         use super::*;
@@ -184,7 +188,7 @@ mod test {
         let mut master_key = [0; 64];
         rng.fill_bytes(&mut master_key);
 
-        let prk = ring::hkdf::Salt::new(ring::hkdf::HKDF_SHA256, &[]).extract(&master_key);
+        let prk = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key);
 
         let addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 4433);
         let retry_src_cid = RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid();
@@ -200,7 +204,6 @@ mod test {
         assert_eq!(token.issued, decoded.issued);
     }
 
-    #[cfg(feature = "ring")]
     #[test]
     fn invalid_token_returns_err() {
         use super::*;
@@ -214,7 +217,7 @@ mod test {
         let mut master_key = [0; 64];
         rng.fill_bytes(&mut master_key);
 
-        let prk = ring::hkdf::Salt::new(ring::hkdf::HKDF_SHA256, &[]).extract(&master_key);
+        let prk = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(&master_key);
 
         let addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 4433);
         let retry_src_cid = RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid();
diff --git a/quinn/Cargo.toml b/quinn/Cargo.toml
index e64d1235..a54ef7b9 100644
--- a/quinn/Cargo.toml
+++ b/quinn/Cargo.toml
@@ -15,12 +15,19 @@ rust-version.workspace = true
 all-features = true
 
 [features]
-default = ["log", "platform-verifier", "ring", "runtime-tokio", "rustls"]
+default = ["log", "platform-verifier", "runtime-tokio", "rustls-ring"]
+# Enables `Endpoint::client` and `Endpoint::server` conveniences
+aws-lc-rs = ["proto/aws-lc-rs"]
 # Records how long locks are held, and warns if they are held >= 1ms
 lock_tracking = []
 # Provides `ClientConfig::with_platform_verifier()` convenience method
 platform-verifier = ["proto/platform-verifier"]
-rustls = ["dep:rustls", "proto/rustls", "proto/ring"]
+# For backwards compatibility, `rustls` forwards to `rustls-ring`
+rustls = ["rustls-ring"]
+# Enable rustls with the `aws-lc-rs` crypto provider
+rustls-aws-lc-rs = ["dep:rustls", "aws-lc-rs", "proto/rustls-aws-lc-rs", "proto/aws-lc-rs"]
+# Enable rustls with the `ring` crypto provider
+rustls-ring = ["dep:rustls", "ring", "proto/rustls-ring", "proto/ring"]
 # Enables `Endpoint::client` and `Endpoint::server` conveniences
 ring = ["proto/ring"]
 runtime-tokio = ["tokio/time", "tokio/rt", "tokio/net"]
diff --git a/quinn/examples/common/mod.rs b/quinn/examples/common/mod.rs
index 05e9f37b..0aa690f8 100644
--- a/quinn/examples/common/mod.rs
+++ b/quinn/examples/common/mod.rs
@@ -1,4 +1,4 @@
-#![cfg(feature = "rustls")]
+#![cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 //! Commonly used code in most examples.
 
 use quinn::{ClientConfig, Endpoint, ServerConfig};
diff --git a/quinn/src/endpoint.rs b/quinn/src/endpoint.rs
index 816be054..7dd20357 100644
--- a/quinn/src/endpoint.rs
+++ b/quinn/src/endpoint.rs
@@ -12,7 +12,7 @@ use std::{
     time::Instant,
 };
 
-#[cfg(feature = "ring")]
+#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
 use crate::runtime::default_runtime;
 use crate::{
     runtime::{AsyncUdpSocket, Runtime},
@@ -25,7 +25,7 @@ use proto::{
     EndpointEvent, ServerConfig,
 };
 use rustc_hash::FxHashMap;
-#[cfg(feature = "ring")]
+#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
 use socket2::{Domain, Protocol, Socket, Type};
 use tokio::sync::{futures::Notified, mpsc, Notify};
 use tracing::{Instrument, Span};
@@ -67,7 +67,7 @@ impl Endpoint {
     ///
     /// Some environments may not allow creation of dual-stack sockets, in which case an IPv6
     /// client will only be able to connect to IPv6 servers. An IPv4 client is never dual-stack.
-    #[cfg(feature = "ring")]
+    #[cfg(any(feature = "aws-lc-rs", feature = "ring"))] // `EndpointConfig::default()` is only available with these
     pub fn client(addr: SocketAddr) -> io::Result<Self> {
         let socket = Socket::new(Domain::for_address(addr), Type::DGRAM, Some(Protocol::UDP))?;
         if addr.is_ipv6() {
@@ -97,7 +97,7 @@ impl Endpoint {
     /// IPv6 address on Windows will not by default be able to communicate with IPv4
     /// addresses. Portable applications should bind an address that matches the family they wish to
     /// communicate within.
-    #[cfg(feature = "ring")]
+    #[cfg(any(feature = "aws-lc-rs", feature = "ring"))] // `EndpointConfig::default()` is only available with these
     pub fn server(config: ServerConfig, addr: SocketAddr) -> io::Result<Self> {
         let socket = std::net::UdpSocket::bind(addr)?;
         let runtime = default_runtime()
diff --git a/quinn/src/incoming.rs b/quinn/src/incoming.rs
index d776874b..e306a368 100644
--- a/quinn/src/incoming.rs
+++ b/quinn/src/incoming.rs
@@ -6,7 +6,7 @@ use std::{
     task::{Context, Poll},
 };
 
-use proto::{ConnectionError, ServerConfig};
+use proto::{ConnectionError, ConnectionId, ServerConfig};
 use thiserror::Error;
 
 use crate::{
@@ -85,6 +85,11 @@ impl Incoming {
     pub fn remote_address_validated(&self) -> bool {
         self.0.as_ref().unwrap().inner.remote_address_validated()
     }
+
+    /// The original destination CID when initiating the connection
+    pub fn orig_dst_cid(&self) -> ConnectionId {
+        *self.0.as_ref().unwrap().inner.orig_dst_cid()
+    }
 }
 
 impl Drop for Incoming {
diff --git a/quinn/src/lib.rs b/quinn/src/lib.rs
index ac93c1cb..31e13677 100644
--- a/quinn/src/lib.rs
+++ b/quinn/src/lib.rs
@@ -66,15 +66,15 @@ pub use proto::{
     ConfigError, ConnectError, ConnectionClose, ConnectionError, ConnectionStats, EndpointConfig,
     IdleTimeout, MtuDiscoveryConfig, ServerConfig, StreamId, Transmit, TransportConfig, VarInt,
 };
-#[cfg(feature = "rustls")]
+#[cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 pub use rustls;
 pub use udp;
 
 pub use crate::connection::{
-    AcceptBi, AcceptUni, Connecting, Connection, OpenBi, OpenUni, ReadDatagram, SendDatagramError,
-    ZeroRttAccepted,
+    AcceptBi, AcceptUni, Connecting, Connection, OpenBi, OpenUni, ReadDatagram, SendDatagram,
+    SendDatagramError, ZeroRttAccepted,
 };
-pub use crate::endpoint::{Accept, Endpoint};
+pub use crate::endpoint::{Accept, Endpoint, EndpointStats};
 pub use crate::incoming::{Incoming, IncomingFuture, RetryError};
 pub use crate::recv_stream::{ReadError, ReadExactError, ReadToEndError, RecvStream, ResetError};
 #[cfg(feature = "runtime-async-std")]


Test patch:
diff --git a/quinn-proto/src/tests/mod.rs b/quinn-proto/src/tests/mod.rs
index 50bb6ef0..6e2336b3 100644
--- a/quinn-proto/src/tests/mod.rs
+++ b/quinn-proto/src/tests/mod.rs
@@ -7,9 +7,12 @@ use std::{
 };
 
 use assert_matches::assert_matches;
+#[cfg(all(feature = "aws-lc-rs", not(feature = "ring")))]
+use aws_lc_rs::hmac;
 use bytes::{Bytes, BytesMut};
 use hex_literal::hex;
 use rand::RngCore;
+#[cfg(feature = "ring")]
 use ring::hmac;
 use rustls::{
     pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer},
diff --git a/quinn-proto/src/tests/util.rs b/quinn-proto/src/tests/util.rs
index 5c66a263..b293057e 100644
--- a/quinn-proto/src/tests/util.rs
+++ b/quinn-proto/src/tests/util.rs
@@ -21,7 +21,7 @@ use rustls::{
 };
 use tracing::{info_span, trace};
 
-use super::crypto::rustls::{QuicClientConfig, QuicServerConfig};
+use super::crypto::rustls::{configured_provider, QuicClientConfig, QuicServerConfig};
 use super::*;
 
 pub(super) const DEFAULT_MTU: usize = 1452;
@@ -640,7 +640,7 @@ fn client_crypto_inner(
     }
 
     let mut inner = QuicClientConfig::inner(
-        WebPkiServerVerifier::builder(Arc::new(roots))
+        WebPkiServerVerifier::builder_with_provider(Arc::new(roots), configured_provider())
             .build()
             .unwrap(),
     );
diff --git a/quinn/src/tests.rs b/quinn/src/tests.rs
index fe83d834..dca2e111 100755
--- a/quinn/src/tests.rs
+++ b/quinn/src/tests.rs
@@ -1,4 +1,4 @@
-#![cfg(feature = "rustls")]
+#![cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 
 use std::{
     convert::TryInto,
diff --git a/quinn/tests/many_connections.rs b/quinn/tests/many_connections.rs
index ff98539a..b97e58fb 100644
--- a/quinn/tests/many_connections.rs
+++ b/quinn/tests/many_connections.rs
@@ -1,4 +1,4 @@
-#![cfg(feature = "rustls")]
+#![cfg(any(feature = "rustls-aws-lc-rs", feature = "rustls-ring"))]
 use std::{
     convert::TryInto,
     sync::{Arc, Mutex},