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