Repository navigation
Harden gRPC transport #266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
edacdef
9d3f44e
4cf2cb8
4dd8f17
4b6679f
ed4f201
babe787
8c7880f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,7 +39,7 @@ use prost::Message; | |
| use tokio::net::TcpListener; | ||
| use tokio::select; | ||
| use tokio::signal::unix::{Signal, SignalKind}; | ||
| use tokio::sync::broadcast; | ||
| use tokio::sync::{broadcast, Semaphore}; | ||
|
|
||
| use crate::api::node_to_proto_custom_tlv; | ||
| use crate::macaroons::MacaroonStore; | ||
|
|
@@ -58,6 +58,9 @@ const KV_STORE_IN_USE_RETRY_TIMEOUT: Duration = Duration::from_secs(60); | |
| const KV_STORE_IN_USE_RETRY_INTERVAL: Duration = Duration::from_secs(2); | ||
| pub(crate) const FULL_VERSION: &str = | ||
| concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), ")"); | ||
| const MAX_CONCURRENT_HTTP2_STREAMS: u32 = 32; | ||
| const MAX_PENDING_TLS_HANDSHAKES: usize = 64; | ||
| const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10); | ||
|
|
||
| pub fn get_default_data_dir() -> Option<PathBuf> { | ||
| #[cfg(target_os = "macos")] | ||
|
|
@@ -406,6 +409,7 @@ fn main() { | |
| None | ||
| }; | ||
|
|
||
| let tls_handshake_semaphore = Arc::new(Semaphore::new(MAX_PENDING_TLS_HANDSHAKES)); | ||
| info!("gRPC service listening on {}", config_file.grpc_service_addr); | ||
|
|
||
| systemd::notify_ready(); | ||
|
|
@@ -721,25 +725,46 @@ fn main() { | |
| }, | ||
| res = grpc_listener.accept() => { | ||
| match res { | ||
| Ok((stream, _)) => { | ||
| Ok((stream, peer_addr)) => { | ||
| let handshake_permit = | ||
| match Arc::clone(&tls_handshake_semaphore).try_acquire_owned() { | ||
| Ok(permit) => permit, | ||
| Err(_) => { | ||
| debug!("TLS handshake limit reached, rejecting connection"); | ||
| continue; | ||
| }, | ||
| }; | ||
| let node_service = NodeService::new( | ||
| Arc::clone(&node), | ||
| Arc::clone(&macaroon_store), | ||
| metrics.clone(), | ||
| metrics_auth_header.clone(), | ||
| event_sender.clone(), | ||
| shutdown_rx.clone(), | ||
| peer_addr.ip(), | ||
| ); | ||
| let acceptor = tls_acceptor.clone(); | ||
| runtime.spawn(async move { | ||
| match acceptor.accept(stream).await { | ||
| Ok(tls_stream) => { | ||
| match tokio::time::timeout( | ||
| TLS_HANDSHAKE_TIMEOUT, | ||
| acceptor.accept(stream), | ||
| ) | ||
| .await | ||
| { | ||
| Ok(Ok(tls_stream)) => { | ||
| // Only the handshake holds a slot. Holding it for the whole | ||
| // connection would let an unauthenticated peer block new | ||
| // connections by keeping established ones idle. | ||
| drop(handshake_permit); | ||
| let io_stream = TokioIo::new(tls_stream); | ||
| if let Err(err) = http2::Builder::new(TokioExecutor::new()).serve_connection(io_stream, node_service).await { | ||
| let mut builder = http2::Builder::new(TokioExecutor::new()); | ||
| builder.max_concurrent_streams(MAX_CONCURRENT_HTTP2_STREAMS); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The semaphore slot is released once the handshake succeeds (nice), but nothing else bounds the connections that follow: there is no connection cap, no idle timeout, and no HTTP/2 keepalive ( Could we add an overall connection limit plus an idle/keepalive timeout (and maybe |
||
| if let Err(err) = builder.serve_connection(io_stream, node_service).await { | ||
| error!("Failed to serve TLS connection: {err}"); | ||
| } | ||
| }, | ||
| Err(e) => error!("TLS handshake failed: {e}"), | ||
| Ok(Err(e)) => error!("TLS handshake failed: {e}"), | ||
| Err(_) => debug!("TLS handshake timed out"), | ||
| } | ||
| }); | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 64-slot pending-handshake semaphore is shared by all peers, and each slot can be held for up to
TLS_HANDSHAKE_TIMEOUT(10 s) by a connection that never sends a ClientHello. One unauthenticated host can keep re-opening such connections as they expire and hold all 64 slots, so new legitimate connections hit thecontinueat line 740 and are dropped (existing connections keep working, but a new CLI run or a reconnect mostly fails).This is the same starvation pattern as the body-read pool discussed above, just on the handshake side. Since
peer_addr.ip()is already available here, could we also cap pending handshakes per IP (say 4–8), and maybe shorten the timeout to a few seconds? A TLS handshake on a healthy link takes milliseconds.