Skip to content

Commit be0f7e8

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 5b6b691 commit be0f7e8

File tree

7 files changed

+382
-3
lines changed

7 files changed

+382
-3
lines changed

lightning-liquidity/src/lsps1/service.rs

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

177+
/// Returns whether the peer currently has any active LSPS1 order flows.
178+
///
179+
/// An order is considered active only after we have validated the client's
180+
/// `CreateOrder` request and replied with a `CreateOrder` response containing
181+
/// an `order_id`.
182+
/// Pending requests that are still awaiting our response are deliberately NOT counted.
183+
pub(crate) fn has_active_requests(&self, counterparty_node_id: &PublicKey) -> bool {
184+
let outer_state_lock = self.per_peer_state.read().unwrap();
185+
outer_state_lock.get(counterparty_node_id).map_or(false, |inner| {
186+
let peer_state = inner.lock().unwrap();
187+
!peer_state.outbound_channels_by_order_id.is_empty()
188+
})
189+
}
190+
177191
fn handle_get_info_request(
178192
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey,
179193
) -> Result<(), LightningError> {

lightning-liquidity/src/lsps2/service.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,15 @@ where
566566
&self.config
567567
}
568568

569+
/// Returns whether the peer has any active LSPS2 requests.
570+
pub(crate) fn has_active_requests(&self, counterparty_node_id: &PublicKey) -> bool {
571+
let outer_state_lock = self.per_peer_state.read().unwrap();
572+
outer_state_lock.get(counterparty_node_id).map_or(false, |inner| {
573+
let peer_state = inner.lock().unwrap();
574+
!peer_state.outbound_channels_by_intercept_scid.is_empty()
575+
})
576+
}
577+
569578
/// Used by LSP to inform a client requesting a JIT Channel the token they used is invalid.
570579
///
571580
/// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event.

lightning-liquidity/src/lsps5/client.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ where
185185
/// Also ensure the URL is valid, has HTTPS protocol, its length does not exceed [`MAX_WEBHOOK_URL_LENGTH`]
186186
/// and that the URL points to a public host.
187187
///
188+
/// Your request may fail if you recently opened a channel or started an LSPS1 / LSPS2 flow.
189+
/// Please retry shortly.
190+
///
188191
/// [`MAX_WEBHOOK_URL_LENGTH`]: super::msgs::MAX_WEBHOOK_URL_LENGTH
189192
/// [`MAX_APP_NAME_LENGTH`]: super::msgs::MAX_APP_NAME_LENGTH
190193
/// [`WebhookRegistered`]: super::event::LSPS5ClientEvent::WebhookRegistered

lightning-liquidity/src/lsps5/msgs.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ pub const LSPS5_UNKNOWN_ERROR_CODE: i32 = 1000;
5353
pub const LSPS5_SERIALIZATION_ERROR_CODE: i32 = 1001;
5454
/// A notification was sent too frequently.
5555
pub const LSPS5_SLOW_DOWN_ERROR_CODE: i32 = 1002;
56+
/// A request was rejected because the client has no prior activity with the LSP (no open channel and no active LSPS1 or LSPS2 flow). The client should first open a channel
57+
pub const LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE: i32 = 1003;
5658

5759
pub(crate) const LSPS5_SET_WEBHOOK_METHOD_NAME: &str = "lsps5.set_webhook";
5860
pub(crate) const LSPS5_LIST_WEBHOOKS_METHOD_NAME: &str = "lsps5.list_webhooks";
@@ -113,6 +115,10 @@ pub enum LSPS5ProtocolError {
113115
///
114116
/// [`NOTIFICATION_COOLDOWN_TIME`]: super::service::NOTIFICATION_COOLDOWN_TIME
115117
SlowDownError,
118+
119+
/// Request rejected because the client has no prior activity with the LSP (no open channel and no active LSPS1 or LSPS2 flow). The client should first open a channel
120+
/// or initiate an LSPS1/LSPS2 interaction before retrying.
121+
NoPriorActivityError,
116122
}
117123

118124
impl LSPS5ProtocolError {
@@ -129,6 +135,7 @@ impl LSPS5ProtocolError {
129135
LSPS5ProtocolError::UnknownError => LSPS5_UNKNOWN_ERROR_CODE,
130136
LSPS5ProtocolError::SerializationError => LSPS5_SERIALIZATION_ERROR_CODE,
131137
LSPS5ProtocolError::SlowDownError => LSPS5_SLOW_DOWN_ERROR_CODE,
138+
LSPS5ProtocolError::NoPriorActivityError => LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE,
132139
}
133140
}
134141
/// The error message for the LSPS5 protocol error.
@@ -145,6 +152,9 @@ impl LSPS5ProtocolError {
145152
"Error serializing LSPS5 webhook notification"
146153
},
147154
LSPS5ProtocolError::SlowDownError => "Notification sent too frequently",
155+
LSPS5ProtocolError::NoPriorActivityError => {
156+
"Request rejected due to no prior activity with the LSP"
157+
},
148158
}
149159
}
150160
}
@@ -249,6 +259,9 @@ impl From<LSPSResponseError> for LSPS5ProtocolError {
249259
LSPS5_UNSUPPORTED_PROTOCOL_ERROR_CODE => LSPS5ProtocolError::UnsupportedProtocol,
250260
LSPS5_TOO_MANY_WEBHOOKS_ERROR_CODE => LSPS5ProtocolError::TooManyWebhooks,
251261
LSPS5_APP_NAME_NOT_FOUND_ERROR_CODE => LSPS5ProtocolError::AppNameNotFound,
262+
LSPS5_SERIALIZATION_ERROR_CODE => LSPS5ProtocolError::SerializationError,
263+
LSPS5_SLOW_DOWN_ERROR_CODE => LSPS5ProtocolError::SlowDownError,
264+
LSPS5_NO_PRIOR_ACTIVITY_ERROR_CODE => LSPS5ProtocolError::NoPriorActivityError,
252265
_ => LSPS5ProtocolError::UnknownError,
253266
}
254267
}
@@ -640,6 +653,12 @@ pub enum LSPS5Request {
640653
RemoveWebhook(RemoveWebhookRequest),
641654
}
642655

656+
impl LSPS5Request {
657+
pub(crate) fn is_state_allocating(&self) -> bool {
658+
matches!(self, LSPS5Request::SetWebhook(_))
659+
}
660+
}
661+
643662
/// An LSPS5 protocol response.
644663
#[derive(Clone, Debug, PartialEq, Eq)]
645664
pub enum LSPS5Response {

lightning-liquidity/src/lsps5/service.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,34 @@ where
149149
}
150150
}
151151

152+
/// Enforces the prior-activity requirement for state-allocating LSPS5 requests (e.g.
153+
/// `lsps5.set_webhook`), rejecting and replying with `NoPriorActivityError` if not met.
154+
pub(crate) fn enforce_prior_activity_or_reject(
155+
&self, client_id: &PublicKey, lsps2_has_active_requests: bool, lsps1_has_activity: bool,
156+
request_id: LSPSRequestId,
157+
) -> Result<(), LightningError> {
158+
let can_accept = self.client_has_open_channel(client_id)
159+
|| lsps2_has_active_requests
160+
|| lsps1_has_activity;
161+
162+
let mut message_queue_notifier = self.pending_messages.notifier();
163+
if !can_accept {
164+
let error = LSPS5ProtocolError::NoPriorActivityError;
165+
let msg = LSPS5Message::Response(
166+
request_id,
167+
LSPS5Response::SetWebhookError(error.clone().into()),
168+
)
169+
.into();
170+
message_queue_notifier.enqueue(&client_id, msg);
171+
return Err(LightningError {
172+
err: error.message().into(),
173+
action: ErrorAction::IgnoreAndLog(Level::Info),
174+
});
175+
} else {
176+
Ok(())
177+
}
178+
}
179+
152180
fn check_prune_stale_webhooks<'a>(
153181
&self, outer_state_lock: &mut RwLockWriteGuard<'a, HashMap<PublicKey, PeerState>>,
154182
) {

lightning-liquidity/src/manager.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,29 @@ where
568568
LSPSMessage::LSPS5(msg @ LSPS5Message::Request(..)) => {
569569
match &self.lsps5_service_handler {
570570
Some(lsps5_service_handler) => {
571+
if let LSPS5Message::Request(ref req_id, ref req) = msg {
572+
if req.is_state_allocating() {
573+
let lsps2_has_active_requests = self
574+
.lsps2_service_handler
575+
.as_ref()
576+
.map_or(false, |h| h.has_active_requests(sender_node_id));
577+
#[cfg(lsps1_service)]
578+
let lsps1_has_active_requests = self
579+
.lsps1_service_handler
580+
.as_ref()
581+
.map_or(false, |h| h.has_active_requests(sender_node_id));
582+
#[cfg(not(lsps1_service))]
583+
let lsps1_has_active_requests = false;
584+
585+
lsps5_service_handler.enforce_prior_activity_or_reject(
586+
sender_node_id,
587+
lsps2_has_active_requests,
588+
lsps1_has_active_requests,
589+
req_id.clone(),
590+
)?
591+
}
592+
}
593+
571594
lsps5_service_handler.handle_message(msg, sender_node_id)?;
572595
},
573596
None => {

0 commit comments

Comments
 (0)