github.com/ethw3/go-ethereuma@v0.0.0-20221013053120-c14602a4c23c/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/ethw3/go-ethereuma/crypto"
    27  	"github.com/ethw3/go-ethereuma/internal/utesting"
    28  	"github.com/ethw3/go-ethereuma/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  	// IP where the first tester is listening, port will be assigned
    41  	Listen1 string = "127.0.0.1"
    42  	// 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  // This test 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  // This test 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  // This test 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  // 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  	bond(t, te)
   260  
   261  	wrongEndpoint := v4wire.Endpoint{IP: net.ParseIP("192.0.2.0")}
   262  	pingHash := te.send(te.l1, &v4wire.Ping{
   263  		Version:    4,
   264  		From:       wrongEndpoint,
   265  		To:         te.remoteEndpoint(),
   266  		Expiration: futureExpiration(),
   267  	})
   268  	if reply, _, err := te.read(te.l1); err != nil {
   269  		t.Fatal(err)
   270  	} else if err := te.checkPong(reply, pingHash); err != nil {
   271  		t.Fatal(err)
   272  	}
   273  }
   274  
   275  // This test just sends FINDNODE. The remote node should not reply
   276  // because the endpoint proof has not completed.
   277  func FindnodeWithoutEndpointProof(t *utesting.T) {
   278  	te := newTestEnv(Remote, Listen1, Listen2)
   279  	defer te.close()
   280  
   281  	req := v4wire.Findnode{Expiration: futureExpiration()}
   282  	rand.Read(req.Target[:])
   283  	te.send(te.l1, &req)
   284  
   285  	for {
   286  		reply, _, _ := te.read(te.l1)
   287  		if reply == nil {
   288  			// No response, all good
   289  			break
   290  		}
   291  		if reply.Kind() == v4wire.PingPacket {
   292  			continue // A ping is ok, just ignore it
   293  		}
   294  		t.Fatalf("Expected no reply, got %v %v", reply.Name(), reply)
   295  	}
   296  }
   297  
   298  // BasicFindnode sends a FINDNODE request after performing the endpoint
   299  // proof. The remote node should respond.
   300  func BasicFindnode(t *utesting.T) {
   301  	te := newTestEnv(Remote, Listen1, Listen2)
   302  	defer te.close()
   303  	bond(t, te)
   304  
   305  	findnode := v4wire.Findnode{Expiration: futureExpiration()}
   306  	rand.Read(findnode.Target[:])
   307  	te.send(te.l1, &findnode)
   308  
   309  	reply, _, err := te.read(te.l1)
   310  	if err != nil {
   311  		t.Fatal("read find nodes", err)
   312  	}
   313  	if reply.Kind() != v4wire.NeighborsPacket {
   314  		t.Fatalf("Expected neighbors, got %v %v", reply.Name(), reply)
   315  	}
   316  }
   317  
   318  // This test sends an unsolicited NEIGHBORS packet after the endpoint proof, then sends
   319  // FINDNODE to read the remote table. The remote node should not return the node contained
   320  // in the unsolicited NEIGHBORS packet.
   321  func UnsolicitedNeighbors(t *utesting.T) {
   322  	te := newTestEnv(Remote, Listen1, Listen2)
   323  	defer te.close()
   324  	bond(t, te)
   325  
   326  	// Send unsolicited NEIGHBORS response.
   327  	fakeKey, _ := crypto.GenerateKey()
   328  	encFakeKey := v4wire.EncodePubkey(&fakeKey.PublicKey)
   329  	neighbors := v4wire.Neighbors{
   330  		Expiration: futureExpiration(),
   331  		Nodes: []v4wire.Node{{
   332  			ID:  encFakeKey,
   333  			IP:  net.IP{1, 2, 3, 4},
   334  			UDP: 30303,
   335  			TCP: 30303,
   336  		}},
   337  	}
   338  	te.send(te.l1, &neighbors)
   339  
   340  	// Check if the remote node included the fake node.
   341  	te.send(te.l1, &v4wire.Findnode{
   342  		Expiration: futureExpiration(),
   343  		Target:     encFakeKey,
   344  	})
   345  
   346  	reply, _, err := te.read(te.l1)
   347  	if err != nil {
   348  		t.Fatal("read find nodes", err)
   349  	}
   350  	if reply.Kind() != v4wire.NeighborsPacket {
   351  		t.Fatalf("Expected neighbors, got %v %v", reply.Name(), reply)
   352  	}
   353  	nodes := reply.(*v4wire.Neighbors).Nodes
   354  	if contains(nodes, encFakeKey) {
   355  		t.Fatal("neighbors response contains node from earlier unsolicited neighbors response")
   356  	}
   357  }
   358  
   359  // This test sends FINDNODE with an expiration timestamp in the past.
   360  // The remote node should not respond.
   361  func FindnodePastExpiration(t *utesting.T) {
   362  	te := newTestEnv(Remote, Listen1, Listen2)
   363  	defer te.close()
   364  	bond(t, te)
   365  
   366  	findnode := v4wire.Findnode{Expiration: -futureExpiration()}
   367  	rand.Read(findnode.Target[:])
   368  	te.send(te.l1, &findnode)
   369  
   370  	for {
   371  		reply, _, _ := te.read(te.l1)
   372  		if reply == nil {
   373  			return
   374  		} else if reply.Kind() == v4wire.NeighborsPacket {
   375  			t.Fatal("Unexpected NEIGHBORS response for expired FINDNODE request")
   376  		}
   377  	}
   378  }
   379  
   380  // bond performs the endpoint proof with the remote node.
   381  func bond(t *utesting.T, te *testenv) {
   382  	te.send(te.l1, &v4wire.Ping{
   383  		Version:    4,
   384  		From:       te.localEndpoint(te.l1),
   385  		To:         te.remoteEndpoint(),
   386  		Expiration: futureExpiration(),
   387  	})
   388  
   389  	var gotPing, gotPong bool
   390  	for !gotPing || !gotPong {
   391  		req, hash, err := te.read(te.l1)
   392  		if err != nil {
   393  			t.Fatal(err)
   394  		}
   395  		switch req.(type) {
   396  		case *v4wire.Ping:
   397  			te.send(te.l1, &v4wire.Pong{
   398  				To:         te.remoteEndpoint(),
   399  				ReplyTok:   hash,
   400  				Expiration: futureExpiration(),
   401  			})
   402  			gotPing = true
   403  		case *v4wire.Pong:
   404  			// TODO: maybe verify pong data here
   405  			gotPong = true
   406  		}
   407  	}
   408  }
   409  
   410  // This test attempts to perform a traffic amplification attack against a
   411  // 'victim' endpoint using FINDNODE. In this attack scenario, the attacker
   412  // attempts to complete the endpoint proof non-interactively by sending a PONG
   413  // with mismatching reply token from the 'victim' endpoint. The attack works if
   414  // the remote node does not verify the PONG reply token field correctly. The
   415  // attacker could then perform traffic amplification by sending many FINDNODE
   416  // requests to the discovery node, which would reply to the 'victim' address.
   417  func FindnodeAmplificationInvalidPongHash(t *utesting.T) {
   418  	te := newTestEnv(Remote, Listen1, Listen2)
   419  	defer te.close()
   420  
   421  	// Send PING to start endpoint verification.
   422  	te.send(te.l1, &v4wire.Ping{
   423  		Version:    4,
   424  		From:       te.localEndpoint(te.l1),
   425  		To:         te.remoteEndpoint(),
   426  		Expiration: futureExpiration(),
   427  	})
   428  
   429  	var gotPing, gotPong bool
   430  	for !gotPing || !gotPong {
   431  		req, _, err := te.read(te.l1)
   432  		if err != nil {
   433  			t.Fatal(err)
   434  		}
   435  		switch req.(type) {
   436  		case *v4wire.Ping:
   437  			// Send PONG from this node ID, but with invalid ReplyTok.
   438  			te.send(te.l1, &v4wire.Pong{
   439  				To:         te.remoteEndpoint(),
   440  				ReplyTok:   make([]byte, macSize),
   441  				Expiration: futureExpiration(),
   442  			})
   443  			gotPing = true
   444  		case *v4wire.Pong:
   445  			gotPong = true
   446  		}
   447  	}
   448  
   449  	// Now send FINDNODE. The remote node should not respond because our
   450  	// PONG did not reference the PING hash.
   451  	findnode := v4wire.Findnode{Expiration: futureExpiration()}
   452  	rand.Read(findnode.Target[:])
   453  	te.send(te.l1, &findnode)
   454  
   455  	// If we receive a NEIGHBORS response, the attack worked and the test fails.
   456  	reply, _, _ := te.read(te.l1)
   457  	if reply != nil && reply.Kind() == v4wire.NeighborsPacket {
   458  		t.Error("Got neighbors")
   459  	}
   460  }
   461  
   462  // This test attempts to perform a traffic amplification attack using FINDNODE.
   463  // The attack works if the remote node does not verify the IP address of FINDNODE
   464  // against the endpoint verification proof done by PING/PONG.
   465  func FindnodeAmplificationWrongIP(t *utesting.T) {
   466  	te := newTestEnv(Remote, Listen1, Listen2)
   467  	defer te.close()
   468  
   469  	// Do the endpoint proof from the l1 IP.
   470  	bond(t, te)
   471  
   472  	// Now send FINDNODE from the same node ID, but different IP address.
   473  	// The remote node should not respond.
   474  	findnode := v4wire.Findnode{Expiration: futureExpiration()}
   475  	rand.Read(findnode.Target[:])
   476  	te.send(te.l2, &findnode)
   477  
   478  	// If we receive a NEIGHBORS response, the attack worked and the test fails.
   479  	reply, _, _ := te.read(te.l2)
   480  	if reply != nil {
   481  		t.Error("Got NEIGHORS response for FINDNODE from wrong IP")
   482  	}
   483  }
   484  
   485  var AllTests = []utesting.Test{
   486  	{Name: "Ping/Basic", Fn: BasicPing},
   487  	{Name: "Ping/WrongTo", Fn: PingWrongTo},
   488  	{Name: "Ping/WrongFrom", Fn: PingWrongFrom},
   489  	{Name: "Ping/ExtraData", Fn: PingExtraData},
   490  	{Name: "Ping/ExtraDataWrongFrom", Fn: PingExtraDataWrongFrom},
   491  	{Name: "Ping/PastExpiration", Fn: PingPastExpiration},
   492  	{Name: "Ping/WrongPacketType", Fn: WrongPacketType},
   493  	{Name: "Ping/BondThenPingWithWrongFrom", Fn: BondThenPingWithWrongFrom},
   494  	{Name: "Findnode/WithoutEndpointProof", Fn: FindnodeWithoutEndpointProof},
   495  	{Name: "Findnode/BasicFindnode", Fn: BasicFindnode},
   496  	{Name: "Findnode/UnsolicitedNeighbors", Fn: UnsolicitedNeighbors},
   497  	{Name: "Findnode/PastExpiration", Fn: FindnodePastExpiration},
   498  	{Name: "Amplification/InvalidPongHash", Fn: FindnodeAmplificationInvalidPongHash},
   499  	{Name: "Amplification/WrongIP", Fn: FindnodeAmplificationWrongIP},
   500  }