github.com/core-coin/go-core/v2@v2.1.9/cmd/devp2p/internal/v4test/discv4tests.go (about)

     1  // Copyright 2020 by the Authors
     2  // This file is part of go-core.
     3  //
     4  // go-core is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // go-core is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU General Public License
    15  // along with go-core. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package v4test
    18  
    19  import (
    20  	"bytes"
    21  	"crypto/rand"
    22  	"fmt"
    23  	"net"
    24  	"reflect"
    25  	"time"
    26  
    27  	"github.com/core-coin/go-core/v2/crypto"
    28  	"github.com/core-coin/go-core/v2/internal/utesting"
    29  	"github.com/core-coin/go-core/v2/p2p/discover/v4wire"
    30  )
    31  
    32  const (
    33  	expiration  = 20 * time.Second
    34  	wrongPacket = 66
    35  	macSize     = 256 / 8
    36  )
    37  
    38  var (
    39  	// Remote node under test
    40  	Remote string
    41  	// IP where the first tester is listening, port will be assigned
    42  	Listen1 string = "127.0.0.1"
    43  	// IP where the second tester is listening, port will be assigned
    44  	// Before running the test, you may have to `sudo ifconfig lo0 add 127.0.0.2` (on MacOS at least)
    45  	Listen2 string = "127.0.0.2"
    46  )
    47  
    48  type pingWithJunk struct {
    49  	Version    uint
    50  	From, To   v4wire.Endpoint
    51  	Expiration uint64
    52  	JunkData1  uint
    53  	JunkData2  []byte
    54  }
    55  
    56  func (req *pingWithJunk) Name() string { return "PING/v4" }
    57  func (req *pingWithJunk) Kind() byte   { return v4wire.PingPacket }
    58  
    59  type pingWrongType struct {
    60  	Version    uint
    61  	From, To   v4wire.Endpoint
    62  	Expiration uint64
    63  }
    64  
    65  func (req *pingWrongType) Name() string { return "WRONG/v4" }
    66  func (req *pingWrongType) Kind() byte   { return wrongPacket }
    67  
    68  func futureExpiration() uint64 {
    69  	return uint64(time.Now().Add(expiration).Unix())
    70  }
    71  
    72  // This test just sends a PING packet and expects a response.
    73  func BasicPing(t *utesting.T) {
    74  	te := newTestEnv(Remote, Listen1, Listen2)
    75  	defer te.close()
    76  
    77  	pingHash := te.send(te.l1, &v4wire.Ping{
    78  		Version:    4,
    79  		From:       te.localEndpoint(te.l1),
    80  		To:         te.remoteEndpoint(),
    81  		Expiration: futureExpiration(),
    82  	})
    83  
    84  	reply, _, _ := te.read(te.l1)
    85  	if err := te.checkPong(reply, pingHash); err != nil {
    86  		t.Fatal(err)
    87  	}
    88  }
    89  
    90  // checkPong verifies that reply is a valid PONG matching the given ping hash.
    91  func (te *testenv) checkPong(reply v4wire.Packet, pingHash []byte) error {
    92  	if reply == nil || reply.Kind() != v4wire.PongPacket {
    93  		return fmt.Errorf("expected PONG reply, got %v", reply)
    94  	}
    95  	pong := reply.(*v4wire.Pong)
    96  	if !bytes.Equal(pong.ReplyTok, pingHash) {
    97  		return fmt.Errorf("PONG reply token mismatch: got %x, want %x", pong.ReplyTok, pingHash)
    98  	}
    99  	wantEndpoint := te.localEndpoint(te.l1)
   100  	if !reflect.DeepEqual(pong.To, wantEndpoint) {
   101  		return fmt.Errorf("PONG 'to' endpoint mismatch: got %+v, want %+v", pong.To, wantEndpoint)
   102  	}
   103  	if v4wire.Expired(pong.Expiration) {
   104  		return fmt.Errorf("PONG is expired (%v)", pong.Expiration)
   105  	}
   106  	return nil
   107  }
   108  
   109  // This test sends a PING packet with wrong 'to' field and expects a PONG response.
   110  func PingWrongTo(t *utesting.T) {
   111  	te := newTestEnv(Remote, Listen1, Listen2)
   112  	defer te.close()
   113  
   114  	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
   115  	pingHash := te.send(te.l1, &v4wire.Ping{
   116  		Version:    4,
   117  		From:       te.localEndpoint(te.l1),
   118  		To:         wrongEndpoint,
   119  		Expiration: futureExpiration(),
   120  	})
   121  
   122  	reply, _, _ := te.read(te.l1)
   123  	if err := te.checkPong(reply, pingHash); err != nil {
   124  		t.Fatal(err)
   125  	}
   126  }
   127  
   128  // This test sends a PING packet with wrong 'from' field and expects a PONG response.
   129  func PingWrongFrom(t *utesting.T) {
   130  	te := newTestEnv(Remote, Listen1, Listen2)
   131  	defer te.close()
   132  
   133  	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
   134  	pingHash := te.send(te.l1, &v4wire.Ping{
   135  		Version:    4,
   136  		From:       wrongEndpoint,
   137  		To:         te.remoteEndpoint(),
   138  		Expiration: futureExpiration(),
   139  	})
   140  
   141  	reply, _, _ := te.read(te.l1)
   142  	if err := te.checkPong(reply, pingHash); err != nil {
   143  		t.Fatal(err)
   144  	}
   145  }
   146  
   147  // This test sends a PING packet with additional data at the end and expects a PONG
   148  // response. The remote node should respond because CIP-8 mandates ignoring additional
   149  // trailing data.
   150  func PingExtraData(t *utesting.T) {
   151  	te := newTestEnv(Remote, Listen1, Listen2)
   152  	defer te.close()
   153  
   154  	pingHash := te.send(te.l1, &pingWithJunk{
   155  		Version:    4,
   156  		From:       te.localEndpoint(te.l1),
   157  		To:         te.remoteEndpoint(),
   158  		Expiration: futureExpiration(),
   159  		JunkData1:  42,
   160  		JunkData2:  []byte{9, 8, 7, 6, 5, 4, 3, 2, 1},
   161  	})
   162  
   163  	reply, _, _ := te.read(te.l1)
   164  	if err := te.checkPong(reply, pingHash); err != nil {
   165  		t.Fatal(err)
   166  	}
   167  }
   168  
   169  // This test sends a PING packet with additional data and wrong 'from' field
   170  // and expects a PONG response.
   171  func PingExtraDataWrongFrom(t *utesting.T) {
   172  	te := newTestEnv(Remote, Listen1, Listen2)
   173  	defer te.close()
   174  
   175  	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
   176  	req := pingWithJunk{
   177  		Version:    4,
   178  		From:       wrongEndpoint,
   179  		To:         te.remoteEndpoint(),
   180  		Expiration: futureExpiration(),
   181  		JunkData1:  42,
   182  		JunkData2:  []byte{9, 8, 7, 6, 5, 4, 3, 2, 1},
   183  	}
   184  	pingHash := te.send(te.l1, &req)
   185  	reply, _, _ := te.read(te.l1)
   186  	if err := te.checkPong(reply, pingHash); err != nil {
   187  		t.Fatal(err)
   188  	}
   189  }
   190  
   191  // This test sends a PING packet with an expiration in the past.
   192  // The remote node should not respond.
   193  func PingPastExpiration(t *utesting.T) {
   194  	te := newTestEnv(Remote, Listen1, Listen2)
   195  	defer te.close()
   196  
   197  	te.send(te.l1, &v4wire.Ping{
   198  		Version:    4,
   199  		From:       te.localEndpoint(te.l1),
   200  		To:         te.remoteEndpoint(),
   201  		Expiration: -futureExpiration(),
   202  	})
   203  
   204  	reply, _, _ := te.read(te.l1)
   205  	if reply != nil {
   206  		t.Fatal("Expected no reply, got", reply)
   207  	}
   208  }
   209  
   210  // This test sends an invalid packet. The remote node should not respond.
   211  func WrongPacketType(t *utesting.T) {
   212  	te := newTestEnv(Remote, Listen1, Listen2)
   213  	defer te.close()
   214  
   215  	te.send(te.l1, &pingWrongType{
   216  		Version:    4,
   217  		From:       te.localEndpoint(te.l1),
   218  		To:         te.remoteEndpoint(),
   219  		Expiration: futureExpiration(),
   220  	})
   221  
   222  	reply, _, _ := te.read(te.l1)
   223  	if reply != nil {
   224  		t.Fatal("Expected no reply, got", reply)
   225  	}
   226  }
   227  
   228  // This test verifies that the default behaviour of ignoring 'from' fields is unaffected by
   229  // the bonding process. After bonding, it pings the target with a different from endpoint.
   230  func BondThenPingWithWrongFrom(t *utesting.T) {
   231  	te := newTestEnv(Remote, Listen1, Listen2)
   232  	defer te.close()
   233  	bond(t, te)
   234  
   235  	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
   236  	pingHash := te.send(te.l1, &v4wire.Ping{
   237  		Version:    4,
   238  		From:       wrongEndpoint,
   239  		To:         te.remoteEndpoint(),
   240  		Expiration: futureExpiration(),
   241  	})
   242  
   243  	reply, _, _ := te.read(te.l1)
   244  	if err := te.checkPong(reply, pingHash); err != nil {
   245  		t.Fatal(err)
   246  	}
   247  }
   248  
   249  // This test just sends FINDNODE. The remote node should not reply
   250  // because the endpoint proof has not completed.
   251  func FindnodeWithoutEndpointProof(t *utesting.T) {
   252  	te := newTestEnv(Remote, Listen1, Listen2)
   253  	defer te.close()
   254  
   255  	req := v4wire.Findnode{Expiration: futureExpiration()}
   256  	rand.Read(req.Target[:])
   257  	te.send(te.l1, &req)
   258  
   259  	reply, _, _ := te.read(te.l1)
   260  	if reply != nil {
   261  		t.Fatal("Expected no response, got", reply)
   262  	}
   263  }
   264  
   265  // BasicFindnode sends a FINDNODE request after performing the endpoint
   266  // proof. The remote node should respond.
   267  func BasicFindnode(t *utesting.T) {
   268  	te := newTestEnv(Remote, Listen1, Listen2)
   269  	defer te.close()
   270  	bond(t, te)
   271  
   272  	findnode := v4wire.Findnode{Expiration: futureExpiration()}
   273  	rand.Read(findnode.Target[:])
   274  	te.send(te.l1, &findnode)
   275  
   276  	reply, _, err := te.read(te.l1)
   277  	if err != nil {
   278  		t.Fatal("read find nodes", err)
   279  	}
   280  	if reply.Kind() != v4wire.NeighborsPacket {
   281  		t.Fatal("Expected neighbors, got", reply.Name())
   282  	}
   283  }
   284  
   285  // This test sends an unsolicited NEIGHBORS packet after the endpoint proof, then sends
   286  // FINDNODE to read the remote table. The remote node should not return the node contained
   287  // in the unsolicited NEIGHBORS packet.
   288  func UnsolicitedNeighbors(t *utesting.T) {
   289  	te := newTestEnv(Remote, Listen1, Listen2)
   290  	defer te.close()
   291  	bond(t, te)
   292  
   293  	// Send unsolicited NEIGHBORS response.
   294  	fakeKey, _ := crypto.GenerateKey(rand.Reader)
   295  	encFakeKey := v4wire.EncodePubkey(fakeKey.PublicKey())
   296  	neighbors := v4wire.Neighbors{
   297  		Expiration: futureExpiration(),
   298  		Nodes: []v4wire.Node{{
   299  			ID:  encFakeKey,
   300  			IP:  net.IP{1, 2, 3, 4},
   301  			UDP: 30300,
   302  			TCP: 30300,
   303  		}},
   304  	}
   305  	te.send(te.l1, &neighbors)
   306  
   307  	// Check if the remote node included the fake node.
   308  	te.send(te.l1, &v4wire.Findnode{
   309  		Expiration: futureExpiration(),
   310  		Target:     encFakeKey,
   311  	})
   312  
   313  	reply, _, err := te.read(te.l1)
   314  	if err != nil {
   315  		t.Fatal("read find nodes", err)
   316  	}
   317  	if reply.Kind() != v4wire.NeighborsPacket {
   318  		t.Fatal("Expected neighbors, got", reply.Name())
   319  	}
   320  	nodes := reply.(*v4wire.Neighbors).Nodes
   321  	if contains(nodes, encFakeKey) {
   322  		t.Fatal("neighbors response contains node from earlier unsolicited neighbors response")
   323  	}
   324  }
   325  
   326  // This test sends FINDNODE with an expiration timestamp in the past.
   327  // The remote node should not respond.
   328  func FindnodePastExpiration(t *utesting.T) {
   329  	te := newTestEnv(Remote, Listen1, Listen2)
   330  	defer te.close()
   331  	bond(t, te)
   332  
   333  	findnode := v4wire.Findnode{Expiration: -futureExpiration()}
   334  	rand.Read(findnode.Target[:])
   335  	te.send(te.l1, &findnode)
   336  
   337  	for {
   338  		reply, _, _ := te.read(te.l1)
   339  		if reply == nil {
   340  			return
   341  		} else if reply.Kind() == v4wire.NeighborsPacket {
   342  			t.Fatal("Unexpected NEIGHBORS response for expired FINDNODE request")
   343  		}
   344  	}
   345  }
   346  
   347  // bond performs the endpoint proof with the remote node.
   348  func bond(t *utesting.T, te *testenv) {
   349  	te.send(te.l1, &v4wire.Ping{
   350  		Version:    4,
   351  		From:       te.localEndpoint(te.l1),
   352  		To:         te.remoteEndpoint(),
   353  		Expiration: futureExpiration(),
   354  	})
   355  
   356  	var gotPing, gotPong bool
   357  	for !gotPing || !gotPong {
   358  		req, hash, err := te.read(te.l1)
   359  		if err != nil {
   360  			t.Fatal(err)
   361  		}
   362  		switch req.(type) {
   363  		case *v4wire.Ping:
   364  			te.send(te.l1, &v4wire.Pong{
   365  				To:         te.remoteEndpoint(),
   366  				ReplyTok:   hash,
   367  				Expiration: futureExpiration(),
   368  			})
   369  			gotPing = true
   370  		case *v4wire.Pong:
   371  			// TODO: maybe verify pong data here
   372  			gotPong = true
   373  		}
   374  	}
   375  }
   376  
   377  // This test attempts to perform a traffic amplification attack against a
   378  // 'victim' endpoint using FINDNODE. In this attack scenario, the attacker
   379  // attempts to complete the endpoint proof non-interactively by sending a PONG
   380  // with mismatching reply token from the 'victim' endpoint. The attack works if
   381  // the remote node does not verify the PONG reply token field correctly. The
   382  // attacker could then perform traffic amplification by sending many FINDNODE
   383  // requests to the discovery node, which would reply to the 'victim' address.
   384  func FindnodeAmplificationInvalidPongHash(t *utesting.T) {
   385  	te := newTestEnv(Remote, Listen1, Listen2)
   386  	defer te.close()
   387  
   388  	// Send PING to start endpoint verification.
   389  	te.send(te.l1, &v4wire.Ping{
   390  		Version:    4,
   391  		From:       te.localEndpoint(te.l1),
   392  		To:         te.remoteEndpoint(),
   393  		Expiration: futureExpiration(),
   394  	})
   395  
   396  	var gotPing, gotPong bool
   397  	for !gotPing || !gotPong {
   398  		req, _, err := te.read(te.l1)
   399  		if err != nil {
   400  			t.Fatal(err)
   401  		}
   402  		switch req.(type) {
   403  		case *v4wire.Ping:
   404  			// Send PONG from this node ID, but with invalid ReplyTok.
   405  			te.send(te.l1, &v4wire.Pong{
   406  				To:         te.remoteEndpoint(),
   407  				ReplyTok:   make([]byte, macSize),
   408  				Expiration: futureExpiration(),
   409  			})
   410  			gotPing = true
   411  		case *v4wire.Pong:
   412  			gotPong = true
   413  		}
   414  	}
   415  
   416  	// Now send FINDNODE. The remote node should not respond because our
   417  	// PONG did not reference the PING hash.
   418  	findnode := v4wire.Findnode{Expiration: futureExpiration()}
   419  	rand.Read(findnode.Target[:])
   420  	te.send(te.l1, &findnode)
   421  
   422  	// If we receive a NEIGHBORS response, the attack worked and the test fails.
   423  	reply, _, _ := te.read(te.l1)
   424  	if reply != nil && reply.Kind() == v4wire.NeighborsPacket {
   425  		t.Error("Got neighbors")
   426  	}
   427  }
   428  
   429  // This test attempts to perform a traffic amplification attack using FINDNODE.
   430  // The attack works if the remote node does not verify the IP address of FINDNODE
   431  // against the endpoint verification proof done by PING/PONG.
   432  func FindnodeAmplificationWrongIP(t *utesting.T) {
   433  	te := newTestEnv(Remote, Listen1, Listen2)
   434  	defer te.close()
   435  
   436  	// Do the endpoint proof from the l1 IP.
   437  	bond(t, te)
   438  
   439  	// Now send FINDNODE from the same node ID, but different IP address.
   440  	// The remote node should not respond.
   441  	findnode := v4wire.Findnode{Expiration: futureExpiration()}
   442  	rand.Read(findnode.Target[:])
   443  	te.send(te.l2, &findnode)
   444  
   445  	// If we receive a NEIGHBORS response, the attack worked and the test fails.
   446  	reply, _, _ := te.read(te.l2)
   447  	if reply != nil {
   448  		t.Error("Got NEIGHORS response for FINDNODE from wrong IP")
   449  	}
   450  }
   451  
   452  var AllTests = []utesting.Test{
   453  	{Name: "Ping/Basic", Fn: BasicPing},
   454  	{Name: "Ping/WrongTo", Fn: PingWrongTo},
   455  	{Name: "Ping/WrongFrom", Fn: PingWrongFrom},
   456  	{Name: "Ping/ExtraData", Fn: PingExtraData},
   457  	{Name: "Ping/ExtraDataWrongFrom", Fn: PingExtraDataWrongFrom},
   458  	{Name: "Ping/PastExpiration", Fn: PingPastExpiration},
   459  	{Name: "Ping/WrongPacketType", Fn: WrongPacketType},
   460  	{Name: "Ping/BondThenPingWithWrongFrom", Fn: BondThenPingWithWrongFrom},
   461  	{Name: "Findnode/WithoutEndpointProof", Fn: FindnodeWithoutEndpointProof},
   462  	{Name: "Findnode/BasicFindnode", Fn: BasicFindnode},
   463  	{Name: "Findnode/UnsolicitedNeighbors", Fn: UnsolicitedNeighbors},
   464  	{Name: "Findnode/PastExpiration", Fn: FindnodePastExpiration},
   465  	{Name: "Amplification/InvalidPongHash", Fn: FindnodeAmplificationInvalidPongHash},
   466  	{Name: "Amplification/WrongIP", Fn: FindnodeAmplificationWrongIP},
   467  }