wss-proxy: time out handshakes so the accept loop can't be stalled
What changed, and why it matters
This update fixes a bug in Core Lightning's WebSocket Secure (WSS) proxy where a single client could connect but never finish its handshake, accidentally freezing the entire WSS service so no one else could connect. The fix moves each connection's TLS and WebSocket handshakes into its own background task and adds a 10-second timeout, so a stuck client is dropped without blocking others.
Deploy this patch to any node running the wss-proxy plugin, especially nodes exposing WSS to untrusted networks. Monitor logs for 'Timed out upgrading to tls' or 'Timed out upgrading to websocket' to confirm the fix is active. No immediate incident response beyond patching is indicated.
Security signals we found
Denial-of-service vector: single idle client blocks accept loop
Missing timeout on network handshake operations
Network-facing plugin (wss-proxy) availability issue
Changelog-Fixed labels this as a fixed bug affecting all new connections
Evidence from the diff
The wss-proxy accept loop previously awaited TLS and WebSocket handshakes inline before spawning per-connection work. A client that opened a TCP connection and then idled during either handshake would stall the accept loop, causing a denial of service for the WSS endpoint. The patch spawns a tokio task immediately after accept(), performs both acceptor.accept() and accept_async_with_config() under tokio::time::timeout(Duration::from_secs(10)), and releases the connection permit on failure, restoring availability.
Changed components
plugins/wss-proxy-plugin/src/main.rsplugins/wss-proxy-plugin/Cargo.tomlInspect captured patch +42 / −20
### plugins/wss-proxy-plugin/Cargo.toml
@@ -19,6 +19,7 @@ tokio = { version = "1", features = [
'sync',
'macros',
'io-util',
+ 'time',
] }
rcgen = "0.14"
futures-util = { version = "0.3", default-features = false, features = [
### plugins/wss-proxy-plugin/src/main.rs
@@ -1,4 +1,4 @@
-use std::{net::SocketAddr, process, sync::Arc};
+use std::{net::SocketAddr, process, sync::Arc, time::Duration};
use anyhow::anyhow;
use certs::get_tls_config;
@@ -112,30 +112,51 @@ async fn start_proxy(
) -> Result<(), anyhow::Error> {
let listener = TcpListener::bind(wss_address).await?;
let connection_slots = Arc::new(Semaphore::new(MAX_CONNECTIONS));
- log::info!("Websocket Secure Server Started at {}", wss_address);
+ log::info!("Websocket Secure Server Started at {wss_address}");
+
+ let handshake_timeout = Duration::from_secs(10);
loop {
let permit = connection_slots.clone().acquire_owned().await?;
if let Ok((stream, _)) = listener.accept().await {
- let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config.clone()));
- let tls_stream = match tls_acceptor.accept(stream).await {
- Ok(o) => o,
- Err(e) => {
- log::debug!("Error upgrading to tls: {}", e);
- continue;
- }
- };
- let wss_stream = match accept_async_with_config(tls_stream, Some(ws_config())).await {
- Ok(o) => o,
- Err(e) => {
- log::debug!("Error upgrading to websocket: {}", e);
- continue;
- }
- };
+ let tls_config_clone = tls_config.clone();
+ let ws_address = wss_proxy_options.ws_address;
tokio::spawn(async move {
- match relay_messages(wss_stream, wss_proxy_options.ws_address, permit).await {
- Ok(_) => (),
- Err(e) => log::info!("Error relaying messages: {}", e),
+ let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config_clone));
+ let tls_stream = match tokio::time::timeout(
+ handshake_timeout,
+ tls_acceptor.accept(stream),
+ )
+ .await
+ {
+ Ok(Ok(o)) => o,
+ Ok(Err(e)) => {
+ log::debug!("Error upgrading to tls: {e}");
+ return;
+ }
+ Err(_) => {
+ log::debug!("Timed out upgrading to tls");
+ return;
+ }
+ };
+ let wss_stream = match tokio::time::timeout(
+ handshake_timeout,
+ accept_async_with_config(tls_stream, Some(ws_config())),
+ )
+ .await
+ {
+ Ok(Ok(o)) => o,
+ Ok(Err(e)) => {
+ log::debug!("Error upgrading to websocket: {e}");
+ return;
+ }
+ Err(_) => {
+ log::debug!("Timed out upgrading to websocket");
+ return;
+ }
+ };
+ if let Err(e) = relay_messages(wss_stream, ws_address, permit).await {
+ log::info!("Error relaying messages: {e}");
}
});
} else {Why this scored 63/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.