Skip to content

Commit 6cebc12

Browse files
Add LSPS5 DOS protections.
When handling an incoming LSPS5 request, the manager will check if the counterparty is 'engaged' in some way before responding. `Engaged` meaning = active channel | LSPS2 active operation | LSPS1 active operation. Logic: `If not engaged then reject request;` A single test is added only checking for the active channel condition, because it's not super easy to get LSPS1-2 on the correct state to check this (yet). Other tangential work is happening that will make this easier and more tests will come in the near future
1 parent 3b939c0 commit 6cebc12

File tree

7 files changed

+229
-17
lines changed

7 files changed

+229
-17
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//! DoS Protection Enforcement for LSP operations.
2+
//!
3+
//! This module provides mechanisms to prevent denial-of-service attacks
4+
//! when using the Lightning Service Provider (LSP) protocols.
5+
6+
#[cfg(lsps1_service)]
7+
use crate::lsps1::service::LSPS1ServiceHandler;
8+
use crate::lsps2::service::LSPS2ServiceHandler;
9+
use crate::lsps5::service::LSPS5ServiceHandler;
10+
use crate::utils::time::TimeProvider;
11+
use bitcoin::secp256k1::PublicKey;
12+
use core::ops::Deref;
13+
#[cfg(lsps1_service)]
14+
use lightning::chain::Filter;
15+
use lightning::ln::channelmanager::AChannelManager;
16+
#[cfg(lsps1_service)]
17+
use lightning::sign::EntropySource;
18+
use lightning::sign::NodeSigner;
19+
20+
/// A trait for implementing Denial-of-Service (DoS) protection mechanisms for LSP services.
21+
pub trait DosProtectionEnforcer {
22+
/// Checks if the specified peer is currently engaged in an ongoing operation.
23+
///
24+
/// Different LSP protocols have different definitions of "engagement":
25+
/// - **LSPS1**: Checks for active channel order requests
26+
/// - **LSPS2**: Checks for pending channel open requests
27+
/// - **LSPS5**: Checks for existing open channels with the client
28+
fn is_engaged(&self, counterparty_node_id: &PublicKey) -> bool;
29+
}
30+
31+
#[cfg(lsps1_service)]
32+
impl<ES: Deref, CM: Deref + Clone, C: Deref> DosProtectionEnforcer
33+
for LSPS1ServiceHandler<ES, CM, C>
34+
where
35+
ES::Target: EntropySource,
36+
CM::Target: AChannelManager,
37+
C::Target: Filter,
38+
{
39+
fn is_engaged(&self, counterparty_node_id: &PublicKey) -> bool {
40+
self.has_active_requests(counterparty_node_id)
41+
}
42+
}
43+
44+
impl<CM: Deref> DosProtectionEnforcer for LSPS2ServiceHandler<CM>
45+
where
46+
CM::Target: AChannelManager,
47+
{
48+
fn is_engaged(&self, counterparty_node_id: &PublicKey) -> bool {
49+
self.has_pending_channel_open_request(counterparty_node_id)
50+
}
51+
}
52+
53+
impl<CM: Deref, NS: Deref, TP: Deref> DosProtectionEnforcer for LSPS5ServiceHandler<CM, NS, TP>
54+
where
55+
CM::Target: AChannelManager,
56+
TP::Target: TimeProvider,
57+
NS::Target: NodeSigner,
58+
{
59+
fn is_engaged(&self, counterparty_node_id: &PublicKey) -> bool {
60+
self.client_has_open_channel(counterparty_node_id)
61+
}
62+
}

lightning-liquidity/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ mod prelude {
5858
pub(crate) use lightning::util::hash_tables::*;
5959
}
6060

61+
pub mod dos_protection_enforcer;
6162
pub mod events;
6263
pub mod lsps0;
6364
pub mod lsps1;

lightning-liquidity/src/lsps1/service.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,17 @@ where
174174
&self.config
175175
}
176176

177+
pub(crate) fn has_active_requests(&self, counterparty_node_id: &PublicKey) -> bool {
178+
let outer_state_lock = self.per_peer_state.read().unwrap();
179+
if let Some(inner_state_lock) = outer_state_lock.get(counterparty_node_id) {
180+
let peer_state = inner_state_lock.lock().unwrap();
181+
!(peer_state.pending_requests.is_empty()
182+
&& peer_state.outbound_channels_by_order_id.is_empty())
183+
} else {
184+
false
185+
}
186+
}
187+
177188
fn handle_get_info_request(
178189
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey,
179190
) -> Result<(), LightningError> {

lightning-liquidity/src/lsps2/service.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,10 @@ impl OutboundJITChannel {
427427
matches!(self.state, OutboundJITChannelState::PendingInitialPayment { .. })
428428
}
429429

430+
fn is_pending_channel_open(&self) -> bool {
431+
matches!(self.state, OutboundJITChannelState::PendingChannelOpen { .. })
432+
}
433+
430434
fn is_prunable(&self) -> bool {
431435
// We deem an OutboundJITChannel prunable if our offer expired and we haven't intercepted
432436
// any HTLCs initiating the flow yet.
@@ -572,6 +576,21 @@ where
572576
&self.config
573577
}
574578

579+
pub(crate) fn has_pending_channel_open_request(
580+
&self, counterparty_node_id: &PublicKey,
581+
) -> bool {
582+
let outer_state_lock = self.per_peer_state.read().unwrap();
583+
if let Some(inner_state_lock) = outer_state_lock.get(counterparty_node_id) {
584+
let peer_state = inner_state_lock.lock().unwrap();
585+
peer_state
586+
.outbound_channels_by_intercept_scid
587+
.values()
588+
.any(|c| c.is_pending_channel_open())
589+
} else {
590+
false
591+
}
592+
}
593+
575594
/// Used by LSP to inform a client requesting a JIT Channel the token they used is invalid.
576595
///
577596
/// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event.

lightning-liquidity/src/lsps5/service.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ struct StoredWebhook {
6464
pub struct LSPS5ServiceConfig {
6565
/// Maximum number of webhooks allowed per client.
6666
pub max_webhooks_per_client: u32,
67+
/// Require an existing channel or active LSPS1/LSPS2 flow before accepting requests.
68+
pub enforce_dos_protections: bool,
6769
}
6870

6971
/// Default maximum number of webhooks allowed per client.
@@ -74,7 +76,10 @@ pub const DEFAULT_NOTIFICATION_COOLDOWN_HOURS: Duration = Duration::from_secs(60
7476
// Default configuration for LSPS5 service.
7577
impl Default for LSPS5ServiceConfig {
7678
fn default() -> Self {
77-
Self { max_webhooks_per_client: DEFAULT_MAX_WEBHOOKS_PER_CLIENT }
79+
Self {
80+
max_webhooks_per_client: DEFAULT_MAX_WEBHOOKS_PER_CLIENT,
81+
enforce_dos_protections: true,
82+
}
7883
}
7984
}
8085

@@ -150,6 +155,11 @@ where
150155
}
151156
}
152157

158+
/// Returns a reference to the used config.
159+
pub fn config(&self) -> &LSPS5ServiceConfig {
160+
&self.config
161+
}
162+
153163
fn check_prune_stale_webhooks(&self) {
154164
let now =
155165
LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch());
@@ -515,7 +525,7 @@ where
515525
*last_pruning = Some(now);
516526
}
517527

518-
fn client_has_open_channel(&self, client_id: &PublicKey) -> bool {
528+
pub(crate) fn client_has_open_channel(&self, client_id: &PublicKey) -> bool {
519529
self.channel_manager
520530
.get_cm()
521531
.list_channels()

lightning-liquidity/src/manager.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use alloc::string::ToString;
22
use alloc::vec::Vec;
33

4+
use crate::dos_protection_enforcer::DosProtectionEnforcer;
45
use crate::events::{EventQueue, LiquidityEvent};
56
use crate::lsps0::client::LSPS0ClientHandler;
67
use crate::lsps0::msgs::LSPS0Message;
@@ -483,6 +484,18 @@ where
483484
self.pending_events.get_and_clear_pending_events()
484485
}
485486

487+
fn peer_is_engaged(&self, peer: &PublicKey) -> bool {
488+
let lsps5_engaged =
489+
self.lsps5_service_handler.as_ref().map_or(false, |h| h.is_engaged(peer));
490+
let lsps2_engaged =
491+
self.lsps2_service_handler.as_ref().map_or(false, |h| h.is_engaged(peer));
492+
#[cfg(lsps1_service)]
493+
let lsps1_engaged = self.lsps1_service_handler.as_ref().map_or(false, |h| h.is_engaged(peer));
494+
#[cfg(not(lsps1_service))]
495+
let lsps1_engaged = false;
496+
lsps5_engaged || lsps2_engaged || lsps1_engaged
497+
}
498+
486499
fn handle_lsps_message(
487500
&self, msg: LSPSMessage, sender_node_id: &PublicKey,
488501
) -> Result<(), lightning::ln::msgs::LightningError> {
@@ -559,6 +572,17 @@ where
559572
LSPSMessage::LSPS5(msg @ LSPS5Message::Request(..)) => {
560573
match &self.lsps5_service_handler {
561574
Some(lsps5_service_handler) => {
575+
if lsps5_service_handler.config().enforce_dos_protections {
576+
if !self.peer_is_engaged(sender_node_id) {
577+
return Err(LightningError {
578+
err: format!(
579+
"Rejecting LSPS5 request from {:?} without existing engagement",
580+
sender_node_id
581+
),
582+
action: ErrorAction::IgnoreAndLog(Level::Info),
583+
});
584+
}
585+
}
562586
lsps5_service_handler.handle_message(msg, sender_node_id)?;
563587
},
564588
None => {

0 commit comments

Comments
 (0)