Skip to content

Commit a2cd064

Browse files
committed
fix(ws): install a rustls crypto provider so the TLS connect does not panic
1 parent 3ae1aae commit a2cd064

3 files changed

Lines changed: 59 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ bridge = []
3131
ctf = ["alloy/contract", "alloy/providers"]
3232
rfq = []
3333
tracing = ["dep:tracing", "dep:serde_ignored", "dep:serde_path_to_error"]
34-
ws = ["dep:backoff", "dep:bitflags", "dep:tokio", "dep:tokio-tungstenite"]
35-
rtds = ["dep:backoff", "dep:tokio", "dep:tokio-tungstenite"]
34+
ws = ["dep:backoff", "dep:bitflags", "dep:rustls", "dep:tokio", "dep:tokio-tungstenite"]
35+
rtds = ["dep:backoff", "dep:rustls", "dep:tokio", "dep:tokio-tungstenite"]
3636
heartbeats = ["dep:tokio", "dep:tokio-util"]
3737

3838
[dependencies]
@@ -72,6 +72,7 @@ sha1 = "0.10.6"
7272
sha2 = "0.10.9"
7373
strum_macros = "0.28.0"
7474
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros"], optional = true }
75+
rustls = { version = "0.23.38", optional = true }
7576
tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-native-roots"], optional = true }
7677
tokio-util = { version = "0.7.18", optional = true }
7778
tracing = { version = "0.1", optional = true }
@@ -386,3 +387,8 @@ unwrap_used = "warn"
386387
[profile.bench]
387388
lto = "thin" # Link-Time Optimization: enables cross-crate inlining
388389
codegen-units = 1 # Single codegen unit allows more aggressive optimizations
390+
391+
392+
393+
394+

src/ws/connection.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::marker::PhantomData;
88
use std::time::Instant;
99

1010
use backoff::backoff::Backoff as _;
11-
use futures::{SinkExt as _, StreamExt as _};
11+
use futures::{FutureExt as _, SinkExt as _, StreamExt as _};
1212
use serde::Serialize;
1313
use serde::de::DeserializeOwned;
1414
use tokio::net::TcpStream;
@@ -29,6 +29,23 @@ type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
2929
/// Broadcast channel capacity for incoming messages.
3030
const BROADCAST_CAPACITY: usize = 1024;
3131

32+
/// Install a default rustls crypto provider if the process does not already have one.
33+
///
34+
/// Establishing the TLS connection resolves the process-level rustls `CryptoProvider`
35+
/// from crate features, which panics when more than one provider is present in the
36+
/// dependency graph. That happens readily in practice, because an application only has
37+
/// to pull in `ring` alongside the `aws-lc-rs` that rustls enables by default. Installing
38+
/// a provider explicitly avoids the panic, and an application that has already installed
39+
/// its own provider is left untouched.
40+
fn ensure_crypto_provider() {
41+
static INIT: std::sync::Once = std::sync::Once::new();
42+
INIT.call_once(|| {
43+
if rustls::crypto::CryptoProvider::get_default().is_none() {
44+
_ = rustls::crypto::aws_lc_rs::default_provider().install_default();
45+
}
46+
});
47+
}
48+
3249
/// Connection state tracking.
3350
#[non_exhaustive]
3451
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -119,22 +136,36 @@ where
119136
let (broadcast_tx, _) = broadcast::channel(BROADCAST_CAPACITY);
120137
let (state_tx, state_rx) = watch::channel(ConnectionState::Disconnected);
121138

139+
ensure_crypto_provider();
140+
122141
// Spawn connection task
123142
let connection_config = config;
124143
let connection_endpoint = endpoint;
125144
let broadcast_tx_clone = broadcast_tx.clone();
126145
let state_tx_clone = state_tx.clone();
146+
let panic_state_tx = state_tx.clone();
127147

128148
tokio::spawn(async move {
129-
Self::connection_loop(
149+
let connection_loop = std::panic::AssertUnwindSafe(Self::connection_loop(
130150
connection_endpoint,
131151
connection_config,
132152
sender_rx,
133153
broadcast_tx_clone,
134154
parser,
135155
state_tx_clone,
136-
)
137-
.await;
156+
));
157+
158+
if connection_loop.catch_unwind().await.is_err() {
159+
// This task is detached, so a panic in the connection loop is otherwise
160+
// swallowed: the manager would keep reporting whatever state it reached
161+
// last (typically `Connecting`) and every subscription stream would hang
162+
// with no error and no data. Report the connection as disconnected so the
163+
// failure is at least observable.
164+
_ = panic_state_tx.send(ConnectionState::Disconnected);
165+
166+
#[cfg(feature = "tracing")]
167+
tracing::error!("websocket connection task panicked; reporting disconnected");
168+
}
138169
});
139170

140171
Ok(Self {
@@ -424,3 +455,17 @@ where
424455
self.state_tx.subscribe()
425456
}
426457
}
458+
459+
#[cfg(test)]
460+
mod tests {
461+
use super::ensure_crypto_provider;
462+
463+
#[test]
464+
fn ensure_crypto_provider_installs_a_default() {
465+
// More than one rustls crypto provider is present in the dependency graph, so
466+
// the process-level default cannot be resolved from crate features and setting
467+
// up TLS panics. Installing one keeps that from happening.
468+
ensure_crypto_provider();
469+
assert!(rustls::crypto::CryptoProvider::get_default().is_some());
470+
}
471+
}

0 commit comments

Comments
 (0)