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:
86
cmd/devp2p/README.md
Normal file
86
cmd/devp2p/README.md
Normal file
@ -0,0 +1,86 @@
|
||||
# The devp2p command
|
||||
|
||||
The devp2p command line tool is a utility for low-level peer-to-peer debugging and
|
||||
protocol development purposes. It can do many things.
|
||||
|
||||
### ENR Decoding
|
||||
|
||||
Use `devp2p enrdump <base64>` to verify and display an Ethereum Node Record.
|
||||
|
||||
### Node Key Management
|
||||
|
||||
The `devp2p key ...` command family deals with node key files.
|
||||
|
||||
Run `devp2p key generate mynode.key` to create a new node key in the `mynode.key` file.
|
||||
|
||||
Run `devp2p key to-enode mynode.key -ip 127.0.0.1 -tcp 30303` to create an enode:// URL
|
||||
corresponding to the given node key and address information.
|
||||
|
||||
### Maintaining DNS Discovery Node Lists
|
||||
|
||||
The devp2p command can create and publish DNS discovery node lists.
|
||||
|
||||
Run `devp2p dns sign <directory>` to update the signature of a DNS discovery tree.
|
||||
|
||||
Run `devp2p dns sync <enrtree-URL>` to download a complete DNS discovery tree.
|
||||
|
||||
Run `devp2p dns to-cloudflare <directory>` to publish a tree to CloudFlare DNS.
|
||||
|
||||
Run `devp2p dns to-route53 <directory>` to publish a tree to Amazon Route53.
|
||||
|
||||
You can find more information about these commands in the [DNS Discovery Setup Guide][dns-tutorial].
|
||||
|
||||
### Discovery v4 Utilities
|
||||
|
||||
The `devp2p discv4 ...` command family deals with the [Node Discovery v4][discv4]
|
||||
protocol.
|
||||
|
||||
Run `devp2p discv4 ping <enode/ENR>` to ping a node.
|
||||
|
||||
Run `devp2p discv4 resolve <enode/ENR>` to find the most recent node record of a node in
|
||||
the DHT.
|
||||
|
||||
Run `devp2p discv4 crawl <nodes.json path>` to create or update a JSON node set.
|
||||
|
||||
### Discovery v5 Utilities
|
||||
|
||||
The `devp2p discv5 ...` command family deals with the [Node Discovery v5][discv5]
|
||||
protocol. This protocol is currently under active development.
|
||||
|
||||
Run `devp2p discv5 ping <ENR>` to ping a node.
|
||||
|
||||
Run `devp2p discv5 resolve <ENR>` to find the most recent node record of a node in
|
||||
the discv5 DHT.
|
||||
|
||||
Run `devp2p discv5 listen` to run a Discovery v5 node.
|
||||
|
||||
Run `devp2p discv5 crawl <nodes.json path>` to create or update a JSON node set containing
|
||||
discv5 nodes.
|
||||
|
||||
### Discovery Test Suites
|
||||
|
||||
The devp2p command also contains interactive test suites for Discovery v4 and Discovery
|
||||
v5.
|
||||
|
||||
To run these tests against your implementation, you need to set up a networking
|
||||
environment where two separate UDP listening addresses are available on the same machine.
|
||||
The two listening addresses must also be routed such that they are able to reach the node
|
||||
you want to test.
|
||||
|
||||
For example, if you want to run the test on your local host, and the node under test is
|
||||
also on the local host, you need to assign two IP addresses (or a larger range) to your
|
||||
loopback interface. On macOS, this can be done by executing the following command:
|
||||
|
||||
sudo ifconfig lo0 add 127.0.0.2
|
||||
|
||||
You can now run either test suite as follows: Start the node under test first, ensuring
|
||||
that it won't talk to the Internet (i.e. disable bootstrapping). An easy way to prevent
|
||||
unintended connections to the global DHT is listening on `127.0.0.1`.
|
||||
|
||||
Now get the ENR of your node and store it in the `NODE` environment variable.
|
||||
|
||||
Start the test by running `devp2p discv5 test -listen1 127.0.0.1 -listen2 127.0.0.2 $NODE`.
|
||||
|
||||
[dns-tutorial]: https://geth.ethereum.org/docs/developers/dns-discovery-setup
|
||||
[discv4]: https://github.com/ethereum/devp2p/tree/master/discv4.md
|
||||
[discv5]: https://github.com/ethereum/devp2p/tree/master/discv5/discv5.md
|
@ -286,7 +286,11 @@ func listen(ln *enode.LocalNode, addr string) *net.UDPConn {
|
||||
}
|
||||
usocket := socket.(*net.UDPConn)
|
||||
uaddr := socket.LocalAddr().(*net.UDPAddr)
|
||||
ln.SetFallbackIP(net.IP{127, 0, 0, 1})
|
||||
if uaddr.IP.IsUnspecified() {
|
||||
ln.SetFallbackIP(net.IP{127, 0, 0, 1})
|
||||
} else {
|
||||
ln.SetFallbackIP(uaddr.IP)
|
||||
}
|
||||
ln.SetFallbackUDP(uaddr.Port)
|
||||
return usocket
|
||||
}
|
||||
@ -294,7 +298,11 @@ func listen(ln *enode.LocalNode, addr string) *net.UDPConn {
|
||||
func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
|
||||
s := params.RinkebyBootnodes
|
||||
if ctx.IsSet(bootnodesFlag.Name) {
|
||||
s = strings.Split(ctx.String(bootnodesFlag.Name), ",")
|
||||
input := ctx.String(bootnodesFlag.Name)
|
||||
if input == "" {
|
||||
return nil, nil
|
||||
}
|
||||
s = strings.Split(input, ",")
|
||||
}
|
||||
nodes := make([]*enode.Node, len(s))
|
||||
var err error
|
||||
|
@ -18,9 +18,13 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/cmd/devp2p/internal/v5test"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/internal/utesting"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
@ -33,6 +37,7 @@ var (
|
||||
discv5PingCommand,
|
||||
discv5ResolveCommand,
|
||||
discv5CrawlCommand,
|
||||
discv5TestCommand,
|
||||
discv5ListenCommand,
|
||||
},
|
||||
}
|
||||
@ -53,6 +58,12 @@ var (
|
||||
Action: discv5Crawl,
|
||||
Flags: []cli.Flag{bootnodesFlag, crawlTimeoutFlag},
|
||||
}
|
||||
discv5TestCommand = cli.Command{
|
||||
Name: "test",
|
||||
Usage: "Runs protocol tests against a node",
|
||||
Action: discv5Test,
|
||||
Flags: []cli.Flag{testPatternFlag, testListen1Flag, testListen2Flag},
|
||||
}
|
||||
discv5ListenCommand = cli.Command{
|
||||
Name: "listen",
|
||||
Usage: "Runs a node",
|
||||
@ -103,6 +114,30 @@ func discv5Crawl(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func discv5Test(ctx *cli.Context) error {
|
||||
// Disable logging unless explicitly enabled.
|
||||
if !ctx.GlobalIsSet("verbosity") && !ctx.GlobalIsSet("vmodule") {
|
||||
log.Root().SetHandler(log.DiscardHandler())
|
||||
}
|
||||
|
||||
// Filter and run test cases.
|
||||
suite := &v5test.Suite{
|
||||
Dest: getNodeArg(ctx),
|
||||
Listen1: ctx.String(testListen1Flag.Name),
|
||||
Listen2: ctx.String(testListen2Flag.Name),
|
||||
}
|
||||
tests := suite.AllTests()
|
||||
if ctx.IsSet(testPatternFlag.Name) {
|
||||
tests = utesting.MatchTests(tests, ctx.String(testPatternFlag.Name))
|
||||
}
|
||||
results := utesting.RunTests(tests, os.Stdout)
|
||||
if fails := utesting.CountFailures(results); fails > 0 {
|
||||
return fmt.Errorf("%v/%v tests passed.", len(tests)-fails, len(tests))
|
||||
}
|
||||
fmt.Printf("%v/%v passed\n", len(tests), len(tests))
|
||||
return nil
|
||||
}
|
||||
|
||||
func discv5Listen(ctx *cli.Context) error {
|
||||
disc := startV5(ctx)
|
||||
defer disc.Close()
|
||||
|
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