Skip to content

Commit fb1a7e6

Browse files
authored
Merge pull request #10674 from yyforyongyu/bugfix/10671-bolt1-ping-ignore
lnwire+peer: ignore BOLT 1 no-reply pings
2 parents 61f4492 + 59f50fb commit fb1a7e6

7 files changed

Lines changed: 159 additions & 11 deletions

File tree

docs/release-notes/release-notes-0.21.0.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
transitions during startup, avoiding lost unlocks during slow database
5757
initialization.
5858

59+
* [Fixed handling of BOLT 1 pings requesting 65532 or more pong
60+
bytes](https://github.com/lightningnetwork/lnd/pull/10674). LND now ignores
61+
these valid no-reply pings instead of disconnecting peers, restoring
62+
compatibility with implementations that pad `channel_reestablish` messages
63+
with them.
64+
5965
# New Features
6066

6167
- [Basic Support](https://github.com/lightningnetwork/lnd/pull/9868) for onion

lnwire/ping.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,8 @@ func (p *Ping) Decode(r io.Reader, pver uint32) error {
4747
return err
4848
}
4949

50-
if p.NumPongBytes > MaxPongBytes {
51-
return ErrMaxPongBytesExceeded
52-
}
53-
50+
// Values above MaxPongBytes are still valid on the wire. Per BOLT 1,
51+
// receivers must ignore those pings rather than fail deserialization.
5452
return nil
5553
}
5654

lnwire/ping_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package lnwire
2+
3+
import (
4+
"bytes"
5+
"strconv"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestPingDecodeAllowsNoReplyPongSizes asserts that ping messages using the
12+
// BOLT 1 no-reply sentinel range still deserialize successfully.
13+
func TestPingDecodeAllowsNoReplyPongSizes(t *testing.T) {
14+
t.Parallel()
15+
16+
// Arrange: Pick values from the BOLT 1 no-reply range. These
17+
// pings are valid on the wire and should decode successfully
18+
// even though they do not require a pong response.
19+
testCases := []uint16{65532, 65535}
20+
21+
for _, numPongBytes := range testCases {
22+
numPongBytes := numPongBytes
23+
testName := strconv.FormatUint(uint64(numPongBytes), 10)
24+
25+
t.Run(testName, func(t *testing.T) {
26+
// Arrange: Encode a ping carrying a no-reply pong
27+
// size together with a small payload so we exercise
28+
// the normal wire format.
29+
var buf bytes.Buffer
30+
31+
want := &Ping{
32+
NumPongBytes: numPongBytes,
33+
PaddingBytes: PingPayload{1, 2, 3},
34+
}
35+
36+
_, err := WriteMessage(&buf, want, 0)
37+
require.NoError(t, err)
38+
39+
// Act: Decode the serialized message through the
40+
// standard parser.
41+
msg, err := ReadMessage(bytes.NewReader(buf.Bytes()), 0)
42+
require.NoError(t, err)
43+
44+
// Assert: The decoded ping matches the original
45+
// input exactly.
46+
got, ok := msg.(*Ping)
47+
require.True(t, ok)
48+
require.Equal(t, want, got)
49+
})
50+
}
51+
}

lnwire/pong.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package lnwire
22

33
import (
44
"bytes"
5-
"fmt"
65
"io"
76
)
87

@@ -11,10 +10,6 @@ import (
1110
// 2 bytes, leaving 65531 bytes.
1211
const MaxPongBytes = 65531
1312

14-
// ErrMaxPongBytesExceeded indicates that the NumPongBytes field from the ping
15-
// message has exceeded MaxPongBytes.
16-
var ErrMaxPongBytesExceeded = fmt.Errorf("pong bytes exceeded")
17-
1813
// PongPayload is a set of opaque bytes sent in response to a ping message.
1914
type PongPayload []byte
2015

lnwire/test_message.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1541,7 +1541,7 @@ var _ TestMessage = (*Ping)(nil)
15411541
//
15421542
// This is part of the TestMessage interface.
15431543
func (p *Ping) RandTestMessage(t *rapid.T) Message {
1544-
numPongBytes := uint16(rapid.IntRange(0, int(MaxPongBytes)).Draw(
1544+
numPongBytes := uint16(rapid.IntRange(0, math.MaxUint16).Draw(
15451545
t, "numPongBytes"),
15461546
)
15471547

peer/brontide.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2220,6 +2220,13 @@ out:
22202220
// the relevant atomic variable.
22212221
p.lastPingPayload.Store(msg.PaddingBytes[:])
22222222

2223+
// BOLT 1 requires us to ignore pings requesting 65532
2224+
// or more pong bytes instead of replying or
2225+
// disconnecting.
2226+
if msg.NumPongBytes > lnwire.MaxPongBytes {
2227+
continue
2228+
}
2229+
22232230
// Next, we'll send over the amount of specified pong
22242231
// bytes.
22252232
pong := lnwire.NewPong(p.cfg.PongBuf[0:msg.NumPongBytes])
@@ -2591,7 +2598,8 @@ func messageSummary(msg lnwire.Message) string {
25912598
msg.NodeID, time.Unix(int64(msg.Timestamp), 0))
25922599

25932600
case *lnwire.Ping:
2594-
return fmt.Sprintf("ping_bytes=%x", msg.PaddingBytes[:])
2601+
return fmt.Sprintf("num_pong_bytes=%d, len(ping_bytes)=%d",
2602+
msg.NumPongBytes, len(msg.PaddingBytes[:]))
25952603

25962604
case *lnwire.Pong:
25972605
return fmt.Sprintf("len(pong_bytes)=%d", len(msg.PongBytes[:]))

peer/brontide_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,96 @@ func TestPeerCustomMessage(t *testing.T) {
10731073
require.Equal(t, receivedCustomMsg, &receivedCustom.msg)
10741074
}
10751075

1076+
// TestPeerIgnoresPingWithoutPongReply ensures we keep the connection alive for
1077+
// pings using the BOLT 1 no-reply sentinel range.
1078+
func TestPeerIgnoresPingWithoutPongReply(t *testing.T) {
1079+
t.Parallel()
1080+
1081+
// Arrange: Start a peer using the mock connection so we can
1082+
// inject incoming pings and observe any outgoing responses.
1083+
params := createTestPeer(t)
1084+
1085+
var (
1086+
mockConn = params.mockConn
1087+
alicePeer = params.peer
1088+
)
1089+
1090+
startPeerDone := startPeer(t, mockConn, alicePeer)
1091+
_, err := fn.RecvOrTimeout(startPeerDone, 2*timeout)
1092+
require.NoError(t, err)
1093+
1094+
writePing := func(msg *lnwire.Ping) {
1095+
t.Helper()
1096+
1097+
var b bytes.Buffer
1098+
_, err := lnwire.WriteMessage(&b, msg, 0)
1099+
require.NoError(t, err)
1100+
1101+
select {
1102+
case mockConn.readMessages <- b.Bytes():
1103+
case <-time.After(timeout):
1104+
t.Fatal("timeout sending ping to peer")
1105+
}
1106+
}
1107+
1108+
// Act: Deliver a ping in the BOLT 1 no-reply range.
1109+
ignoredPayload := []byte{1, 2, 3}
1110+
writePing(&lnwire.Ping{
1111+
NumPongBytes: 65535,
1112+
PaddingBytes: ignoredPayload,
1113+
})
1114+
1115+
// Assert: The peer records the latest ping payload for observability.
1116+
require.Eventually(t, func() bool {
1117+
return bytes.Equal(
1118+
alicePeer.LastRemotePingPayload(), ignoredPayload,
1119+
)
1120+
}, timeout, 10*time.Millisecond)
1121+
1122+
// Assert: No pong is sent for the no-reply sentinel range.
1123+
select {
1124+
case rawMsg := <-mockConn.writtenMessages:
1125+
t.Fatalf("expected no pong reply, got %x", rawMsg)
1126+
case <-time.After(100 * time.Millisecond):
1127+
}
1128+
1129+
// Act: Send a normal ping afterward to prove the peer
1130+
// stayed connected and still handles standard ping/pong
1131+
// traffic.
1132+
writePing(&lnwire.Ping{NumPongBytes: 1})
1133+
1134+
rawMsg, err := fn.RecvOrTimeout(mockConn.writtenMessages, timeout)
1135+
require.NoError(t, err)
1136+
1137+
msg, err := lnwire.ReadMessage(bytes.NewReader(rawMsg), 0)
1138+
require.NoError(t, err)
1139+
1140+
// Assert: The follow-up ping receives the requested pong reply.
1141+
pong, ok := msg.(*lnwire.Pong)
1142+
require.True(t, ok)
1143+
require.Len(t, pong.PongBytes, 1)
1144+
}
1145+
1146+
// TestMessageSummaryPingIncludesNumPongBytes ensures the debug summary for a
1147+
// ping exposes the requested pong size, which makes ignored no-reply pings
1148+
// visible without requiring trace-level logging.
1149+
func TestMessageSummaryPingIncludesNumPongBytes(t *testing.T) {
1150+
t.Parallel()
1151+
1152+
// Arrange: Build a ping that uses the BOLT 1 no-reply sentinel range.
1153+
msg := &lnwire.Ping{
1154+
NumPongBytes: 65535,
1155+
PaddingBytes: []byte{1, 2, 3},
1156+
}
1157+
1158+
// Act: Generate the human-readable message summary.
1159+
summary := messageSummary(msg)
1160+
1161+
// Assert: The summary includes both the requested pong size and payload
1162+
// length so debug logs can explain why no pong was sent.
1163+
require.Equal(t, "num_pong_bytes=65535, len(ping_bytes)=3", summary)
1164+
}
1165+
10761166
// TestUpdateNextRevocation checks that the method `updateNextRevocation` is
10771167
// behave as expected.
10781168
func TestUpdateNextRevocation(t *testing.T) {

0 commit comments

Comments
 (0)