Skip to content

Commit 5103143

Browse files
committed
fixup! Bound concurrent request buffering
Limit concurrent body reads per source IP across connections so one peer cannot occupy the global pool with replacement stalled requests. Keep the global limit and body deadline, and release peer records when reads finish, fail, or are cancelled. Test repeated saturation while another peer reads and authenticates a request, plus global capacity, address normalization, and cleanup. This change was made with assistance from Codex.
1 parent 08d14ca commit 5103143

3 files changed

Lines changed: 213 additions & 42 deletions

File tree

‎ldk-server/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ default = []
3939
experimental-lsps2-support = []
4040

4141
[dev-dependencies]
42+
tokio = { version = "1.38.0", default-features = false, features = ["test-util"] }
4243
futures-util = "0.3.31"

‎ldk-server/src/main.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ fn main() {
717717
},
718718
res = grpc_listener.accept() => {
719719
match res {
720-
Ok((stream, _)) => {
720+
Ok((stream, peer_addr)) => {
721721
let handshake_permit =
722722
match Arc::clone(&tls_handshake_semaphore).try_acquire_owned() {
723723
Ok(permit) => permit,
@@ -733,6 +733,7 @@ fn main() {
733733
metrics_auth_header.clone(),
734734
event_sender.clone(),
735735
shutdown_rx.clone(),
736+
peer_addr.ip(),
736737
);
737738
let acceptor = tls_acceptor.clone();
738739
runtime.spawn(async move {

‎ldk-server/src/service.rs‎

Lines changed: 210 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
// You may not use this file except in accordance with one or both of these
88
// licenses.
99

10+
use std::collections::BTreeMap;
1011
use std::future::Future;
12+
use std::net::IpAddr;
1113
use std::pin::Pin;
12-
use std::sync::Arc;
14+
use std::sync::{Arc, Mutex};
1315
use std::time::Duration;
1416

1517
use http_body_util::{BodyExt, Limited};
@@ -44,7 +46,7 @@ use ldk_server_grpc::grpc::{
4446
GRPC_STATUS_UNAUTHENTICATED, GRPC_STATUS_UNAVAILABLE, GRPC_STATUS_UNIMPLEMENTED,
4547
};
4648
use prost::Message;
47-
use tokio::sync::{broadcast, mpsc, Semaphore};
49+
use tokio::sync::{broadcast, mpsc};
4850

4951
use crate::api::bolt11_claim_for_id::handle_bolt11_claim_for_id_request;
5052
use crate::api::bolt11_fail_for_id::handle_bolt11_fail_for_id_request;
@@ -101,13 +103,57 @@ const GRPC_SERVICE_PREFIX: &str = "/api.LightningNode/";
101103
// Maximum request body size: 10 MB
102104
const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
103105
const MAX_CONCURRENT_BODY_READS: usize = 8;
106+
// Share this limit across all connections from the same source IP. Authentication
107+
// covers the body, so it cannot be checked before reserving buffer space.
108+
const MAX_BODY_READS_PER_PEER: usize = 2;
104109
// A client that stalls mid-body would otherwise hold one of the few body-read slots
105110
// indefinitely.
106111
const REQUEST_BODY_TIMEOUT: Duration = Duration::from_secs(30);
107-
static REQUEST_BODY_SEMAPHORE: Semaphore = Semaphore::const_new(MAX_CONCURRENT_BODY_READS);
112+
static REQUEST_BODY_LIMITER: RequestBodyLimiter = RequestBodyLimiter::new();
113+
114+
// Track only active reads; the map has at most MAX_CONCURRENT_BODY_READS entries.
115+
struct RequestBodyLimiter {
116+
active: Mutex<BTreeMap<IpAddr, usize>>,
117+
}
118+
119+
impl RequestBodyLimiter {
120+
const fn new() -> Self {
121+
Self { active: Mutex::new(BTreeMap::new()) }
122+
}
123+
124+
fn try_acquire(&self, peer_ip: IpAddr) -> Result<RequestBodyPermit<'_>, GrpcStatus> {
125+
// Treat IPv4 and its IPv4-mapped IPv6 representation as the same peer.
126+
let peer_ip = peer_ip.to_canonical();
127+
let mut active = self.active.lock().unwrap();
128+
if active.get(&peer_ip).copied().unwrap_or(0) >= MAX_BODY_READS_PER_PEER
129+
|| active.values().sum::<usize>() >= MAX_CONCURRENT_BODY_READS
130+
{
131+
return Err(GrpcStatus::new(GRPC_STATUS_UNAVAILABLE, "Too many concurrent requests"));
132+
}
133+
*active.entry(peer_ip).or_default() += 1;
134+
Ok(RequestBodyPermit { limiter: self, peer_ip })
135+
}
136+
}
137+
138+
struct RequestBodyPermit<'a> {
139+
limiter: &'a RequestBodyLimiter,
140+
peer_ip: IpAddr,
141+
}
142+
143+
impl Drop for RequestBodyPermit<'_> {
144+
fn drop(&mut self) {
145+
let mut active = self.limiter.active.lock().unwrap();
146+
let count = active.get_mut(&self.peer_ip).unwrap();
147+
*count -= 1;
148+
if *count == 0 {
149+
active.remove(&self.peer_ip);
150+
}
151+
}
152+
}
108153

109154
#[derive(Clone)]
110155
pub(crate) struct NodeService {
156+
peer_ip: IpAddr,
111157
context: Arc<Context>,
112158
api_key: String,
113159
metrics: Option<Arc<Metrics>>,
@@ -120,10 +166,10 @@ impl NodeService {
120166
pub(crate) fn new(
121167
node: Arc<Node>, api_key: String, metrics: Option<Arc<Metrics>>,
122168
metrics_auth_header: Option<String>, event_sender: broadcast::Sender<EventEnvelope>,
123-
shutdown_rx: tokio::sync::watch::Receiver<bool>,
169+
shutdown_rx: tokio::sync::watch::Receiver<bool>, peer_ip: IpAddr,
124170
) -> Self {
125171
let context = Arc::new(Context { node });
126-
Self { context, api_key, metrics, metrics_auth_header, event_sender, shutdown_rx }
172+
Self { context, api_key, metrics, metrics_auth_header, event_sender, shutdown_rx, peer_ip }
127173
}
128174
}
129175

@@ -275,36 +321,23 @@ impl Service<Request<Incoming>> for NodeService {
275321
let event_sender = self.event_sender.clone();
276322
let shutdown_rx = self.shutdown_rx.clone();
277323
let (request_parts, request_body) = req.into_parts();
324+
let peer_ip = self.peer_ip;
278325
let future: Self::Future = Box::pin(async move {
279-
let body_permit = match REQUEST_BODY_SEMAPHORE.try_acquire() {
280-
Ok(permit) => permit,
281-
Err(_) => {
282-
return Ok(grpc_error_response(GrpcStatus::new(
283-
GRPC_STATUS_UNAVAILABLE,
284-
"Too many concurrent requests",
285-
)));
286-
},
287-
};
288326
let content_length = match request_content_length(&request_parts.headers) {
289327
Ok(content_length) => content_length,
290328
Err(status) => return Ok(grpc_error_response(status)),
291329
};
292-
let body_bytes = match tokio::time::timeout(
293-
REQUEST_BODY_TIMEOUT,
294-
read_request_body(request_body, content_length),
330+
let body_bytes = match read_request_body(
331+
request_body,
332+
content_length,
333+
peer_ip,
334+
&REQUEST_BODY_LIMITER,
295335
)
296336
.await
297337
{
298-
Ok(Ok(bytes)) => bytes,
299-
Ok(Err(status)) => return Ok(grpc_error_response(status)),
300-
Err(_) => {
301-
return Ok(grpc_error_response(GrpcStatus::new(
302-
GRPC_STATUS_UNAVAILABLE,
303-
"Timed out reading request body",
304-
)));
305-
},
338+
Ok(bytes) => bytes,
339+
Err(status) => return Ok(grpc_error_response(status)),
306340
};
307-
drop(body_permit);
308341

309342
let auth_req = Request::from_parts(request_parts, ());
310343
if let Err(e) = validate_auth(&auth_req, &api_key, &body_bytes) {
@@ -627,21 +660,32 @@ fn validate_request_body_len(
627660
Ok(())
628661
}
629662

630-
async fn read_request_body(
631-
body: Incoming, content_length: Option<u64>,
632-
) -> Result<bytes::Bytes, GrpcStatus> {
633-
let limited_body = Limited::new(body, MAX_BODY_SIZE);
634-
let bytes = match limited_body.collect().await {
635-
Ok(collected) => collected.to_bytes(),
636-
Err(_) => {
637-
return Err(GrpcStatus::new(
638-
GRPC_STATUS_INVALID_ARGUMENT,
639-
"Request body too large or failed to read",
640-
));
641-
},
642-
};
643-
validate_request_body_len(content_length, bytes.len())?;
644-
Ok(bytes)
663+
async fn read_request_body<B>(
664+
body: B, content_length: Option<u64>, peer_ip: IpAddr, limiter: &RequestBodyLimiter,
665+
) -> Result<bytes::Bytes, GrpcStatus>
666+
where
667+
B: hyper::body::Body<Data = bytes::Bytes>,
668+
B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
669+
{
670+
let _permit = limiter.try_acquire(peer_ip)?;
671+
tokio::time::timeout(REQUEST_BODY_TIMEOUT, async move {
672+
let limited_body = Limited::new(body, MAX_BODY_SIZE);
673+
let bytes = match limited_body.collect().await {
674+
Ok(collected) => collected.to_bytes(),
675+
Err(_) => {
676+
return Err(GrpcStatus::new(
677+
GRPC_STATUS_INVALID_ARGUMENT,
678+
"Request body too large or failed to read",
679+
));
680+
},
681+
};
682+
validate_request_body_len(content_length, bytes.len())?;
683+
Ok(bytes)
684+
})
685+
.await
686+
.unwrap_or_else(|_| {
687+
Err(GrpcStatus::new(GRPC_STATUS_UNAVAILABLE, "Timed out reading request body"))
688+
})
645689
}
646690

647691
/// Map an `LdkServerError` to a `GrpcStatus`.
@@ -659,6 +703,131 @@ pub(crate) fn ldk_error_to_grpc_status(e: LdkServerError) -> GrpcStatus {
659703
mod tests {
660704
use super::*;
661705

706+
fn stalled_body(
707+
) -> impl hyper::body::Body<Data = bytes::Bytes, Error = std::convert::Infallible> {
708+
http_body_util::StreamBody::new(futures_util::stream::pending::<
709+
Result<hyper::body::Frame<bytes::Bytes>, std::convert::Infallible>,
710+
>())
711+
}
712+
713+
#[test]
714+
fn test_request_body_sustained_peer_saturation() {
715+
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(
716+
async {
717+
tokio::time::pause();
718+
let limiter = RequestBodyLimiter::new();
719+
let attacker = "192.0.2.1".parse().unwrap();
720+
let client = "192.0.2.2".parse().unwrap();
721+
722+
// Refill the attacker's slots after each deadline, as replacement
723+
// streams on either existing or new connections would do.
724+
for _ in 0..10 {
725+
let mut stalled = Vec::new();
726+
for _ in 0..MAX_BODY_READS_PER_PEER {
727+
let mut read =
728+
Box::pin(read_request_body(stalled_body(), None, attacker, &limiter));
729+
assert!(futures_util::poll!(&mut read).is_pending());
730+
stalled.push(read);
731+
}
732+
for _ in 0..MAX_CONCURRENT_BODY_READS {
733+
let err = read_request_body(stalled_body(), None, attacker, &limiter)
734+
.await
735+
.unwrap_err();
736+
assert_eq!(err.code, GRPC_STATUS_UNAVAILABLE);
737+
assert_eq!(err.message, "Too many concurrent requests");
738+
}
739+
740+
// A correctly signed request can still collect its body and
741+
// authenticate while the attacker's bodies remain unfinished.
742+
let body = encode_grpc_frame(&[]);
743+
let timestamp = std::time::SystemTime::now()
744+
.duration_since(std::time::UNIX_EPOCH)
745+
.unwrap()
746+
.as_secs();
747+
let hmac = compute_hmac("test_key", timestamp, &body);
748+
let req = create_test_request(Some(format!("HMAC {timestamp}:{hmac}")));
749+
let received = read_request_body(
750+
http_body_util::Full::new(body.clone()),
751+
Some(body.len() as u64),
752+
client,
753+
&limiter,
754+
)
755+
.await
756+
.unwrap();
757+
assert_eq!(received, body);
758+
assert!(validate_auth(&req, "test_key", &received).is_ok());
759+
760+
tokio::time::advance(REQUEST_BODY_TIMEOUT).await;
761+
for read in stalled {
762+
let err = read.await.unwrap_err();
763+
assert_eq!(err.code, GRPC_STATUS_UNAVAILABLE);
764+
assert_eq!(err.message, "Timed out reading request body");
765+
}
766+
assert!(limiter.active.lock().unwrap().is_empty());
767+
}
768+
},
769+
);
770+
}
771+
772+
#[test]
773+
fn test_request_body_global_limit_and_peer_cleanup() {
774+
let limiter = RequestBodyLimiter::new();
775+
let mut permits = Vec::new();
776+
for i in 1..=MAX_CONCURRENT_BODY_READS {
777+
permits.push(limiter.try_acquire(IpAddr::from([192, 0, 2, i as u8])).unwrap());
778+
}
779+
let next_peer = "192.0.2.100".parse().unwrap();
780+
assert!(limiter.try_acquire(next_peer).is_err());
781+
assert_eq!(limiter.active.lock().unwrap().len(), MAX_CONCURRENT_BODY_READS);
782+
permits.pop();
783+
permits.push(limiter.try_acquire(next_peer).unwrap());
784+
drop(permits);
785+
assert!(limiter.active.lock().unwrap().is_empty());
786+
787+
let peer = "192.0.2.1".parse().unwrap();
788+
let mapped_peer = "::ffff:192.0.2.1".parse().unwrap();
789+
let _first = limiter.try_acquire(peer).unwrap();
790+
let _second = limiter.try_acquire(mapped_peer).unwrap();
791+
assert!(limiter.try_acquire(peer).is_err());
792+
assert!(limiter.try_acquire(mapped_peer).is_err());
793+
}
794+
795+
#[test]
796+
fn test_request_body_releases_slots_on_cancel_and_error() {
797+
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(
798+
async {
799+
let limiter = RequestBodyLimiter::new();
800+
let peer = "192.0.2.1".parse().unwrap();
801+
let mut read = Box::pin(read_request_body(stalled_body(), None, peer, &limiter));
802+
assert!(futures_util::poll!(&mut read).is_pending());
803+
drop(read);
804+
assert!(limiter.active.lock().unwrap().is_empty());
805+
806+
let err = read_request_body(
807+
http_body_util::Full::new(bytes::Bytes::from_static(b"short")),
808+
Some(10),
809+
peer,
810+
&limiter,
811+
)
812+
.await
813+
.unwrap_err();
814+
assert_eq!(err.code, GRPC_STATUS_INVALID_ARGUMENT);
815+
assert!(limiter.active.lock().unwrap().is_empty());
816+
817+
let err = read_request_body(
818+
http_body_util::Full::new(bytes::Bytes::from(vec![0; MAX_BODY_SIZE + 1])),
819+
None,
820+
peer,
821+
&limiter,
822+
)
823+
.await
824+
.unwrap_err();
825+
assert_eq!(err.code, GRPC_STATUS_INVALID_ARGUMENT);
826+
assert!(limiter.active.lock().unwrap().is_empty());
827+
},
828+
);
829+
}
830+
662831
fn compute_hmac(api_key: &str, timestamp: u64, body: &[u8]) -> String {
663832
compute_auth_hmac(api_key, timestamp, body).to_string()
664833
}

0 commit comments

Comments
 (0)