123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- // 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},
- }
|