Skip to content

Commit 9eb610f

Browse files
authored
p2p/discover: repeat WHOAREYOU challenge when handshake in progress (ethereum#31356)
This fixes the handshake in a scenario where the remote end sends two unknown packets in a row. When this happens, we would previously respond to both with a WHOAREYOU challenge, but keep only the latest sent challenge. Transmission is assumed to be unreliable, so any client that sends two request packets simultaneously has to be prepared to follow up on whichever request leads to a handshake. With this fix, we force them to do the handshake that we can actually complete. Fixes ethereum#30581
1 parent 9fc2bbe commit 9eb610f

File tree

3 files changed

+100
-2
lines changed

3 files changed

+100
-2
lines changed

p2p/discover/v5_udp.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,20 @@ const (
5050
// encoding/decoding and with the handshake; the UDPv5 object handles higher-level concerns.
5151
type codecV5 interface {
5252
// Encode encodes a packet.
53-
Encode(enode.ID, string, v5wire.Packet, *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error)
53+
//
54+
// If the underlying type of 'p' is *v5wire.Whoareyou, a Whoareyou challenge packet is
55+
// encoded. If the 'challenge' parameter is non-nil, the packet is encoded as a
56+
// handshake message packet. Otherwise, the packet will be encoded as an ordinary
57+
// message packet.
58+
Encode(id enode.ID, addr string, p v5wire.Packet, challenge *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error)
5459

5560
// Decode decodes a packet. It returns a *v5wire.Unknown packet if decryption fails.
5661
// The *enode.Node return value is non-nil when the input contains a handshake response.
57-
Decode([]byte, string) (enode.ID, *enode.Node, v5wire.Packet, error)
62+
Decode(b []byte, addr string) (enode.ID, *enode.Node, v5wire.Packet, error)
63+
64+
// CurrentChallenge returns the most recent WHOAREYOU challenge that was encoded to given node.
65+
// This will return a non-nil value if there is an active handshake attempt with the node, and nil otherwise.
66+
CurrentChallenge(id enode.ID, addr string) *v5wire.Whoareyou
5867
}
5968

6069
// UDPv5 is the implementation of protocol version 5.
@@ -824,6 +833,19 @@ func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr netip.AddrPort
824833

825834
// handleUnknown initiates a handshake by responding with WHOAREYOU.
826835
func (t *UDPv5) handleUnknown(p *v5wire.Unknown, fromID enode.ID, fromAddr netip.AddrPort) {
836+
currentChallenge := t.codec.CurrentChallenge(fromID, fromAddr.String())
837+
if currentChallenge != nil {
838+
// This case happens when the sender issues multiple concurrent requests.
839+
// Since we only support one in-progress handshake at a time, we need to tell
840+
// them which handshake attempt they need to complete. We tell them to use the
841+
// existing handshake attempt since the response to that one might still be in
842+
// transit.
843+
t.log.Debug("Repeating discv5 handshake challenge", "id", fromID, "addr", fromAddr)
844+
t.sendResponse(fromID, fromAddr, currentChallenge)
845+
return
846+
}
847+
848+
// Send a fresh challenge.
827849
challenge := &v5wire.Whoareyou{Nonce: p.Nonce}
828850
crand.Read(challenge.IDNonce[:])
829851
if n := t.GetNode(fromID); n != nil {

p2p/discover/v5_udp_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,26 @@ func TestUDPv5_unknownPacket(t *testing.T) {
140140
test.waitPacketOut(func(p *v5wire.Whoareyou, addr netip.AddrPort, _ v5wire.Nonce) {
141141
check(p, 0)
142142
})
143+
}
144+
145+
func TestUDPv5_unknownPacketKnownNode(t *testing.T) {
146+
t.Parallel()
147+
test := newUDPV5Test(t)
148+
defer test.close()
149+
150+
nonce := v5wire.Nonce{1, 2, 3}
151+
check := func(p *v5wire.Whoareyou, wantSeq uint64) {
152+
t.Helper()
153+
if p.Nonce != nonce {
154+
t.Error("wrong nonce in WHOAREYOU:", p.Nonce, nonce)
155+
}
156+
if p.IDNonce == ([16]byte{}) {
157+
t.Error("all zero ID nonce")
158+
}
159+
if p.RecordSeq != wantSeq {
160+
t.Errorf("wrong record seq %d in WHOAREYOU, want %d", p.RecordSeq, wantSeq)
161+
}
162+
}
143163

144164
// Make node known.
145165
n := test.getNode(test.remotekey, test.remoteaddr).Node()
@@ -151,6 +171,42 @@ func TestUDPv5_unknownPacket(t *testing.T) {
151171
})
152172
}
153173

174+
// This test checks that, when multiple 'unknown' packets are received during a handshake,
175+
// the node sticks to the first handshake attempt.
176+
func TestUDPv5_handshakeRepeatChallenge(t *testing.T) {
177+
t.Parallel()
178+
test := newUDPV5Test(t)
179+
defer test.close()
180+
181+
nonce1 := v5wire.Nonce{1}
182+
nonce2 := v5wire.Nonce{2}
183+
nonce3 := v5wire.Nonce{3}
184+
check := func(p *v5wire.Whoareyou, wantNonce v5wire.Nonce) {
185+
t.Helper()
186+
if p.Nonce != wantNonce {
187+
t.Error("wrong nonce in WHOAREYOU:", p.Nonce, wantNonce)
188+
}
189+
}
190+
191+
// Unknown packet from unknown node.
192+
test.packetIn(&v5wire.Unknown{Nonce: nonce1})
193+
test.waitPacketOut(func(p *v5wire.Whoareyou, addr netip.AddrPort, _ v5wire.Nonce) {
194+
check(p, nonce1)
195+
})
196+
197+
// Second unknown packet. Here we expect the response to reference the
198+
// first unknown packet.
199+
test.packetIn(&v5wire.Unknown{Nonce: nonce2})
200+
test.waitPacketOut(func(p *v5wire.Whoareyou, addr netip.AddrPort, _ v5wire.Nonce) {
201+
check(p, nonce1)
202+
})
203+
// Third unknown packet. This should still return the first nonce.
204+
test.packetIn(&v5wire.Unknown{Nonce: nonce3})
205+
test.waitPacketOut(func(p *v5wire.Whoareyou, addr netip.AddrPort, _ v5wire.Nonce) {
206+
check(p, nonce1)
207+
})
208+
}
209+
154210
// This test checks that incoming FINDNODE calls are handled correctly.
155211
func TestUDPv5_findnodeHandling(t *testing.T) {
156212
t.Parallel()
@@ -698,6 +754,8 @@ type testCodec struct {
698754
test *udpV5Test
699755
id enode.ID
700756
ctr uint64
757+
758+
sentChallenges map[enode.ID]*v5wire.Whoareyou
701759
}
702760

703761
type testCodecFrame struct {
@@ -712,11 +770,23 @@ func (c *testCodec) Encode(toID enode.ID, addr string, p v5wire.Packet, _ *v5wir
712770
var authTag v5wire.Nonce
713771
binary.BigEndian.PutUint64(authTag[:], c.ctr)
714772

773+
if w, ok := p.(*v5wire.Whoareyou); ok {
774+
// Store recently sent Whoareyou challenges.
775+
if c.sentChallenges == nil {
776+
c.sentChallenges = make(map[enode.ID]*v5wire.Whoareyou)
777+
}
778+
c.sentChallenges[toID] = w
779+
}
780+
715781
penc, _ := rlp.EncodeToBytes(p)
716782
frame, err := rlp.EncodeToBytes(testCodecFrame{c.id, authTag, p.Kind(), penc})
717783
return frame, authTag, err
718784
}
719785

786+
func (c *testCodec) CurrentChallenge(id enode.ID, addr string) *v5wire.Whoareyou {
787+
return c.sentChallenges[id]
788+
}
789+
720790
func (c *testCodec) Decode(input []byte, addr string) (enode.ID, *enode.Node, v5wire.Packet, error) {
721791
frame, p, err := c.decodeFrame(input)
722792
if err != nil {

p2p/discover/v5wire/encoding.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, err
245245
return c.buf.Bytes(), nil
246246
}
247247

248+
// CurrentChallenge returns the latest challenge sent to the given node.
249+
// This will return non-nil while a handshake is in progress.
250+
func (c *Codec) CurrentChallenge(id enode.ID, addr string) *Whoareyou {
251+
return c.sc.getHandshake(id, addr)
252+
}
253+
248254
func (c *Codec) writeHeaders(head *Header) {
249255
c.buf.Reset()
250256
c.buf.Write(head.IV[:])

0 commit comments

Comments
 (0)