p2p/discover: implement v5.1 wire protocol (#21647)
This change implements the Discovery v5.1 wire protocol and also adds an interactive test suite for this protocol.
This commit is contained in:
377
cmd/devp2p/internal/v5test/discv5tests.go
Normal file
377
cmd/devp2p/internal/v5test/discv5tests.go
Normal file
@ -0,0 +1,377 @@
|
||||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v5test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/internal/utesting"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover/v5wire"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
||||
)
|
||||
|
||||
// Suite is the discv5 test suite.
|
||||
type Suite struct {
|
||||
Dest *enode.Node
|
||||
Listen1, Listen2 string // listening addresses
|
||||
}
|
||||
|
||||
func (s *Suite) listen1(log logger) (*conn, net.PacketConn) {
|
||||
c := newConn(s.Dest, log)
|
||||
l := c.listen(s.Listen1)
|
||||
return c, l
|
||||
}
|
||||
|
||||
func (s *Suite) listen2(log logger) (*conn, net.PacketConn, net.PacketConn) {
|
||||
c := newConn(s.Dest, log)
|
||||
l1, l2 := c.listen(s.Listen1), c.listen(s.Listen2)
|
||||
return c, l1, l2
|
||||
}
|
||||
|
||||
func (s *Suite) AllTests() []utesting.Test {
|
||||
return []utesting.Test{
|
||||
{Name: "Ping", Fn: s.TestPing},
|
||||
{Name: "PingLargeRequestID", Fn: s.TestPingLargeRequestID},
|
||||
{Name: "PingMultiIP", Fn: s.TestPingMultiIP},
|
||||
{Name: "PingHandshakeInterrupted", Fn: s.TestPingHandshakeInterrupted},
|
||||
{Name: "TalkRequest", Fn: s.TestTalkRequest},
|
||||
{Name: "FindnodeZeroDistance", Fn: s.TestFindnodeZeroDistance},
|
||||
{Name: "FindnodeResults", Fn: s.TestFindnodeResults},
|
||||
}
|
||||
}
|
||||
|
||||
// This test sends PING and expects a PONG response.
|
||||
func (s *Suite) TestPing(t *utesting.T) {
|
||||
conn, l1 := s.listen1(t)
|
||||
defer conn.close()
|
||||
|
||||
ping := &v5wire.Ping{ReqID: conn.nextReqID()}
|
||||
switch resp := conn.reqresp(l1, ping).(type) {
|
||||
case *v5wire.Pong:
|
||||
checkPong(t, resp, ping, l1)
|
||||
default:
|
||||
t.Fatal("expected PONG, got", resp.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func checkPong(t *utesting.T, pong *v5wire.Pong, ping *v5wire.Ping, c net.PacketConn) {
|
||||
if !bytes.Equal(pong.ReqID, ping.ReqID) {
|
||||
t.Fatalf("wrong request ID %x in PONG, want %x", pong.ReqID, ping.ReqID)
|
||||
}
|
||||
if !pong.ToIP.Equal(laddr(c).IP) {
|
||||
t.Fatalf("wrong destination IP %v in PONG, want %v", pong.ToIP, laddr(c).IP)
|
||||
}
|
||||
if int(pong.ToPort) != laddr(c).Port {
|
||||
t.Fatalf("wrong destination port %v in PONG, want %v", pong.ToPort, laddr(c).Port)
|
||||
}
|
||||
}
|
||||
|
||||
// This test sends PING with a 9-byte request ID, which isn't allowed by the spec.
|
||||
// The remote node should not respond.
|
||||
func (s *Suite) TestPingLargeRequestID(t *utesting.T) {
|
||||
conn, l1 := s.listen1(t)
|
||||
defer conn.close()
|
||||
|
||||
ping := &v5wire.Ping{ReqID: make([]byte, 9)}
|
||||
switch resp := conn.reqresp(l1, ping).(type) {
|
||||
case *v5wire.Pong:
|
||||
t.Errorf("PONG response with unknown request ID %x", resp.ReqID)
|
||||
case *readError:
|
||||
if resp.err == v5wire.ErrInvalidReqID {
|
||||
t.Error("response with oversized request ID")
|
||||
} else if !netutil.IsTimeout(resp.err) {
|
||||
t.Error(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In this test, a session is established from one IP as usual. The session is then reused
|
||||
// on another IP, which shouldn't work. The remote node should respond with WHOAREYOU for
|
||||
// the attempt from a different IP.
|
||||
func (s *Suite) TestPingMultiIP(t *utesting.T) {
|
||||
conn, l1, l2 := s.listen2(t)
|
||||
defer conn.close()
|
||||
|
||||
// Create the session on l1.
|
||||
ping := &v5wire.Ping{ReqID: conn.nextReqID()}
|
||||
resp := conn.reqresp(l1, ping)
|
||||
if resp.Kind() != v5wire.PongMsg {
|
||||
t.Fatal("expected PONG, got", resp)
|
||||
}
|
||||
checkPong(t, resp.(*v5wire.Pong), ping, l1)
|
||||
|
||||
// Send on l2. This reuses the session because there is only one codec.
|
||||
ping2 := &v5wire.Ping{ReqID: conn.nextReqID()}
|
||||
conn.write(l2, ping2, nil)
|
||||
switch resp := conn.read(l2).(type) {
|
||||
case *v5wire.Pong:
|
||||
t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(l2).IP, laddr(l1).IP)
|
||||
case *v5wire.Whoareyou:
|
||||
t.Logf("got WHOAREYOU for new session as expected")
|
||||
resp.Node = s.Dest
|
||||
conn.write(l2, ping2, resp)
|
||||
default:
|
||||
t.Fatal("expected WHOAREYOU, got", resp)
|
||||
}
|
||||
|
||||
// Catch the PONG on l2.
|
||||
switch resp := conn.read(l2).(type) {
|
||||
case *v5wire.Pong:
|
||||
checkPong(t, resp, ping2, l2)
|
||||
default:
|
||||
t.Fatal("expected PONG, got", resp)
|
||||
}
|
||||
|
||||
// Try on l1 again.
|
||||
ping3 := &v5wire.Ping{ReqID: conn.nextReqID()}
|
||||
conn.write(l1, ping3, nil)
|
||||
switch resp := conn.read(l1).(type) {
|
||||
case *v5wire.Pong:
|
||||
t.Fatalf("remote responded to PING from %v for session on IP %v", laddr(l1).IP, laddr(l2).IP)
|
||||
case *v5wire.Whoareyou:
|
||||
t.Logf("got WHOAREYOU for new session as expected")
|
||||
default:
|
||||
t.Fatal("expected WHOAREYOU, got", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// This test starts a handshake, but doesn't finish it and sends a second ordinary message
|
||||
// packet instead of a handshake message packet. The remote node should respond with
|
||||
// another WHOAREYOU challenge for the second packet.
|
||||
func (s *Suite) TestPingHandshakeInterrupted(t *utesting.T) {
|
||||
conn, l1 := s.listen1(t)
|
||||
defer conn.close()
|
||||
|
||||
// First PING triggers challenge.
|
||||
ping := &v5wire.Ping{ReqID: conn.nextReqID()}
|
||||
conn.write(l1, ping, nil)
|
||||
switch resp := conn.read(l1).(type) {
|
||||
case *v5wire.Whoareyou:
|
||||
t.Logf("got WHOAREYOU for PING")
|
||||
default:
|
||||
t.Fatal("expected WHOAREYOU, got", resp)
|
||||
}
|
||||
|
||||
// Send second PING.
|
||||
ping2 := &v5wire.Ping{ReqID: conn.nextReqID()}
|
||||
switch resp := conn.reqresp(l1, ping2).(type) {
|
||||
case *v5wire.Pong:
|
||||
checkPong(t, resp, ping2, l1)
|
||||
default:
|
||||
t.Fatal("expected WHOAREYOU, got", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// This test sends TALKREQ and expects an empty TALKRESP response.
|
||||
func (s *Suite) TestTalkRequest(t *utesting.T) {
|
||||
conn, l1 := s.listen1(t)
|
||||
defer conn.close()
|
||||
|
||||
// Non-empty request ID.
|
||||
id := conn.nextReqID()
|
||||
resp := conn.reqresp(l1, &v5wire.TalkRequest{ReqID: id, Protocol: "test-protocol"})
|
||||
switch resp := resp.(type) {
|
||||
case *v5wire.TalkResponse:
|
||||
if !bytes.Equal(resp.ReqID, id) {
|
||||
t.Fatalf("wrong request ID %x in TALKRESP, want %x", resp.ReqID, id)
|
||||
}
|
||||
if len(resp.Message) > 0 {
|
||||
t.Fatalf("non-empty message %x in TALKRESP", resp.Message)
|
||||
}
|
||||
default:
|
||||
t.Fatal("expected TALKRESP, got", resp.Name())
|
||||
}
|
||||
|
||||
// Empty request ID.
|
||||
resp = conn.reqresp(l1, &v5wire.TalkRequest{Protocol: "test-protocol"})
|
||||
switch resp := resp.(type) {
|
||||
case *v5wire.TalkResponse:
|
||||
if len(resp.ReqID) > 0 {
|
||||
t.Fatalf("wrong request ID %x in TALKRESP, want empty byte array", resp.ReqID)
|
||||
}
|
||||
if len(resp.Message) > 0 {
|
||||
t.Fatalf("non-empty message %x in TALKRESP", resp.Message)
|
||||
}
|
||||
default:
|
||||
t.Fatal("expected TALKRESP, got", resp.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks that the remote node returns itself for FINDNODE with distance zero.
|
||||
func (s *Suite) TestFindnodeZeroDistance(t *utesting.T) {
|
||||
conn, l1 := s.listen1(t)
|
||||
defer conn.close()
|
||||
|
||||
nodes, err := conn.findnode(l1, []uint{0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("remote returned more than one node for FINDNODE [0]")
|
||||
}
|
||||
if nodes[0].ID() != conn.remote.ID() {
|
||||
t.Errorf("ID of response node is %v, want %v", nodes[0].ID(), conn.remote.ID())
|
||||
}
|
||||
}
|
||||
|
||||
// In this test, multiple nodes ping the node under test. After waiting for them to be
|
||||
// accepted into the remote table, the test checks that they are returned by FINDNODE.
|
||||
func (s *Suite) TestFindnodeResults(t *utesting.T) {
|
||||
// Create bystanders.
|
||||
nodes := make([]*bystander, 5)
|
||||
added := make(chan enode.ID, len(nodes))
|
||||
for i := range nodes {
|
||||
nodes[i] = newBystander(t, s, added)
|
||||
defer nodes[i].close()
|
||||
}
|
||||
|
||||
// Get them added to the remote table.
|
||||
timeout := 60 * time.Second
|
||||
timeoutCh := time.After(timeout)
|
||||
for count := 0; count < len(nodes); {
|
||||
select {
|
||||
case id := <-added:
|
||||
t.Logf("bystander node %v added to remote table", id)
|
||||
count++
|
||||
case <-timeoutCh:
|
||||
t.Errorf("remote added %d bystander nodes in %v, need %d to continue", count, timeout, len(nodes))
|
||||
t.Logf("this can happen if the node has a non-empty table from previous runs")
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Logf("all %d bystander nodes were added", len(nodes))
|
||||
|
||||
// Collect our nodes by distance.
|
||||
var dists []uint
|
||||
expect := make(map[enode.ID]*enode.Node)
|
||||
for _, bn := range nodes {
|
||||
n := bn.conn.localNode.Node()
|
||||
expect[n.ID()] = n
|
||||
d := uint(enode.LogDist(n.ID(), s.Dest.ID()))
|
||||
if !containsUint(dists, d) {
|
||||
dists = append(dists, d)
|
||||
}
|
||||
}
|
||||
|
||||
// Send FINDNODE for all distances.
|
||||
conn, l1 := s.listen1(t)
|
||||
defer conn.close()
|
||||
foundNodes, err := conn.findnode(l1, dists)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("remote returned %d nodes for distance list %v", len(foundNodes), dists)
|
||||
for _, n := range foundNodes {
|
||||
delete(expect, n.ID())
|
||||
}
|
||||
if len(expect) > 0 {
|
||||
t.Errorf("missing %d nodes in FINDNODE result", len(expect))
|
||||
t.Logf("this can happen if the test is run multiple times in quick succession")
|
||||
t.Logf("and the remote node hasn't removed dead nodes from previous runs yet")
|
||||
} else {
|
||||
t.Logf("all %d expected nodes were returned", len(nodes))
|
||||
}
|
||||
}
|
||||
|
||||
// A bystander is a node whose only purpose is filling a spot in the remote table.
|
||||
type bystander struct {
|
||||
dest *enode.Node
|
||||
conn *conn
|
||||
l net.PacketConn
|
||||
|
||||
addedCh chan enode.ID
|
||||
done sync.WaitGroup
|
||||
}
|
||||
|
||||
func newBystander(t *utesting.T, s *Suite, added chan enode.ID) *bystander {
|
||||
conn, l := s.listen1(t)
|
||||
conn.setEndpoint(l) // bystander nodes need IP/port to get pinged
|
||||
bn := &bystander{
|
||||
conn: conn,
|
||||
l: l,
|
||||
dest: s.Dest,
|
||||
addedCh: added,
|
||||
}
|
||||
bn.done.Add(1)
|
||||
go bn.loop()
|
||||
return bn
|
||||
}
|
||||
|
||||
// id returns the node ID of the bystander.
|
||||
func (bn *bystander) id() enode.ID {
|
||||
return bn.conn.localNode.ID()
|
||||
}
|
||||
|
||||
// close shuts down loop.
|
||||
func (bn *bystander) close() {
|
||||
bn.conn.close()
|
||||
bn.done.Wait()
|
||||
}
|
||||
|
||||
// loop answers packets from the remote node until quit.
|
||||
func (bn *bystander) loop() {
|
||||
defer bn.done.Done()
|
||||
|
||||
var (
|
||||
lastPing time.Time
|
||||
wasAdded bool
|
||||
)
|
||||
for {
|
||||
// Ping the remote node.
|
||||
if !wasAdded && time.Since(lastPing) > 10*time.Second {
|
||||
bn.conn.reqresp(bn.l, &v5wire.Ping{
|
||||
ReqID: bn.conn.nextReqID(),
|
||||
ENRSeq: bn.dest.Seq(),
|
||||
})
|
||||
lastPing = time.Now()
|
||||
}
|
||||
// Answer packets.
|
||||
switch p := bn.conn.read(bn.l).(type) {
|
||||
case *v5wire.Ping:
|
||||
bn.conn.write(bn.l, &v5wire.Pong{
|
||||
ReqID: p.ReqID,
|
||||
ENRSeq: bn.conn.localNode.Seq(),
|
||||
ToIP: bn.dest.IP(),
|
||||
ToPort: uint16(bn.dest.UDP()),
|
||||
}, nil)
|
||||
wasAdded = true
|
||||
bn.notifyAdded()
|
||||
case *v5wire.Findnode:
|
||||
bn.conn.write(bn.l, &v5wire.Nodes{ReqID: p.ReqID, Total: 1}, nil)
|
||||
wasAdded = true
|
||||
bn.notifyAdded()
|
||||
case *v5wire.TalkRequest:
|
||||
bn.conn.write(bn.l, &v5wire.TalkResponse{ReqID: p.ReqID}, nil)
|
||||
case *readError:
|
||||
if !netutil.IsTemporaryError(p.err) {
|
||||
bn.conn.logf("shutting down: %v", p.err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bn *bystander) notifyAdded() {
|
||||
if bn.addedCh != nil {
|
||||
bn.addedCh <- bn.id()
|
||||
bn.addedCh = nil
|
||||
}
|
||||
}
|
263
cmd/devp2p/internal/v5test/framework.go
Normal file
263
cmd/devp2p/internal/v5test/framework.go
Normal file
@ -0,0 +1,263 @@
|
||||
// Copyright 2020 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v5test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover/v5wire"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
||||
)
|
||||
|
||||
// readError represents an error during packet reading.
|
||||
// This exists to facilitate type-switching on the result of conn.read.
|
||||
type readError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *readError) Kind() byte { return 99 }
|
||||
func (p *readError) Name() string { return fmt.Sprintf("error: %v", p.err) }
|
||||
func (p *readError) Error() string { return p.err.Error() }
|
||||
func (p *readError) Unwrap() error { return p.err }
|
||||
func (p *readError) RequestID() []byte { return nil }
|
||||
func (p *readError) SetRequestID([]byte) {}
|
||||
|
||||
// readErrorf creates a readError with the given text.
|
||||
func readErrorf(format string, args ...interface{}) *readError {
|
||||
return &readError{fmt.Errorf(format, args...)}
|
||||
}
|
||||
|
||||
// This is the response timeout used in tests.
|
||||
const waitTime = 300 * time.Millisecond
|
||||
|
||||
// conn is a connection to the node under test.
|
||||
type conn struct {
|
||||
localNode *enode.LocalNode
|
||||
localKey *ecdsa.PrivateKey
|
||||
remote *enode.Node
|
||||
remoteAddr *net.UDPAddr
|
||||
listeners []net.PacketConn
|
||||
|
||||
log logger
|
||||
codec *v5wire.Codec
|
||||
lastRequest v5wire.Packet
|
||||
lastChallenge *v5wire.Whoareyou
|
||||
idCounter uint32
|
||||
}
|
||||
|
||||
type logger interface {
|
||||
Logf(string, ...interface{})
|
||||
}
|
||||
|
||||
// newConn sets up a connection to the given node.
|
||||
func newConn(dest *enode.Node, log logger) *conn {
|
||||
key, err := crypto.GenerateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db, err := enode.OpenDB("")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ln := enode.NewLocalNode(db, key)
|
||||
|
||||
return &conn{
|
||||
localKey: key,
|
||||
localNode: ln,
|
||||
remote: dest,
|
||||
remoteAddr: &net.UDPAddr{IP: dest.IP(), Port: dest.UDP()},
|
||||
codec: v5wire.NewCodec(ln, key, mclock.System{}),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *conn) setEndpoint(c net.PacketConn) {
|
||||
tc.localNode.SetStaticIP(laddr(c).IP)
|
||||
tc.localNode.SetFallbackUDP(laddr(c).Port)
|
||||
}
|
||||
|
||||
func (tc *conn) listen(ip string) net.PacketConn {
|
||||
l, err := net.ListenPacket("udp", fmt.Sprintf("%v:0", ip))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tc.listeners = append(tc.listeners, l)
|
||||
return l
|
||||
}
|
||||
|
||||
// close shuts down all listeners and the local node.
|
||||
func (tc *conn) close() {
|
||||
for _, l := range tc.listeners {
|
||||
l.Close()
|
||||
}
|
||||
tc.localNode.Database().Close()
|
||||
}
|
||||
|
||||
// nextReqID creates a request id.
|
||||
func (tc *conn) nextReqID() []byte {
|
||||
id := make([]byte, 4)
|
||||
tc.idCounter++
|
||||
binary.BigEndian.PutUint32(id, tc.idCounter)
|
||||
return id
|
||||
}
|
||||
|
||||
// reqresp performs a request/response interaction on the given connection.
|
||||
// The request is retried if a handshake is requested.
|
||||
func (tc *conn) reqresp(c net.PacketConn, req v5wire.Packet) v5wire.Packet {
|
||||
reqnonce := tc.write(c, req, nil)
|
||||
switch resp := tc.read(c).(type) {
|
||||
case *v5wire.Whoareyou:
|
||||
if resp.Nonce != reqnonce {
|
||||
return readErrorf("wrong nonce %x in WHOAREYOU (want %x)", resp.Nonce[:], reqnonce[:])
|
||||
}
|
||||
resp.Node = tc.remote
|
||||
tc.write(c, req, resp)
|
||||
return tc.read(c)
|
||||
default:
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
// findnode sends a FINDNODE request and waits for its responses.
|
||||
func (tc *conn) findnode(c net.PacketConn, dists []uint) ([]*enode.Node, error) {
|
||||
var (
|
||||
findnode = &v5wire.Findnode{ReqID: tc.nextReqID(), Distances: dists}
|
||||
reqnonce = tc.write(c, findnode, nil)
|
||||
first = true
|
||||
total uint8
|
||||
results []*enode.Node
|
||||
)
|
||||
for n := 1; n > 0; {
|
||||
switch resp := tc.read(c).(type) {
|
||||
case *v5wire.Whoareyou:
|
||||
// Handle handshake.
|
||||
if resp.Nonce == reqnonce {
|
||||
resp.Node = tc.remote
|
||||
tc.write(c, findnode, resp)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unexpected WHOAREYOU (nonce %x), waiting for NODES", resp.Nonce[:])
|
||||
}
|
||||
case *v5wire.Ping:
|
||||
// Handle ping from remote.
|
||||
tc.write(c, &v5wire.Pong{
|
||||
ReqID: resp.ReqID,
|
||||
ENRSeq: tc.localNode.Seq(),
|
||||
}, nil)
|
||||
case *v5wire.Nodes:
|
||||
// Got NODES! Check request ID.
|
||||
if !bytes.Equal(resp.ReqID, findnode.ReqID) {
|
||||
return nil, fmt.Errorf("NODES response has wrong request id %x", resp.ReqID)
|
||||
}
|
||||
// Check total count. It should be greater than one
|
||||
// and needs to be the same across all responses.
|
||||
if first {
|
||||
if resp.Total == 0 || resp.Total > 6 {
|
||||
return nil, fmt.Errorf("invalid NODES response 'total' %d (not in (0,7))", resp.Total)
|
||||
}
|
||||
total = resp.Total
|
||||
n = int(total) - 1
|
||||
first = false
|
||||
} else {
|
||||
n--
|
||||
if resp.Total != total {
|
||||
return nil, fmt.Errorf("invalid NODES response 'total' %d (!= %d)", resp.Total, total)
|
||||
}
|
||||
}
|
||||
// Check nodes.
|
||||
nodes, err := checkRecords(resp.Nodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid node in NODES response: %v", err)
|
||||
}
|
||||
results = append(results, nodes...)
|
||||
default:
|
||||
return nil, fmt.Errorf("expected NODES, got %v", resp)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// write sends a packet on the given connection.
|
||||
func (tc *conn) write(c net.PacketConn, p v5wire.Packet, challenge *v5wire.Whoareyou) v5wire.Nonce {
|
||||
packet, nonce, err := tc.codec.Encode(tc.remote.ID(), tc.remoteAddr.String(), p, challenge)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't encode %v packet: %v", p.Name(), err))
|
||||
}
|
||||
if _, err := c.WriteTo(packet, tc.remoteAddr); err != nil {
|
||||
tc.logf("Can't send %s: %v", p.Name(), err)
|
||||
} else {
|
||||
tc.logf(">> %s", p.Name())
|
||||
}
|
||||
return nonce
|
||||
}
|
||||
|
||||
// read waits for an incoming packet on the given connection.
|
||||
func (tc *conn) read(c net.PacketConn) v5wire.Packet {
|
||||
buf := make([]byte, 1280)
|
||||
if err := c.SetReadDeadline(time.Now().Add(waitTime)); err != nil {
|
||||
return &readError{err}
|
||||
}
|
||||
n, fromAddr, err := c.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return &readError{err}
|
||||
}
|
||||
_, _, p, err := tc.codec.Decode(buf[:n], fromAddr.String())
|
||||
if err != nil {
|
||||
return &readError{err}
|
||||
}
|
||||
tc.logf("<< %s", p.Name())
|
||||
return p
|
||||
}
|
||||
|
||||
// logf prints to the test log.
|
||||
func (tc *conn) logf(format string, args ...interface{}) {
|
||||
if tc.log != nil {
|
||||
tc.log.Logf("(%s) %s", tc.localNode.ID().TerminalString(), fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func laddr(c net.PacketConn) *net.UDPAddr {
|
||||
return c.LocalAddr().(*net.UDPAddr)
|
||||
}
|
||||
|
||||
func checkRecords(records []*enr.Record) ([]*enode.Node, error) {
|
||||
nodes := make([]*enode.Node, len(records))
|
||||
for i := range records {
|
||||
n, err := enode.New(enode.ValidSchemes, records[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes[i] = n
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func containsUint(ints []uint, x uint) bool {
|
||||
for i := range ints {
|
||||
if ints[i] == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Reference in New Issue
Block a user