This change implements the Discovery v5.1 wire protocol and also adds an interactive test suite for this protocol.
		
			
				
	
	
		
			378 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			378 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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
 | |
| 	}
 | |
| }
 |