lnpeer: add some rate-limiting against ping flood
What changed, and why it matters
This change adds a simple speed bump to stop a connected Lightning peer from bombarding an Electrum node with tiny 'ping' messages that force the node to send back large 'pong' replies. Before the patch, a peer could send pings as fast as the network allowed, making Electrum waste bandwidth and CPU generating replies. After the patch, Electrum waits at least one second between handling pings from the same peer, slowing any abuse to a trickle. The commit author notes this is not considered a serious issue.
Apply the patch. Monitor whether 1 second per peer is sufficient for your threat model, and consider whether inbound message flooding via odd message types also needs mitigation if you operate public or high-value Lightning nodes.
Security signals we found
Rate-limiting added to prevent ping-flood amplification
Asymmetric bandwidth attack: small inbound ping triggers large outbound pong
Per-peer state introduced to throttle message handler
Handler made async to allow non-blocking sleep within message loop
Commit message frames issue as a hardening measure, not a serious vulnerability
Evidence from the diff
In electrum/lnpeer.py, on_ping was converted from a synchronous method to an async coroutine and a per-peer _last_ping_recv_time timestamp was added. When a ping arrives, the code computes elapsed_since_last and, if it is under min_delay (1.0 s), awaits asyncio.sleep for the remainder. This blocks further incoming message processing for that peer before sending the pong with byteslen=payload[‘num_pong_bytes’]. The fix specifically targets asymmetric amplification: a peer can send a small ping (up to 65 KB) and request a large pong (up to 65 KB), costing the attacker little while consuming the victim’s outbound bandwidth and CPU. The author explicitly states this does not address inbound flooding, which can still occur via other odd message types.
Changed components
electrum/lnpeer.pyPeer.on_pingLightning peer message handling loopInspect captured patch +13 / −1
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
index 412241c..75057b4 100644
--- a/electrum/lnpeer.py
+++ b/electrum/lnpeer.py
@@ -140,6 +140,7 @@ class Peer(Logger, EventListener):
self._num_gossip_messages_forwarded = 0
self._processed_onion_cache = LRUCache(maxsize=100) # type: LRUCache[bytes, ProcessedOnionPacket]
self._last_commitsig_sent_time = time.monotonic()
+ self._last_ping_recv_time = min(0, time.monotonic())
def send_message(self, message_name: str, **kwargs):
assert util.get_running_loop() == util.get_asyncio_loop(), f"this must be run on the asyncio thread!"
@@ -372,7 +373,18 @@ class Peer(Logger, EventListener):
self.schedule_force_closing(cid)
raise GracefulDisconnect
- def on_ping(self, payload):
+ async def on_ping(self, payload):
+ elapsed_since_last = time.monotonic() - self._last_ping_recv_time
+ min_delay = 1.0 # seconds
+ if elapsed_since_last < min_delay:
+ self.logger.debug("remote sending PINGs too often, sleeping a bit")
+ # note: This rate-limiting helps limit our outbound traffic usage.
+ # (max inc msg size 65 KB, max out msg size 65 KB, decoupled)
+ # Does not really help for inbound traffic-usage:
+ # there are many other ways for the peer to flood us, e.g. unknown 'odd' message types.
+ # note: this blocks processing *any* further incoming message from this peer
+ await asyncio.sleep(min_delay - elapsed_since_last)
+ self._last_ping_recv_time = time.monotonic()
l = payload['num_pong_bytes']
self.send_message('pong', byteslen=l)
Why this scored 51/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.