Merge pull request #11132 from yyforyongyu/task-11129-pong
What changed, and why it matters
This change fixes how LND answers network 'ping' messages from other Lightning nodes. Previously, LND would sometimes silently ignore valid pings to limit reply traffic, which violates the Lightning protocol (BOLT 1) and could make a peer think the connection is dead. Now LND replies to every valid ping, while still disconnecting peers that flood too many pings. The cost of a reply is counted against a single flood budget based on how large the reply would be.
Treat as a recommended bug-fix/security-hardening update. Nodes should upgrade to ensure BOLT 1 compliance and avoid interoperability issues with peers that rely on timely Pong replies. Monitor for any unexpected disconnects from peers sending large ping bursts.
Security signals we found
Protocol compliance fix: BOLT 1 requires a Pong for every valid Ping
Removed silent suppression of Pong replies that could cause peer timeout/misbehavior
Unified rate limiting now accounts for amplification bandwidth, not just ping count
Oversized pings still consume flood budget and can trigger disconnect
Release notes explicitly describe the change as fixing silent suppression of valid Pong replies
Evidence from the diff
The patch removes a separate ‘pongLimiter’ that suppressed Pong replies and merges ping handling into one ‘pingLimiter’ using golang.org/x/time/rate. Valid pings below lnwire.MaxPongBytes now always produce a Pong via queueMsg. The limiter cost is scaled by requested Pong bytes (pingResponseBytesPerToken=6554 bytes per token, max 10 tokens for a 65535-byte reply), while oversized no-reply pings still consume one token. Flood exhaustion still triggers disconnect via errPingFlood. Tests were updated to verify every admitted ping gets a pong, priority queue classification, and that maximum-size reply bursts disconnect at the expected boundary.
Changed components
peer/brontide.gopeer/ping_limits.gopeer/brontide_test.goInspect captured patch +274 / −171
### docs/release-notes/release-notes-0.20.5.md
@@ -33,6 +33,11 @@
keeping the invoice open so that other accepted sets on reusable static
AMP invoices remain payable.
+* Peers now [answer every valid inbound
+ Ping](https://github.com/lightningnetwork/lnd/pull/11132) as required by
+ BOLT 1. The existing request flood limit remains the connection teardown
+ boundary instead of silently suppressing otherwise valid Pong replies.
+
# New Features
## Functional Enhancements
@@ -81,3 +86,4 @@
* elsirion
* Gijs van Dam
+* Yong Yu
### docs/release-notes/release-notes-0.21.4.md
@@ -38,6 +38,11 @@
keeping the invoice open so that other accepted sets on reusable static
AMP invoices remain payable.
+* Peers now [answer every valid inbound
+ Ping](https://github.com/lightningnetwork/lnd/pull/11132) as required by
+ BOLT 1. The existing request flood limit remains the connection teardown
+ boundary instead of silently suppressing otherwise valid Pong replies.
+
# New Features
## Functional Enhancements
@@ -99,4 +104,5 @@
* elsirion
* Gijs van Dam
* Olaoluwa Osuntokun
+* Yong Yu
* Ziggie
### peer/brontide.go
@@ -58,6 +58,7 @@ import (
"github.com/lightningnetwork/lnd/ticker"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/watchtower/wtclient"
+ "golang.org/x/time/rate"
)
const (
@@ -598,8 +599,9 @@ type Brontide struct {
pingManager *PingManager
- // pingLimits owns the two per-connection inbound Ping policies.
- pingLimits pingLimits
+ // pingLimiter bounds inbound Ping work before message routing. Keeping
+ // the limiter on the peer gives each connection an independent budget.
+ pingLimiter *rate.Limiter
// queueLimits supplies one accounting policy to the producer and queue.
queueLimits queueLimits
@@ -765,7 +767,7 @@ func NewBrontide(cfg Config) *Brontide {
activeSignal: make(chan struct{}),
sendQueue: make(chan outgoingMsg),
outgoingQueue: make(chan outgoingMsg),
- pingLimits: defaultPingLimits(),
+ pingLimiter: defaultPingLimiter(),
queueLimits: defaultQueueLimits(),
addedChannels: &lnutils.SyncMap[lnwire.ChannelID, struct{}]{},
activeChannels: &lnutils.SyncMap[
@@ -2337,10 +2339,13 @@ out:
}
}
- // Count before routing; consuming endpoints skip the switch.
- // All Pings, including oversized ones, use the flood budget.
- if _, ok := nextMsg.(*lnwire.Ping); ok &&
- !p.pingLimits.pingLimiter.Allow() {
+ // Count before routing because endpoints may consume Pings.
+ // Valid requests pay proportionally for their Pong bytes, while
+ // oversized no-reply requests still consume one flood token.
+ ping, ok := nextMsg.(*lnwire.Ping)
+ if ok && !p.pingLimiter.AllowN(
+ time.Now(), calcPingCost(ping),
+ ) {
p.storeError(errPingFlood)
p.log.Warnf("%v", errPingFlood)
@@ -2392,16 +2397,9 @@ out:
continue
}
- // BOLT 1 requires a Pong for every Ping below the size
- // ceiling. We limit reply frequency to guard against
- // floods; normal keepalives remain below this limit.
- if !p.pingLimits.pongLimiter.Allow() {
- p.log.Debugf("Pong reply rate limited")
- continue
- }
-
- // Next, we'll send over the amount of specified pong
- // bytes.
+ // BOLT 1 requires a Pong of the requested size for
+ // every Ping below the size ceiling. The request flood
+ // limiter above disconnects abusive peers first.
pong := lnwire.NewPong(p.cfg.PongBuf[0:msg.NumPongBytes])
p.queueMsg(pong, nil)
### peer/brontide_test.go
@@ -1291,72 +1291,43 @@ func TestPeerIgnoresPingWithoutPongReply(t *testing.T) {
}
// TestPeerPingLimitsProductionBoundaries verifies the exact burst and refill
-// thresholds used by both production Ping policies.
+// thresholds used by the production Ping flood policy.
func TestPeerPingLimitsProductionBoundaries(t *testing.T) {
t.Parallel()
- // Arrange: Use fresh production limiters and expected values
- // so each subtest starts with a full, independent token bucket.
- limits := defaultPingLimits()
- tests := []struct {
- name string
- limiter *rate.Limiter
- limit rate.Limit
- burst int
- }{
- {
- name: "Pong replies",
- limiter: limits.pongLimiter,
- limit: pongReplyRate,
- burst: pongReplyBurst,
- },
- {
- name: "Ping floods",
- limiter: limits.pingLimiter,
- limit: pingFloodRate,
- burst: pingFloodBurst,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- // Arrange: Derive the one-token interval from the rate
- // constant under test, then fix a synthetic timestamp.
- // This makes both sides of the boundary deterministic.
- // Two nanoseconds keep the pre-boundary deficit above
- // rate's duration-truncation quantum.
- now := time.Now()
- refillTime := time.Duration(
- float64(time.Second) / float64(test.limit),
- )
- const boundaryEpsilon = 2 * time.Nanosecond
- require.Equal(t, test.limit, test.limiter.Limit())
- require.Equal(t, test.burst, test.limiter.Burst())
-
- // Act: Consume the burst, probe one token past it, and
- // test just before and at the derived replacement time.
- atBoundary := test.limiter.AllowN(now, test.burst)
- pastBoundary := test.limiter.AllowN(now, 1)
- beforeRefill := test.limiter.AllowN(
- now.Add(refillTime-boundaryEpsilon), 1,
- )
- atRefill := test.limiter.AllowN(
- now.Add(refillTime), 1,
- )
-
- // Assert: The burst boundary is inclusive, both probes
- // before refill are rejected, and the derived boundary
- // restores exactly one token without scheduler timing.
- require.True(t, atBoundary)
- require.False(t, pastBoundary)
- require.False(t, beforeRefill)
- require.True(t, atRefill)
- })
- }
+ // Arrange: Use a fresh production limiter and derive its one-token
+ // interval from the configured rate. A synthetic timestamp and a
+ // two-nanosecond epsilon make both sides of the boundary deterministic
+ // despite rate's duration truncation.
+ limiter := defaultPingLimiter()
+ now := time.Now()
+ refillTime := time.Duration(
+ float64(time.Second) / float64(pingFloodRate),
+ )
+ const boundaryEpsilon = 2 * time.Nanosecond
+ require.Equal(t, pingFloodRate, limiter.Limit())
+ require.Equal(t, pingFloodBurst, limiter.Burst())
+
+ // Act: Consume the entire production burst, probe one token past it,
+ // then probe immediately before and exactly at the derived refill time.
+ atBoundary := limiter.AllowN(now, pingFloodBurst)
+ pastBoundary := limiter.AllowN(now, 1)
+ beforeRefill := limiter.AllowN(
+ now.Add(refillTime-boundaryEpsilon), 1,
+ )
+ atRefill := limiter.AllowN(now.Add(refillTime), 1)
+
+ // Assert: The configured burst is inclusive, requests remain rejected
+ // until the full refill interval passes, and exactly one token becomes
+ // available at that boundary.
+ require.True(t, atBoundary)
+ require.False(t, pastBoundary)
+ require.False(t, beforeRefill)
+ require.True(t, atRefill)
}
-// TestPeerPingLimitsAllowHonestCadence verifies that both inbound Ping
-// limiters admit realistic keepalive cadences for long-lived connections.
+// TestPeerPingLimitsAllowHonestCadence verifies that the inbound Ping flood
+// limiter admits realistic keepalive cadences for long-lived connections.
func TestPeerPingLimitsAllowHonestCadence(t *testing.T) {
t.Parallel()
@@ -1374,7 +1345,7 @@ func TestPeerPingLimitsAllowHonestCadence(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
// Arrange: Construct the production Ping policy
// separately so token history cannot cross test cases.
- limits := defaultPingLimits()
+ limiter := defaultPingLimiter()
start := time.Now()
// Act: Advance a synthetic clock at the selected
@@ -1383,77 +1354,122 @@ func TestPeerPingLimitsAllowHonestCadence(t *testing.T) {
elapsed := time.Duration(i) * test.cadence
now := start.Add(elapsed)
- // Assert: Both budgets admit each ping, so this
- // cadence reaches neither protection tier.
- require.True(
- t, limits.pongLimiter.AllowN(now, 1),
- )
+ // Assert: The flood budget admits every Ping at
+ // this cadence, keeping honest peers connected.
require.True(
- t, limits.pingLimiter.AllowN(now, 1),
+ t, limiter.AllowN(now, 1),
)
}
})
}
}
-// TestPeerPongReplyRateLimited verifies that exhausting the reply budget
-// suppresses Pongs without disconnecting the peer.
-func TestPeerPongReplyRateLimited(t *testing.T) {
+// TestPeerValidPingsReceivePongs verifies that every Ping admitted by the
+// flood limiter receives the Pong response mandated by BOLT 1.
+func TestPeerValidPingsReceivePongs(t *testing.T) {
t.Parallel()
- // Arrange: Start a peer whose reply limiter has one token, so the
- // first valid ping replies and the second exhausts the budget.
+ // Arrange: Start a peer with the production flood policy and prepare to
+ // retain one response for every token in its initial burst. Exercising
+ // the full burst crosses the former reply-only limit while remaining
+ // within the request limit that keeps the connection active.
params := createTestPeer(t)
peer := params.peer
- peer.pingLimits.pongLimiter = rate.NewLimiter(0, 1)
-
startDone := startPeer(t, params.mockConn, peer)
_, err := fn.RecvOrTimeout(startDone, 2*timeout)
require.NoError(t, err)
+ responses := make([][]byte, 0, pingFloodBurst)
- // writePing serializes a valid one-byte-reply ping with an observable
- // payload and injects it through the mock connection's normal reader.
- // Distinct payloads synchronize the assertion with each exact Ping.
- writePing := func(payload []byte) {
+ // Act: Deliver exactly the admitted burst of valid Pings through the
+ // normal read path. Drain one wire response after each request to avoid
+ // mock backpressure while observing the protocol behavior.
+ for i := 0; i < pingFloodBurst; i++ {
var b bytes.Buffer
ping := lnwire.NewPing(1)
- ping.PaddingBytes = payload
+ ping.PaddingBytes = []byte{byte(i)}
_, err := lnwire.WriteMessage(&b, ping, 0)
require.NoError(t, err)
+
select {
case params.mockConn.readMessages <- b.Bytes():
case <-peer.cg.Done():
t.Fatal("peer disconnected before Ping was delivered")
}
+
+ response, err := fn.RecvOrTimeout(
+ params.mockConn.writtenMessages, timeout,
+ )
+ require.NoError(t, err)
+ responses = append(responses, response)
}
- // Act: Deliver two unique Pings and consume the first Pong. Then inject
- // the Ping that exhausts the reply budget.
- firstPayload := []byte{1}
- secondPayload := []byte{2}
- writePing(firstPayload)
- _, err = fn.RecvOrTimeout(params.mockConn.writtenMessages, timeout)
- require.NoError(t, err)
+ // Assert: Every admitted request produced a one-byte Pong and the peer
+ // remained connected at the flood boundary. The response count and wire
+ // decoding prevent silent suppression from regressing.
+ require.Len(t, responses, pingFloodBurst)
+ for _, response := range responses {
+ msg, err := lnwire.ReadMessage(bytes.NewReader(response), 0)
+ require.NoError(t, err)
- writePing(secondPayload)
+ pong, ok := msg.(*lnwire.Pong)
+ require.True(t, ok)
+ require.Len(t, pong.PongBytes, 1)
+ }
+ require.Zero(t, atomic.LoadInt32(&peer.disconnect))
+}
- // Assert: Observe the second payload before checking the write channel.
- // This proves the read loop processed the rate-limited Ping.
- require.Eventually(t, func() bool {
- return bytes.Equal(
- peer.LastRemotePingPayload(), secondPayload,
- )
- }, timeout, 10*time.Millisecond)
+// TestPeerPongReplyUsesPriorityQueue verifies that the read path classifies a
+// generated Pong as high priority before the generic queue handler sees it.
+func TestPeerPongReplyUsesPriorityQueue(t *testing.T) {
+ t.Parallel()
+
+ // Arrange: Isolate readHandler with a buffered outgoing boundary. This
+ // preserves the response before queueHandler can consume it.
+ // Empty remote features avoid unrelated gossip initialization. The mock
+ // router rejects one message, letting the normal Ping switch handle it
+ // without starting the generic router's independent event loop.
+ params := createTestPeer(t)
+ peer := params.peer
+ peer.remoteFeatures = lnwire.EmptyFeatureVector()
+ peer.outgoingQueue = make(chan outgoingMsg, 1)
+ router := &mockMsgRouter{}
+ router.On("RouteMsg", mock.Anything).Return(
+ msgmux.ErrUnableToRouteMsg,
+ ).Once()
+ peer.msgRouter = fn.Some[msgmux.Router](router)
+ peer.globalMsgRouter = true
+
+ const requestedPongBytes = 3
+ var pingBytes bytes.Buffer
+ _, err := lnwire.WriteMessage(
+ &pingBytes, lnwire.NewPing(requestedPongBytes), 0,
+ )
+ require.NoError(t, err)
+
+ peer.cg.WgAdd(1)
+ go peer.readHandler()
+ // Act: Deliver the valid Ping through wire decoding, then capture the
+ // envelope created by queueMsg. Closing the mock input after capture
+ // gives the focused reader a deterministic shutdown path.
select {
- case msg := <-params.mockConn.writtenMessages:
- t.Fatalf("unexpected Pong after reply budget: %x", msg)
- case <-time.After(shortTimeout):
+ case params.mockConn.readMessages <- pingBytes.Bytes():
+ case <-peer.cg.Done():
+ t.Fatal("peer disconnected before Ping was delivered")
}
- // Assert: The peer remains connected, proving reply exhaustion only
- // suppresses amplification and does not trigger flood teardown.
- require.Zero(t, atomic.LoadInt32(&peer.disconnect))
+ queuedMsg, err := fn.RecvOrTimeout(peer.outgoingQueue, timeout)
+ require.NoError(t, err)
+ close(params.mockConn.readMessages)
+ peer.cg.WgWait()
+
+ // Assert: Verify the requested Pong and queueMsg's priority marker. The
+ // marker proves the response cannot enter the lazy-message class.
+ require.True(t, queuedMsg.priority)
+ pong, ok := queuedMsg.msg.(*lnwire.Pong)
+ require.True(t, ok)
+ require.Len(t, pong.PongBytes, requestedPongBytes)
+ router.AssertExpectations(t)
}
// mockMsgRouter records message-router calls while letting a test choose
@@ -1503,29 +1519,29 @@ func (m *mockMsgRouter) Stop() {
// production router interface used by Brontide.
var _ msgmux.Router = (*mockMsgRouter)(nil)
-// TestPeerPingFloodDisconnects verifies flood accounting precedes a generic
-// router that would consume an oversized Ping.
+// TestPeerPingFloodDisconnects verifies one-token oversized Ping accounting
+// precedes a generic router that would otherwise consume the message.
func TestPeerPingFloodDisconnects(t *testing.T) {
t.Parallel()
- // Arrange: Empty the flood budget and retain errors through an active
- // channel. Install a mock router prepared to consume any message;
- // marking it global avoids unrelated lifecycle calls.
+ // Arrange: Give the peer exactly one non-refilling token and retain
+ // errors through an active channel. The mock router consumes the first
+ // oversized Ping, proving that only the second reaches flood teardown.
params := createTestPeer(t)
peer := params.peer
- peer.pingLimits.pingLimiter = rate.NewLimiter(0, 0)
+ peer.pingLimiter = rate.NewLimiter(0, 1)
peer.remoteFeatures = lnwire.EmptyFeatureVector()
peer.activeChannels.Store(
lnwire.ChannelID{1}, &lnwallet.LightningChannel{},
)
router := &mockMsgRouter{}
- router.On("RouteMsg", mock.Anything).Return(nil).Maybe()
+ router.On("RouteMsg", mock.Anything).Return(nil).Once()
peer.msgRouter = fn.Some[msgmux.Router](router)
peer.globalMsgRouter = true
- // Arrange: Encode the first oversized Pong request and register the
- // focused reader with the control group so shutdown remains joinable.
+ // Arrange: Encode one BOLT 1 no-reply request, then register
+ // the read loop with the control group for joinable teardown.
var b bytes.Buffer
_, err := lnwire.WriteMessage(&b, &lnwire.Ping{
NumPongBytes: lnwire.MaxPongBytes + 1,
@@ -1535,23 +1551,107 @@ func TestPeerPingFloodDisconnects(t *testing.T) {
peer.cg.WgAdd(1)
go peer.readHandler()
- // Act: Send the oversized Ping through normal decoding, then wait for
- // the empty flood budget to cancel and fully stop the focused reader.
+ // Act: Send two oversized Pings. The first spends the sole token and
+ // reaches the router; the second finds no budget and disconnects before
+ // routing.
+ for i := 0; i < 2; i++ {
+ select {
+ case params.mockConn.readMessages <- b.Bytes():
+ case <-peer.cg.Done():
+ t.Fatal("peer disconnected before both Pings arrived")
+ }
+ }
+
+ _, err = fn.RecvOrTimeout(peer.cg.Done(), timeout)
+ require.NoError(t, err)
+ peer.cg.WgWait()
+
+ // Assert: One oversized Ping reached routing before teardown blocked
+ // the second from doing so, and the retained error matches the stable
+ // sentinel without depending on its display text.
+ require.EqualValues(t, 1, atomic.LoadInt32(&peer.disconnect))
+ router.AssertExpectations(t)
+
+ storedErrors := peer.ErrorBuffer().List()
+ require.NotEmpty(t, storedErrors)
+ storedErr, ok := storedErrors[0].(*TimestampedError)
+ require.True(t, ok)
+ require.ErrorIs(t, storedErr.Error, errPingFlood)
+}
+
+// TestPeerMaxPongBurstDisconnects verifies maximum-size replies retain the
+// former outbound burst bound without silently suppressing an admitted Pong.
+func TestPeerMaxPongBurstDisconnects(t *testing.T) {
+ t.Parallel()
+
+ // Arrange: Isolate the reader with a non-refilling production 200-token
+ // burst so wall time cannot move the boundary. An active-channel marker
+ // retains errors; the mock router rejects Pings into normal handling.
+ params := createTestPeer(t)
+ peer := params.peer
+ peer.pingLimiter = rate.NewLimiter(0, pingFloodBurst)
+ peer.remoteFeatures = lnwire.EmptyFeatureVector()
+ peer.outgoingQueue = make(chan outgoingMsg, 1)
+ peer.activeChannels.Store(
+ lnwire.ChannelID{1}, &lnwallet.LightningChannel{},
+ )
+
+ const admittedMaxPongs = 20
+ router := &mockMsgRouter{}
+ router.On("RouteMsg", mock.Anything).Return(
+ msgmux.ErrUnableToRouteMsg,
+ ).Times(admittedMaxPongs)
+ peer.msgRouter = fn.Some[msgmux.Router](router)
+ peer.globalMsgRouter = true
+
+ var b bytes.Buffer
+ _, err := lnwire.WriteMessage(
+ &b, lnwire.NewPing(lnwire.MaxPongBytes), 0,
+ )
+ require.NoError(t, err)
+
+ peer.cg.WgAdd(1)
+ go peer.readHandler()
+ responses := make([]outgoingMsg, 0, admittedMaxPongs)
+
+ // Act: Deliver and drain 20 maximum-size requests so the queue cannot
+ // back up, then send the 21st and wait for its insufficient weighted
+ // budget to disconnect.
+ for i := 0; i < admittedMaxPongs; i++ {
+ select {
+ case params.mockConn.readMessages <- b.Bytes():
+ case <-peer.cg.Done():
+ t.Fatal("peer disconnected before admitted Ping")
+ }
+
+ response, err := fn.RecvOrTimeout(
+ peer.outgoingQueue, timeout,
+ )
+ require.NoError(t, err)
+ responses = append(responses, response)
+ }
+
select {
case params.mockConn.readMessages <- b.Bytes():
case <-peer.cg.Done():
- t.Fatal("peer disconnected before Ping was delivered")
+ t.Fatal("peer disconnected before excess Ping was delivered")
}
_, err = fn.RecvOrTimeout(peer.cg.Done(), timeout)
require.NoError(t, err)
peer.cg.WgWait()
- // Assert: Teardown precedes generic routing, and the retained error
- // matches the stable sentinel without depending on its display text.
- require.EqualValues(t, 1, atomic.LoadInt32(&peer.disconnect))
- router.AssertNotCalled(t, "RouteMsg", mock.Anything)
+ // Assert: Each admitted request produced a priority Pong; the first
+ // excess request disconnected with errPingFlood rather than silence.
+ for _, response := range responses {
+ require.True(t, response.priority)
+ pong, ok := response.msg.(*lnwire.Pong)
+ require.True(t, ok)
+ require.Len(t, pong.PongBytes, int(lnwire.MaxPongBytes))
+ }
+ require.EqualValues(t, 1, atomic.LoadInt32(&peer.disconnect))
+ router.AssertExpectations(t)
storedErrors := peer.ErrorBuffer().List()
require.NotEmpty(t, storedErrors)
storedErr, ok := storedErrors[0].(*TimestampedError)
@@ -2390,7 +2490,6 @@ func TestHandleNewPendingChannel(t *testing.T) {
}
for _, tc := range testCases {
-
// Create a request for testing.
errChan := make(chan error, 1)
req := &newChannelMsg{
### peer/ping_limits.go
@@ -1,53 +1,47 @@
package peer
-import "golang.org/x/time/rate"
+import (
+ "github.com/lightningnetwork/lnd/lnwire"
+ "golang.org/x/time/rate"
+)
const (
- // pongReplyRate refills the reply budget quickly enough for normal
- // keepalives while bounding sustained amplification from remote Pings.
- pongReplyRate rate.Limit = 1
-
- // pongReplyBurst absorbs short keepalive bursts without suppressing a
- // reply before the sustained-rate policy has time to take effect.
- pongReplyBurst = 20
-
// pingFloodRate admits substantially more inbound Pings than an honest
// keepalive cadence while placing a finite bound on sustained floods.
pingFloodRate rate.Limit = 10
// pingFloodBurst tolerates transient bursts before the peer is treated
// as a flood source and disconnected by the read loop.
pingFloodBurst = 200
+
+ // pingResponseBytesPerToken scales admission by the requested
+ // Pong size. The quantum maps the largest valid response to ten tokens,
+ // preserving the former worst-case reply bandwidth without penalizing
+ // lnd's normal requests of at most 4,096 bytes.
+ pingResponseBytesPerToken = 6554
)
-// pingLimits holds the stateful limiters for the two inbound Ping policies.
-// Keeping them together makes their different outcomes explicit without
-// exposing fixed denial-of-service thresholds as operator configuration.
-type pingLimits struct {
- // pongLimiter controls whether a valid Ping receives a Pong. Exhausting
- // this limiter suppresses the reply but leaves the connection active.
- pongLimiter *rate.Limiter
-
- // pingLimiter counts every inbound Ping. Exhausting this limiter
- // disconnects the peer, including for Pings that request no reply.
- pingLimiter *rate.Limiter
+// defaultPingLimiter constructs independent flood state for a new peer. The
+// fixed rate leaves ample room above normal keepalive traffic while bounding
+// sustained request floods without exposing a redundant policy wrapper.
+func defaultPingLimiter() *rate.Limiter {
+ // Refill ten tokens per second and allow a burst of 200 before treating
+ // the connection as a flood source.
+ return rate.NewLimiter(pingFloodRate, pingFloodBurst)
}
-// defaultPingLimits constructs independent limiter state for a new peer. The
-// selected rates leave ample room above normal keepalive traffic while
-// separating reply suppression from flood teardown.
-func defaultPingLimits() pingLimits {
- return pingLimits{
- // Refill one Pong per second and absorb a 20-Ping burst,
- // leaving wide headroom above honest keepalives.
- pongLimiter: rate.NewLimiter(
- pongReplyRate, pongReplyBurst,
- ),
-
- // Permit ten Pings per second and a burst of 200 before
- // treating the connection as a flood source.
- pingLimiter: rate.NewLimiter(
- pingFloodRate, pingFloodBurst,
- ),
+// calcPingCost scales admitted Ping work by requested Pong size so one limiter
+// bounds both request rate and response bandwidth. Oversized requests use one
+// token because BOLT 1 requires no reply, but still consume flood capacity.
+func calcPingCost(ping *lnwire.Ping) int {
+ if ping.NumPongBytes > lnwire.MaxPongBytes {
+ return 1
}
+
+ requestedBytes := int(ping.NumPongBytes)
+
+ return max(
+ 1, (requestedBytes+pingResponseBytesPerToken-1)/
+ pingResponseBytesPerToken,
+ )
}Why this scored 53/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.