mirror of https://github.com/ethereum/go-ethereum
p2p: move rlpx into separate package (#21464)
This change moves the RLPx protocol implementation into a separate package, p2p/rlpx. The new package can be used to establish RLPx connections for protocol testing purposes. Co-authored-by: Felix Lange <fjl@twurst.com>pull/21666/head
parent
2c097bb7a2
commit
129cf075e9
@ -0,0 +1,94 @@ |
||||
// 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 main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/p2p" |
||||
"github.com/ethereum/go-ethereum/p2p/rlpx" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
"gopkg.in/urfave/cli.v1" |
||||
) |
||||
|
||||
var ( |
||||
rlpxCommand = cli.Command{ |
||||
Name: "rlpx", |
||||
Usage: "RLPx Commands", |
||||
Subcommands: []cli.Command{ |
||||
rlpxPingCommand, |
||||
}, |
||||
} |
||||
rlpxPingCommand = cli.Command{ |
||||
Name: "ping", |
||||
Usage: "Perform a RLPx handshake", |
||||
ArgsUsage: "<node>", |
||||
Action: rlpxPing, |
||||
} |
||||
) |
||||
|
||||
func rlpxPing(ctx *cli.Context) error { |
||||
n := getNodeArg(ctx) |
||||
|
||||
fd, err := net.Dial("tcp", fmt.Sprintf("%v:%d", n.IP(), n.TCP())) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
conn := rlpx.NewConn(fd, n.Pubkey()) |
||||
|
||||
ourKey, _ := crypto.GenerateKey() |
||||
_, err = conn.Handshake(ourKey) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
code, data, _, err := conn.Read() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
switch code { |
||||
case 0: |
||||
var h devp2pHandshake |
||||
if err := rlp.DecodeBytes(data, &h); err != nil { |
||||
return fmt.Errorf("invalid handshake: %v", err) |
||||
} |
||||
fmt.Printf("%+v\n", h) |
||||
case 1: |
||||
var msg []p2p.DiscReason |
||||
if rlp.DecodeBytes(data, &msg); len(msg) == 0 { |
||||
return fmt.Errorf("invalid disconnect message") |
||||
} |
||||
return fmt.Errorf("received disconnect message: %v", msg[0]) |
||||
default: |
||||
return fmt.Errorf("invalid message code %d, expected handshake (code zero)", code) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// devp2pHandshake is the RLP structure of the devp2p protocol handshake.
|
||||
type devp2pHandshake struct { |
||||
Version uint64 |
||||
Name string |
||||
Caps []p2p.Cap |
||||
ListenPort uint64 |
||||
ID hexutil.Bytes // secp256k1 public key
|
||||
// Ignore additional fields (for forward compatibility).
|
||||
Rest []rlp.RawValue `rlp:"tail"` |
||||
} |
@ -0,0 +1,177 @@ |
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package p2p |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/ecdsa" |
||||
"fmt" |
||||
"io" |
||||
"net" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/common/bitutil" |
||||
"github.com/ethereum/go-ethereum/metrics" |
||||
"github.com/ethereum/go-ethereum/p2p/rlpx" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
const ( |
||||
// total timeout for encryption handshake and protocol
|
||||
// handshake in both directions.
|
||||
handshakeTimeout = 5 * time.Second |
||||
|
||||
// This is the timeout for sending the disconnect reason.
|
||||
// This is shorter than the usual timeout because we don't want
|
||||
// to wait if the connection is known to be bad anyway.
|
||||
discWriteTimeout = 1 * time.Second |
||||
) |
||||
|
||||
// rlpxTransport is the transport used by actual (non-test) connections.
|
||||
// It wraps an RLPx connection with locks and read/write deadlines.
|
||||
type rlpxTransport struct { |
||||
rmu, wmu sync.Mutex |
||||
wbuf bytes.Buffer |
||||
conn *rlpx.Conn |
||||
} |
||||
|
||||
func newRLPX(conn net.Conn, dialDest *ecdsa.PublicKey) transport { |
||||
return &rlpxTransport{conn: rlpx.NewConn(conn, dialDest)} |
||||
} |
||||
|
||||
func (t *rlpxTransport) ReadMsg() (Msg, error) { |
||||
t.rmu.Lock() |
||||
defer t.rmu.Unlock() |
||||
|
||||
var msg Msg |
||||
t.conn.SetReadDeadline(time.Now().Add(frameReadTimeout)) |
||||
code, data, wireSize, err := t.conn.Read() |
||||
if err == nil { |
||||
msg = Msg{ |
||||
ReceivedAt: time.Now(), |
||||
Code: code, |
||||
Size: uint32(len(data)), |
||||
meterSize: uint32(wireSize), |
||||
Payload: bytes.NewReader(data), |
||||
} |
||||
} |
||||
return msg, err |
||||
} |
||||
|
||||
func (t *rlpxTransport) WriteMsg(msg Msg) error { |
||||
t.wmu.Lock() |
||||
defer t.wmu.Unlock() |
||||
|
||||
// Copy message data to write buffer.
|
||||
t.wbuf.Reset() |
||||
if _, err := io.CopyN(&t.wbuf, msg.Payload, int64(msg.Size)); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Write the message.
|
||||
t.conn.SetWriteDeadline(time.Now().Add(frameWriteTimeout)) |
||||
size, err := t.conn.Write(msg.Code, t.wbuf.Bytes()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Set metrics.
|
||||
msg.meterSize = size |
||||
if metrics.Enabled && msg.meterCap.Name != "" { // don't meter non-subprotocol messages
|
||||
m := fmt.Sprintf("%s/%s/%d/%#02x", egressMeterName, msg.meterCap.Name, msg.meterCap.Version, msg.meterCode) |
||||
metrics.GetOrRegisterMeter(m, nil).Mark(int64(msg.meterSize)) |
||||
metrics.GetOrRegisterMeter(m+"/packets", nil).Mark(1) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (t *rlpxTransport) close(err error) { |
||||
t.wmu.Lock() |
||||
defer t.wmu.Unlock() |
||||
|
||||
// Tell the remote end why we're disconnecting if possible.
|
||||
// We only bother doing this if the underlying connection supports
|
||||
// setting a timeout tough.
|
||||
if t.conn != nil { |
||||
if r, ok := err.(DiscReason); ok && r != DiscNetworkError { |
||||
deadline := time.Now().Add(discWriteTimeout) |
||||
if err := t.conn.SetWriteDeadline(deadline); err == nil { |
||||
// Connection supports write deadline.
|
||||
t.wbuf.Reset() |
||||
rlp.Encode(&t.wbuf, []DiscReason{r}) |
||||
t.conn.Write(discMsg, t.wbuf.Bytes()) |
||||
} |
||||
} |
||||
} |
||||
t.conn.Close() |
||||
} |
||||
|
||||
func (t *rlpxTransport) doEncHandshake(prv *ecdsa.PrivateKey) (*ecdsa.PublicKey, error) { |
||||
t.conn.SetDeadline(time.Now().Add(handshakeTimeout)) |
||||
return t.conn.Handshake(prv) |
||||
} |
||||
|
||||
func (t *rlpxTransport) doProtoHandshake(our *protoHandshake) (their *protoHandshake, err error) { |
||||
// Writing our handshake happens concurrently, we prefer
|
||||
// returning the handshake read error. If the remote side
|
||||
// disconnects us early with a valid reason, we should return it
|
||||
// as the error so it can be tracked elsewhere.
|
||||
werr := make(chan error, 1) |
||||
go func() { werr <- Send(t, handshakeMsg, our) }() |
||||
if their, err = readProtocolHandshake(t); err != nil { |
||||
<-werr // make sure the write terminates too
|
||||
return nil, err |
||||
} |
||||
if err := <-werr; err != nil { |
||||
return nil, fmt.Errorf("write error: %v", err) |
||||
} |
||||
// If the protocol version supports Snappy encoding, upgrade immediately
|
||||
t.conn.SetSnappy(their.Version >= snappyProtocolVersion) |
||||
|
||||
return their, nil |
||||
} |
||||
|
||||
func readProtocolHandshake(rw MsgReader) (*protoHandshake, error) { |
||||
msg, err := rw.ReadMsg() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if msg.Size > baseProtocolMaxMsgSize { |
||||
return nil, fmt.Errorf("message too big") |
||||
} |
||||
if msg.Code == discMsg { |
||||
// Disconnect before protocol handshake is valid according to the
|
||||
// spec and we send it ourself if the post-handshake checks fail.
|
||||
// We can't return the reason directly, though, because it is echoed
|
||||
// back otherwise. Wrap it in a string instead.
|
||||
var reason [1]DiscReason |
||||
rlp.Decode(msg.Payload, &reason) |
||||
return nil, reason[0] |
||||
} |
||||
if msg.Code != handshakeMsg { |
||||
return nil, fmt.Errorf("expected handshake, got %x", msg.Code) |
||||
} |
||||
var hs protoHandshake |
||||
if err := msg.Decode(&hs); err != nil { |
||||
return nil, err |
||||
} |
||||
if len(hs.ID) != 64 || !bitutil.TestBytes(hs.ID) { |
||||
return nil, DiscInvalidIdentity |
||||
} |
||||
return &hs, nil |
||||
} |
@ -0,0 +1,148 @@ |
||||
// Copyright 2015 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package p2p |
||||
|
||||
import ( |
||||
"errors" |
||||
"reflect" |
||||
"sync" |
||||
"testing" |
||||
|
||||
"github.com/davecgh/go-spew/spew" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/p2p/simulations/pipes" |
||||
) |
||||
|
||||
func TestProtocolHandshake(t *testing.T) { |
||||
var ( |
||||
prv0, _ = crypto.GenerateKey() |
||||
pub0 = crypto.FromECDSAPub(&prv0.PublicKey)[1:] |
||||
hs0 = &protoHandshake{Version: 3, ID: pub0, Caps: []Cap{{"a", 0}, {"b", 2}}} |
||||
|
||||
prv1, _ = crypto.GenerateKey() |
||||
pub1 = crypto.FromECDSAPub(&prv1.PublicKey)[1:] |
||||
hs1 = &protoHandshake{Version: 3, ID: pub1, Caps: []Cap{{"c", 1}, {"d", 3}}} |
||||
|
||||
wg sync.WaitGroup |
||||
) |
||||
|
||||
fd0, fd1, err := pipes.TCPPipe() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
wg.Add(2) |
||||
go func() { |
||||
defer wg.Done() |
||||
defer fd0.Close() |
||||
frame := newRLPX(fd0, &prv1.PublicKey) |
||||
rpubkey, err := frame.doEncHandshake(prv0) |
||||
if err != nil { |
||||
t.Errorf("dial side enc handshake failed: %v", err) |
||||
return |
||||
} |
||||
if !reflect.DeepEqual(rpubkey, &prv1.PublicKey) { |
||||
t.Errorf("dial side remote pubkey mismatch: got %v, want %v", rpubkey, &prv1.PublicKey) |
||||
return |
||||
} |
||||
|
||||
phs, err := frame.doProtoHandshake(hs0) |
||||
if err != nil { |
||||
t.Errorf("dial side proto handshake error: %v", err) |
||||
return |
||||
} |
||||
phs.Rest = nil |
||||
if !reflect.DeepEqual(phs, hs1) { |
||||
t.Errorf("dial side proto handshake mismatch:\ngot: %s\nwant: %s\n", spew.Sdump(phs), spew.Sdump(hs1)) |
||||
return |
||||
} |
||||
frame.close(DiscQuitting) |
||||
}() |
||||
go func() { |
||||
defer wg.Done() |
||||
defer fd1.Close() |
||||
rlpx := newRLPX(fd1, nil) |
||||
rpubkey, err := rlpx.doEncHandshake(prv1) |
||||
if err != nil { |
||||
t.Errorf("listen side enc handshake failed: %v", err) |
||||
return |
||||
} |
||||
if !reflect.DeepEqual(rpubkey, &prv0.PublicKey) { |
||||
t.Errorf("listen side remote pubkey mismatch: got %v, want %v", rpubkey, &prv0.PublicKey) |
||||
return |
||||
} |
||||
|
||||
phs, err := rlpx.doProtoHandshake(hs1) |
||||
if err != nil { |
||||
t.Errorf("listen side proto handshake error: %v", err) |
||||
return |
||||
} |
||||
phs.Rest = nil |
||||
if !reflect.DeepEqual(phs, hs0) { |
||||
t.Errorf("listen side proto handshake mismatch:\ngot: %s\nwant: %s\n", spew.Sdump(phs), spew.Sdump(hs0)) |
||||
return |
||||
} |
||||
|
||||
if err := ExpectMsg(rlpx, discMsg, []DiscReason{DiscQuitting}); err != nil { |
||||
t.Errorf("error receiving disconnect: %v", err) |
||||
} |
||||
}() |
||||
wg.Wait() |
||||
} |
||||
|
||||
func TestProtocolHandshakeErrors(t *testing.T) { |
||||
tests := []struct { |
||||
code uint64 |
||||
msg interface{} |
||||
err error |
||||
}{ |
||||
{ |
||||
code: discMsg, |
||||
msg: []DiscReason{DiscQuitting}, |
||||
err: DiscQuitting, |
||||
}, |
||||
{ |
||||
code: 0x989898, |
||||
msg: []byte{1}, |
||||
err: errors.New("expected handshake, got 989898"), |
||||
}, |
||||
{ |
||||
code: handshakeMsg, |
||||
msg: make([]byte, baseProtocolMaxMsgSize+2), |
||||
err: errors.New("message too big"), |
||||
}, |
||||
{ |
||||
code: handshakeMsg, |
||||
msg: []byte{1, 2, 3}, |
||||
err: newPeerError(errInvalidMsg, "(code 0) (size 4) rlp: expected input list for p2p.protoHandshake"), |
||||
}, |
||||
{ |
||||
code: handshakeMsg, |
||||
msg: &protoHandshake{Version: 3}, |
||||
err: DiscInvalidIdentity, |
||||
}, |
||||
} |
||||
|
||||
for i, test := range tests { |
||||
p1, p2 := MsgPipe() |
||||
go Send(p1, test.code, test.msg) |
||||
_, err := readProtocolHandshake(p2) |
||||
if !reflect.DeepEqual(err, test.err) { |
||||
t.Errorf("test %d: error mismatch: got %q, want %q", i, err, test.err) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue