77// You may not use this file except in accordance with one or both of these
88// licenses.
99
10+ use std:: collections:: BTreeMap ;
1011use std:: future:: Future ;
12+ use std:: net:: IpAddr ;
1113use std:: pin:: Pin ;
12- use std:: sync:: Arc ;
14+ use std:: sync:: { Arc , Mutex } ;
1315use std:: time:: Duration ;
1416
1517use 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} ;
4648use prost:: Message ;
47- use tokio:: sync:: { broadcast, mpsc, Semaphore } ;
49+ use tokio:: sync:: { broadcast, mpsc} ;
4850
4951use crate :: api:: bolt11_claim_for_id:: handle_bolt11_claim_for_id_request;
5052use 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
102104const MAX_BODY_SIZE : usize = 10 * 1024 * 1024 ;
103105const 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.
106111const 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 ) ]
110155pub ( 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 {
659703mod 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