This adds a test suite for discovery v4. The test suite is a port of the Hive suite for
discovery, and will replace the current suite on Hive soon-ish. The tests can be
run locally with this command:
    devp2p discv4 test -remote enode//...
Co-authored-by: Felix Lange <fjl@twurst.com>
		
	
		
			
				
	
	
		
			468 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			468 lines
		
	
	
		
			14 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 v4test
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"crypto/rand"
 | 
						|
	"fmt"
 | 
						|
	"net"
 | 
						|
	"reflect"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/ethereum/go-ethereum/crypto"
 | 
						|
	"github.com/ethereum/go-ethereum/internal/utesting"
 | 
						|
	"github.com/ethereum/go-ethereum/p2p/discover/v4wire"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	expiration  = 20 * time.Second
 | 
						|
	wrongPacket = 66
 | 
						|
	macSize     = 256 / 8
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	// Remote node under test
 | 
						|
	Remote string
 | 
						|
	// IP where the first tester is listening, port will be assigned
 | 
						|
	Listen1 string = "127.0.0.1"
 | 
						|
	// IP where the second tester is listening, port will be assigned
 | 
						|
	// Before running the test, you may have to `sudo ifconfig lo0 add 127.0.0.2` (on MacOS at least)
 | 
						|
	Listen2 string = "127.0.0.2"
 | 
						|
)
 | 
						|
 | 
						|
type pingWithJunk struct {
 | 
						|
	Version    uint
 | 
						|
	From, To   v4wire.Endpoint
 | 
						|
	Expiration uint64
 | 
						|
	JunkData1  uint
 | 
						|
	JunkData2  []byte
 | 
						|
}
 | 
						|
 | 
						|
func (req *pingWithJunk) Name() string { return "PING/v4" }
 | 
						|
func (req *pingWithJunk) Kind() byte   { return v4wire.PingPacket }
 | 
						|
 | 
						|
type pingWrongType struct {
 | 
						|
	Version    uint
 | 
						|
	From, To   v4wire.Endpoint
 | 
						|
	Expiration uint64
 | 
						|
}
 | 
						|
 | 
						|
func (req *pingWrongType) Name() string { return "WRONG/v4" }
 | 
						|
func (req *pingWrongType) Kind() byte   { return wrongPacket }
 | 
						|
 | 
						|
func futureExpiration() uint64 {
 | 
						|
	return uint64(time.Now().Add(expiration).Unix())
 | 
						|
}
 | 
						|
 | 
						|
// This test just sends a PING packet and expects a response.
 | 
						|
func BasicPing(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	pingHash := te.send(te.l1, &v4wire.Ping{
 | 
						|
		Version:    4,
 | 
						|
		From:       te.localEndpoint(te.l1),
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
	})
 | 
						|
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if err := te.checkPong(reply, pingHash); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// checkPong verifies that reply is a valid PONG matching the given ping hash.
 | 
						|
func (te *testenv) checkPong(reply v4wire.Packet, pingHash []byte) error {
 | 
						|
	if reply == nil || reply.Kind() != v4wire.PongPacket {
 | 
						|
		return fmt.Errorf("expected PONG reply, got %v", reply)
 | 
						|
	}
 | 
						|
	pong := reply.(*v4wire.Pong)
 | 
						|
	if !bytes.Equal(pong.ReplyTok, pingHash) {
 | 
						|
		return fmt.Errorf("PONG reply token mismatch: got %x, want %x", pong.ReplyTok, pingHash)
 | 
						|
	}
 | 
						|
	wantEndpoint := te.localEndpoint(te.l1)
 | 
						|
	if !reflect.DeepEqual(pong.To, wantEndpoint) {
 | 
						|
		return fmt.Errorf("PONG 'to' endpoint mismatch: got %+v, want %+v", pong.To, wantEndpoint)
 | 
						|
	}
 | 
						|
	if v4wire.Expired(pong.Expiration) {
 | 
						|
		return fmt.Errorf("PONG is expired (%v)", pong.Expiration)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// This test sends a PING packet with wrong 'to' field and expects a PONG response.
 | 
						|
func PingWrongTo(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
 | 
						|
	pingHash := te.send(te.l1, &v4wire.Ping{
 | 
						|
		Version:    4,
 | 
						|
		From:       te.localEndpoint(te.l1),
 | 
						|
		To:         wrongEndpoint,
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
	})
 | 
						|
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if err := te.checkPong(reply, pingHash); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test sends a PING packet with wrong 'from' field and expects a PONG response.
 | 
						|
func PingWrongFrom(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
 | 
						|
	pingHash := te.send(te.l1, &v4wire.Ping{
 | 
						|
		Version:    4,
 | 
						|
		From:       wrongEndpoint,
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
	})
 | 
						|
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if err := te.checkPong(reply, pingHash); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test sends a PING packet with additional data at the end and expects a PONG
 | 
						|
// response. The remote node should respond because EIP-8 mandates ignoring additional
 | 
						|
// trailing data.
 | 
						|
func PingExtraData(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	pingHash := te.send(te.l1, &pingWithJunk{
 | 
						|
		Version:    4,
 | 
						|
		From:       te.localEndpoint(te.l1),
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
		JunkData1:  42,
 | 
						|
		JunkData2:  []byte{9, 8, 7, 6, 5, 4, 3, 2, 1},
 | 
						|
	})
 | 
						|
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if err := te.checkPong(reply, pingHash); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test sends a PING packet with additional data and wrong 'from' field
 | 
						|
// and expects a PONG response.
 | 
						|
func PingExtraDataWrongFrom(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
 | 
						|
	req := pingWithJunk{
 | 
						|
		Version:    4,
 | 
						|
		From:       wrongEndpoint,
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
		JunkData1:  42,
 | 
						|
		JunkData2:  []byte{9, 8, 7, 6, 5, 4, 3, 2, 1},
 | 
						|
	}
 | 
						|
	pingHash := te.send(te.l1, &req)
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if err := te.checkPong(reply, pingHash); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test sends a PING packet with an expiration in the past.
 | 
						|
// The remote node should not respond.
 | 
						|
func PingPastExpiration(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	te.send(te.l1, &v4wire.Ping{
 | 
						|
		Version:    4,
 | 
						|
		From:       te.localEndpoint(te.l1),
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: -futureExpiration(),
 | 
						|
	})
 | 
						|
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if reply != nil {
 | 
						|
		t.Fatal("Expected no reply, got", reply)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test sends an invalid packet. The remote node should not respond.
 | 
						|
func WrongPacketType(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	te.send(te.l1, &pingWrongType{
 | 
						|
		Version:    4,
 | 
						|
		From:       te.localEndpoint(te.l1),
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
	})
 | 
						|
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if reply != nil {
 | 
						|
		t.Fatal("Expected no reply, got", reply)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test verifies that the default behaviour of ignoring 'from' fields is unaffected by
 | 
						|
// the bonding process. After bonding, it pings the target with a different from endpoint.
 | 
						|
func BondThenPingWithWrongFrom(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
	bond(t, te)
 | 
						|
 | 
						|
	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
 | 
						|
	pingHash := te.send(te.l1, &v4wire.Ping{
 | 
						|
		Version:    4,
 | 
						|
		From:       wrongEndpoint,
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
	})
 | 
						|
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if err := te.checkPong(reply, pingHash); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test just sends FINDNODE. The remote node should not reply
 | 
						|
// because the endpoint proof has not completed.
 | 
						|
func FindnodeWithoutEndpointProof(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	req := v4wire.Findnode{Expiration: futureExpiration()}
 | 
						|
	rand.Read(req.Target[:])
 | 
						|
	te.send(te.l1, &req)
 | 
						|
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if reply != nil {
 | 
						|
		t.Fatal("Expected no response, got", reply)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// BasicFindnode sends a FINDNODE request after performing the endpoint
 | 
						|
// proof. The remote node should respond.
 | 
						|
func BasicFindnode(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
	bond(t, te)
 | 
						|
 | 
						|
	findnode := v4wire.Findnode{Expiration: futureExpiration()}
 | 
						|
	rand.Read(findnode.Target[:])
 | 
						|
	te.send(te.l1, &findnode)
 | 
						|
 | 
						|
	reply, _, err := te.read(te.l1)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal("read find nodes", err)
 | 
						|
	}
 | 
						|
	if reply.Kind() != v4wire.NeighborsPacket {
 | 
						|
		t.Fatal("Expected neighbors, got", reply.Name())
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test sends an unsolicited NEIGHBORS packet after the endpoint proof, then sends
 | 
						|
// FINDNODE to read the remote table. The remote node should not return the node contained
 | 
						|
// in the unsolicited NEIGHBORS packet.
 | 
						|
func UnsolicitedNeighbors(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
	bond(t, te)
 | 
						|
 | 
						|
	// Send unsolicited NEIGHBORS response.
 | 
						|
	fakeKey, _ := crypto.GenerateKey()
 | 
						|
	encFakeKey := v4wire.EncodePubkey(&fakeKey.PublicKey)
 | 
						|
	neighbors := v4wire.Neighbors{
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
		Nodes: []v4wire.Node{{
 | 
						|
			ID:  encFakeKey,
 | 
						|
			IP:  net.IP{1, 2, 3, 4},
 | 
						|
			UDP: 30303,
 | 
						|
			TCP: 30303,
 | 
						|
		}},
 | 
						|
	}
 | 
						|
	te.send(te.l1, &neighbors)
 | 
						|
 | 
						|
	// Check if the remote node included the fake node.
 | 
						|
	te.send(te.l1, &v4wire.Findnode{
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
		Target:     encFakeKey,
 | 
						|
	})
 | 
						|
 | 
						|
	reply, _, err := te.read(te.l1)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal("read find nodes", err)
 | 
						|
	}
 | 
						|
	if reply.Kind() != v4wire.NeighborsPacket {
 | 
						|
		t.Fatal("Expected neighbors, got", reply.Name())
 | 
						|
	}
 | 
						|
	nodes := reply.(*v4wire.Neighbors).Nodes
 | 
						|
	if contains(nodes, encFakeKey) {
 | 
						|
		t.Fatal("neighbors response contains node from earlier unsolicited neighbors response")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test sends FINDNODE with an expiration timestamp in the past.
 | 
						|
// The remote node should not respond.
 | 
						|
func FindnodePastExpiration(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
	bond(t, te)
 | 
						|
 | 
						|
	findnode := v4wire.Findnode{Expiration: -futureExpiration()}
 | 
						|
	rand.Read(findnode.Target[:])
 | 
						|
	te.send(te.l1, &findnode)
 | 
						|
 | 
						|
	for {
 | 
						|
		reply, _, _ := te.read(te.l1)
 | 
						|
		if reply == nil {
 | 
						|
			return
 | 
						|
		} else if reply.Kind() == v4wire.NeighborsPacket {
 | 
						|
			t.Fatal("Unexpected NEIGHBORS response for expired FINDNODE request")
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// bond performs the endpoint proof with the remote node.
 | 
						|
func bond(t *utesting.T, te *testenv) {
 | 
						|
	te.send(te.l1, &v4wire.Ping{
 | 
						|
		Version:    4,
 | 
						|
		From:       te.localEndpoint(te.l1),
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
	})
 | 
						|
 | 
						|
	var gotPing, gotPong bool
 | 
						|
	for !gotPing || !gotPong {
 | 
						|
		req, hash, err := te.read(te.l1)
 | 
						|
		if err != nil {
 | 
						|
			t.Fatal(err)
 | 
						|
		}
 | 
						|
		switch req.(type) {
 | 
						|
		case *v4wire.Ping:
 | 
						|
			te.send(te.l1, &v4wire.Pong{
 | 
						|
				To:         te.remoteEndpoint(),
 | 
						|
				ReplyTok:   hash,
 | 
						|
				Expiration: futureExpiration(),
 | 
						|
			})
 | 
						|
			gotPing = true
 | 
						|
		case *v4wire.Pong:
 | 
						|
			// TODO: maybe verify pong data here
 | 
						|
			gotPong = true
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test attempts to perform a traffic amplification attack against a
 | 
						|
// 'victim' endpoint using FINDNODE. In this attack scenario, the attacker
 | 
						|
// attempts to complete the endpoint proof non-interactively by sending a PONG
 | 
						|
// with mismatching reply token from the 'victim' endpoint. The attack works if
 | 
						|
// the remote node does not verify the PONG reply token field correctly. The
 | 
						|
// attacker could then perform traffic amplification by sending many FINDNODE
 | 
						|
// requests to the discovery node, which would reply to the 'victim' address.
 | 
						|
func FindnodeAmplificationInvalidPongHash(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	// Send PING to start endpoint verification.
 | 
						|
	te.send(te.l1, &v4wire.Ping{
 | 
						|
		Version:    4,
 | 
						|
		From:       te.localEndpoint(te.l1),
 | 
						|
		To:         te.remoteEndpoint(),
 | 
						|
		Expiration: futureExpiration(),
 | 
						|
	})
 | 
						|
 | 
						|
	var gotPing, gotPong bool
 | 
						|
	for !gotPing || !gotPong {
 | 
						|
		req, _, err := te.read(te.l1)
 | 
						|
		if err != nil {
 | 
						|
			t.Fatal(err)
 | 
						|
		}
 | 
						|
		switch req.(type) {
 | 
						|
		case *v4wire.Ping:
 | 
						|
			// Send PONG from this node ID, but with invalid ReplyTok.
 | 
						|
			te.send(te.l1, &v4wire.Pong{
 | 
						|
				To:         te.remoteEndpoint(),
 | 
						|
				ReplyTok:   make([]byte, macSize),
 | 
						|
				Expiration: futureExpiration(),
 | 
						|
			})
 | 
						|
			gotPing = true
 | 
						|
		case *v4wire.Pong:
 | 
						|
			gotPong = true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Now send FINDNODE. The remote node should not respond because our
 | 
						|
	// PONG did not reference the PING hash.
 | 
						|
	findnode := v4wire.Findnode{Expiration: futureExpiration()}
 | 
						|
	rand.Read(findnode.Target[:])
 | 
						|
	te.send(te.l1, &findnode)
 | 
						|
 | 
						|
	// If we receive a NEIGHBORS response, the attack worked and the test fails.
 | 
						|
	reply, _, _ := te.read(te.l1)
 | 
						|
	if reply != nil && reply.Kind() == v4wire.NeighborsPacket {
 | 
						|
		t.Error("Got neighbors")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// This test attempts to perform a traffic amplification attack using FINDNODE.
 | 
						|
// The attack works if the remote node does not verify the IP address of FINDNODE
 | 
						|
// against the endpoint verification proof done by PING/PONG.
 | 
						|
func FindnodeAmplificationWrongIP(t *utesting.T) {
 | 
						|
	te := newTestEnv(Remote, Listen1, Listen2)
 | 
						|
	defer te.close()
 | 
						|
 | 
						|
	// Do the endpoint proof from the l1 IP.
 | 
						|
	bond(t, te)
 | 
						|
 | 
						|
	// Now send FINDNODE from the same node ID, but different IP address.
 | 
						|
	// The remote node should not respond.
 | 
						|
	findnode := v4wire.Findnode{Expiration: futureExpiration()}
 | 
						|
	rand.Read(findnode.Target[:])
 | 
						|
	te.send(te.l2, &findnode)
 | 
						|
 | 
						|
	// If we receive a NEIGHBORS response, the attack worked and the test fails.
 | 
						|
	reply, _, _ := te.read(te.l2)
 | 
						|
	if reply != nil {
 | 
						|
		t.Error("Got NEIGHORS response for FINDNODE from wrong IP")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
var AllTests = []utesting.Test{
 | 
						|
	{Name: "Ping/Basic", Fn: BasicPing},
 | 
						|
	{Name: "Ping/WrongTo", Fn: PingWrongTo},
 | 
						|
	{Name: "Ping/WrongFrom", Fn: PingWrongFrom},
 | 
						|
	{Name: "Ping/ExtraData", Fn: PingExtraData},
 | 
						|
	{Name: "Ping/ExtraDataWrongFrom", Fn: PingExtraDataWrongFrom},
 | 
						|
	{Name: "Ping/PastExpiration", Fn: PingPastExpiration},
 | 
						|
	{Name: "Ping/WrongPacketType", Fn: WrongPacketType},
 | 
						|
	{Name: "Ping/BondThenPingWithWrongFrom", Fn: BondThenPingWithWrongFrom},
 | 
						|
	{Name: "Findnode/WithoutEndpointProof", Fn: FindnodeWithoutEndpointProof},
 | 
						|
	{Name: "Findnode/BasicFindnode", Fn: BasicFindnode},
 | 
						|
	{Name: "Findnode/UnsolicitedNeighbors", Fn: UnsolicitedNeighbors},
 | 
						|
	{Name: "Findnode/PastExpiration", Fn: FindnodePastExpiration},
 | 
						|
	{Name: "Amplification/InvalidPongHash", Fn: FindnodeAmplificationInvalidPongHash},
 | 
						|
	{Name: "Amplification/WrongIP", Fn: FindnodeAmplificationWrongIP},
 | 
						|
}
 |