github.com/tirogen/go-ethereum@v1.10.12-0.20221226051715-250cfede41b6/cmd/devp2p/internal/v4test/discv4tests.go (about)

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