From f12a3249af84c903e5ae82dfb4761fe74d2aeac8 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Sat, 19 Sep 2015 02:23:40 +0200 Subject: [PATCH 1/6] rlp: add IntSize --- rlp/raw.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rlp/raw.go b/rlp/raw.go index 33aae6ee53..8589c9259b 100644 --- a/rlp/raw.go +++ b/rlp/raw.go @@ -34,6 +34,13 @@ func ListSize(contentSize uint64) uint64 { return uint64(headsize(contentSize)) + contentSize } +func IntSize(i uint64) uint64 { + if i < 128 { + return 1 + } + return 1 + uint64(intsize(i)) +} + // Split returns the content of first RLP value and any // bytes after the value as subslices of b. func Split(b []byte) (k Kind, content, rest []byte, err error) { From d46655c56eeeb14d466e185de804bdf2c7e76a0d Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 21 Oct 2015 14:33:43 +0200 Subject: [PATCH 2/6] crypto/ecies: export and improve ConcatKDF --- crypto/ecies/ecies.go | 59 +++++++++++--------------------- crypto/ecies/ecies_test.go | 69 +++++++++++++++++++++++++++++++------- 2 files changed, 75 insertions(+), 53 deletions(-) diff --git a/crypto/ecies/ecies.go b/crypto/ecies/ecies.go index 65dc5b38ba..2c4dc70a4c 100644 --- a/crypto/ecies/ecies.go +++ b/crypto/ecies/ecies.go @@ -35,9 +35,11 @@ import ( "crypto/elliptic" "crypto/hmac" "crypto/subtle" + "encoding/binary" "fmt" "hash" "io" + "math" "math/big" ) @@ -143,50 +145,27 @@ var ( ErrInvalidMessage = fmt.Errorf("ecies: invalid message") ) -var ( - big2To32 = new(big.Int).Exp(big.NewInt(2), big.NewInt(32), nil) - big2To32M1 = new(big.Int).Sub(big2To32, big.NewInt(1)) -) - -func incCounter(ctr []byte) { - if ctr[3]++; ctr[3] != 0 { - return - } else if ctr[2]++; ctr[2] != 0 { - return - } else if ctr[1]++; ctr[1] != 0 { - return - } else if ctr[0]++; ctr[0] != 0 { - return +// ConcatKDF implements the Concatenation Key Derivation Function +// specified in NIST SP 800-56 (section 5.8.1). +// It returns kdlen bytes of key material derived from z and s1 using hash. +// kdlen is expected to be reasonably small. +func ConcatKDF(hash hash.Hash, z, s1 []byte, kdlen int) ([]byte, error) { + hashlen := hash.Size() + reps := (kdlen + hashlen - 1) / hashlen + if reps > math.MaxUint32 { + return nil, ErrKeyDataTooLong // prevent counter overflow } - return -} - -// NIST SP 800-56 Concatenation Key Derivation Function (see section 5.8.1). -func concatKDF(hash hash.Hash, z, s1 []byte, kdLen int) (k []byte, err error) { - if s1 == nil { - s1 = make([]byte, 0) - } - - reps := ((kdLen + 7) * 8) / (hash.BlockSize() * 8) - if big.NewInt(int64(reps)).Cmp(big2To32M1) > 0 { - fmt.Println(big2To32M1) - return nil, ErrKeyDataTooLong - } - - counter := []byte{0, 0, 0, 1} - k = make([]byte, 0) - - for i := 0; i <= reps; i++ { + counter := []byte{0, 0, 0, 0} + k := make([]byte, 0, reps*hashlen) + for i := uint32(1); i <= uint32(reps); i++ { + binary.BigEndian.PutUint32(counter, i) hash.Write(counter) hash.Write(z) hash.Write(s1) - k = append(k, hash.Sum(nil)...) + k = hash.Sum(k) hash.Reset() - incCounter(counter) } - - k = k[:kdLen] - return + return k[:kdlen], nil } // messageTag computes the MAC of a message (called the tag) as per @@ -264,7 +243,7 @@ func Encrypt(rand io.Reader, pub *PublicKey, m, s1, s2 []byte) (ct []byte, err e if err != nil { return } - K, err := concatKDF(hash, z, s1, params.KeyLen+params.KeyLen) + K, err := ConcatKDF(hash, z, s1, params.KeyLen+params.KeyLen) if err != nil { return } @@ -343,7 +322,7 @@ func (prv *PrivateKey) Decrypt(rand io.Reader, c, s1, s2 []byte) (m []byte, err return } - K, err := concatKDF(hash, z, s1, params.KeyLen+params.KeyLen) + K, err := ConcatKDF(hash, z, s1, params.KeyLen+params.KeyLen) if err != nil { return } diff --git a/crypto/ecies/ecies_test.go b/crypto/ecies/ecies_test.go index 6a0ea3f022..c671e061e5 100644 --- a/crypto/ecies/ecies_test.go +++ b/crypto/ecies/ecies_test.go @@ -53,20 +53,63 @@ func init() { dumpEnc = *flDump } -// Ensure the KDF generates appropriately sized keys. -func TestKDF(t *testing.T) { - msg := []byte("Hello, world") - h := sha256.New() +type kdftest struct { + key, s1 string + kdlen int + output string +} - k, err := concatKDF(h, msg, nil, 64) - if err != nil { - fmt.Println(err.Error()) - t.FailNow() - } - if len(k) != 64 { - fmt.Printf("KDF: generated key is the wrong size (%d instead of 64\n", - len(k)) - t.FailNow() +var kdftests = []kdftest{ + { + key: "38f9a331c022f51d66658f301837108c9710d5ee0697bfac97bb6b0ea8d4f273", + s1: "", + kdlen: 0, + output: "", + }, + { + key: "38f9a331c022f51d66658f301837108c9710d5ee0697bfac97bb6b0ea8d4f273", + s1: "", + kdlen: 32, + output: "bbfb3912ffc0d1789be7c3c2773fb6abd8df69578df2ca16beee3d0f7a9692d1", + }, + { + key: "38f9a331c022f51d66658f301837108c9710d5ee0697bfac97bb6b0ea8d4f273", + s1: "", + kdlen: 64, + output: "bbfb3912ffc0d1789be7c3c2773fb6abd8df69578df2ca16beee3d0f7a9692d1eae543288220e41452942fe268297fff38423b65b19cf7c6263aa611a4190741", + }, + { + key: "38f9a331c022f51d66658f301837108c9710d5ee0697bfac97bb6b0ea8d4f273", + s1: "", + kdlen: 242, + output: "bbfb3912ffc0d1789be7c3c2773fb6abd8df69578df2ca16beee3d0f7a9692d1eae543288220e41452942fe268297fff38423b65b19cf7c6263aa611a41907410a49acd5adbfbe93e902349105d7bd7ef5b106d9357b20bb4a7977d548bc2bf1a0b275a9f1de19ff8f963ec58171aa31da964edb0131436d88a7714e2429d85693409f718c8fea7ecaa076dce68f282aba4010a42feedcb1affc350497fa1078ad89e23e8a04ba2ef179c3c625b054817d792076b5882e80925d45a2874285d45a7767fa6a3853bd8417930923a554743f6eb4691e4790a97c467337a307c64a8bed5b5c1829d0a690a554d3e5334ee4980a", + }, + { + key: "38f9a331c022f51d66658f301837108c9710d5ee0697bfac97bb6b0ea8d4f273", + s1: "343434", + kdlen: 242, + output: "71e6fe05a6a57e2312a996eb3b91fc613fb57196ea09880b7b0ed880afa399f942ad56c1a484f4ccd329f9c21911ae09b497e991d8f47060114c8f137ab65df10c3f1d2478a7af913aa406817f34ef55d7852f3390f8201056451b0c29fb73a5c9e5a5093e6d93fefa15846b81bc02e1a7033948d67fc70c1dbbbf11d97f34b15d9967709c666eff05d895cc2f415064b382a98e68c178f9e7e2d9b6169ef4cdd5bb855b21d0ff71339e7e5b3afcb2393a8c6f4c7a4e618094222be87cbfdd2417ebdced5870bfbe8761d2e646c5a62dd31852dba399507adb84334a81b4ce54714f1828a0cdf3067908815d516368c2dd58", + }, +} + +func TestKDF(t *testing.T) { + for _, test := range kdftests { + z, _ := hex.DecodeString(test.key) + s1, _ := hex.DecodeString(test.s1) + h := sha256.New() + k, err := ConcatKDF(h, z, s1, test.kdlen) + if err != nil { + t.Error(err) + } + if len(k) != test.kdlen { + t.Errorf("output length mismatch: got %d, want %d", len(k), test.kdlen) + } + if hexk := hex.EncodeToString(k); hexk != test.output { + t.Errorf("output mismatch:\ngot %s\nwant %s", hexk, test.output) + } + if t.Failed() { + t.Fatalf("failed test: %#v", test) + } } } From 3417894e4ed6d8952df0c171bb4b00d77fb0dba7 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Sat, 19 Sep 2015 02:24:07 +0200 Subject: [PATCH 3/6] p2p/rlpx: new package --- p2p/rlpx/bufsema.go | 95 +++++++ p2p/rlpx/bufsema_test.go | 95 +++++++ p2p/rlpx/framing.go | 483 ++++++++++++++++++++++++++++++++++ p2p/rlpx/framing_test.go | 156 +++++++++++ p2p/rlpx/handshake.go | 339 ++++++++++++++++++++++++ p2p/rlpx/handshake_test.go | 255 ++++++++++++++++++ p2p/rlpx/handshake_tv_test.go | 254 ++++++++++++++++++ p2p/rlpx/rlpx.go | 408 ++++++++++++++++++++++++++++ p2p/rlpx/rlpx_test.go | 186 +++++++++++++ 9 files changed, 2271 insertions(+) create mode 100644 p2p/rlpx/bufsema.go create mode 100644 p2p/rlpx/bufsema_test.go create mode 100644 p2p/rlpx/framing.go create mode 100644 p2p/rlpx/framing_test.go create mode 100644 p2p/rlpx/handshake.go create mode 100644 p2p/rlpx/handshake_test.go create mode 100644 p2p/rlpx/handshake_tv_test.go create mode 100644 p2p/rlpx/rlpx.go create mode 100644 p2p/rlpx/rlpx_test.go diff --git a/p2p/rlpx/bufsema.go b/p2p/rlpx/bufsema.go new file mode 100644 index 0000000000..65edbc7459 --- /dev/null +++ b/p2p/rlpx/bufsema.go @@ -0,0 +1,95 @@ +// 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 . + +package rlpx + +import ( + "errors" + "fmt" + "sync/atomic" + "time" +) + +var errAcquireTimeout = errors.New("acquisition timeout") + +// bufSema is a counting semaphore. +type bufSema struct { + val, cap, waiting uint32 + wakeup chan struct{} +} + +func newBufSema(cap uint32) *bufSema { + return &bufSema{cap: cap, val: cap, wakeup: make(chan struct{})} +} + +func (sem *bufSema) get() uint32 { + return atomic.LoadUint32(&sem.val) +} + +// release increments sem, potentially unblocking a call to +// waitAcquire if there is one. release never blocks. +func (sem *bufSema) release(n uint32) { + new := atomic.AddUint32(&sem.val, n) + if new > sem.cap { + panic(fmt.Sprintf("semaphore count %d exceeds cap after release(%d)", new, n)) + } + // Wake up a pending waitAcquire call if there is one. + if atomic.CompareAndSwapUint32(&sem.waiting, 1, 0) { + sem.wakeup <- struct{}{} + } +} + +// waitAcquire decrements the semaphore by n. If less than +// n units are available, waitAcquire blocks until release is called. +// It may only be called from one goroutine at a time. +func (sem *bufSema) waitAcquire(n uint32, timeout time.Duration) error { + if n > sem.cap { + return fmt.Errorf("requested amount %d exceeds semaphore cap of %d", n, sem.cap) + } + var timer *time.Timer + for { + // Set the waiting flag so release will try to wake us after + // incrementing sem.val. + if !atomic.CompareAndSwapUint32(&sem.waiting, 0, 1) { + panic("concurrent call to waitAcquire") + } + // Decrement if sem.val if possible. + if atomic.LoadUint32(&sem.val) >= n { + atomic.AddUint32(&sem.val, ^(n - 1)) + // Gobble up wakeup signal in case release decremented sem.waiting. + if !atomic.CompareAndSwapUint32(&sem.waiting, 1, 0) { + <-sem.wakeup + } + return nil + } + // Start the timeout on the first iteration. + if timer == nil { + timer = time.NewTimer(timeout) + defer timer.Stop() + } + select { + case <-sem.wakeup: + // Woken by release. It has decremented sem.waiting back to zero. + case <-timer.C: + // Gobble up wakeup signal in case release decremented sem.waiting. + if !atomic.CompareAndSwapUint32(&sem.waiting, 1, 0) { + <-sem.wakeup + } + return errAcquireTimeout + } + } + return nil +} diff --git a/p2p/rlpx/bufsema_test.go b/p2p/rlpx/bufsema_test.go new file mode 100644 index 0000000000..9c42e2b0c4 --- /dev/null +++ b/p2p/rlpx/bufsema_test.go @@ -0,0 +1,95 @@ +// 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 . + +package rlpx + +import ( + "errors" + "math/rand" + "reflect" + "testing" + "time" +) + +func TestBufSemaCountSimple(t *testing.T) { + sem := newBufSema(2000) + + checkacquire := func(count, wantCount uint32, wantErr error) { + err := sem.waitAcquire(count, 10*time.Millisecond) + if !reflect.DeepEqual(err, wantErr) { + t.Fatalf("wrong error after acquire(%d): got %q, want %q", count, err, wantErr) + } + if val := sem.get(); val != wantCount { + t.Fatalf("wrong count after acquire(%d): got %d, want %d", count, val, wantCount) + } + } + checkrelease := func(count, wantCount uint32) { + sem.release(count) + if val := sem.get(); val != wantCount { + t.Fatalf("wrong count after release(%d): got %d, want %d", count, val, wantCount) + } + } + + // Check that the counter is maintained correctly. + checkacquire(1000, 1000, nil) + checkacquire(1000, 0, nil) + checkacquire(1000, 0, errAcquireTimeout) + checkrelease(900, 900) + checkrelease(900, 1800) + checkrelease(199, 1999) + checkrelease(1, 2000) + + // Check that requesting more than sem.cap fails. + checkacquire(2001, 2000, errors.New("requested amount 2001 exceeds semaphore cap of 2000")) + + // Check that a failed waitAcquire leaves sem.val as is when it is < sem.cap. + checkacquire(500, 1500, nil) + checkrelease(200, 1700) + checkacquire(2000, 1700, errAcquireTimeout) +} + +// This test checks that release wakes up waitAcquire. +func TestBufSemaRace(t *testing.T) { + const ( + waitCount = 10000 + iterations = 5000 + ) + sem := newBufSema(waitCount) + pleaserelease := make(chan uint32, 500) + releaser := func() { + for rv := range pleaserelease { + sem.release(rv) + } + } + defer close(pleaserelease) + go releaser() + go releaser() + go releaser() + + for i := 0; i < iterations; i++ { + if err := sem.waitAcquire(waitCount, 1*time.Second); err != nil { + t.Fatalf("iteration %d: %v", i, err) + } + for i := uint32(0); i < waitCount; { + rv := rand.Uint32() % waitCount + if i+rv > waitCount { + rv = waitCount - i + } + i += rv + pleaserelease <- rv + } + } +} diff --git a/p2p/rlpx/framing.go b/p2p/rlpx/framing.go new file mode 100644 index 0000000000..7abd7e75bb --- /dev/null +++ b/p2p/rlpx/framing.go @@ -0,0 +1,483 @@ +// 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 . + +package rlpx + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "errors" + "fmt" + "hash" + "io" + "sync" + "time" + + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + staticFrameSize uint32 = 8 * 1024 + frameHeaderSize = 16 // encoded header + frameHeaderFullSize = 32 // encoded header + MAC +) + +var ( + errProtocolClaimTimeout = errors.New("protocol for pending message was not claimed in time") + errUnexpectedChunkStart = errors.New("received chunk start header for existing transfer") + errChunkTooLarge = errors.New("chunk size larger than remaining message size") +) + +// readLoop runs in its own goroutine for each connection, +// dispatching frames to protocols. +func readLoop(c *Conn) (err error) { + defer func() { + // When the loop ends, forward the error to all protocols so + // their next ReadPacket fails. Active chunked transfers also + // need to cancel immediately so shutdown is not delayed. + c.mu.Lock() + for _, p := range c.proto { + p.readClose(err) + for _, pr := range p.xfers { + pr.close(err) + } + } + c.readErr = err + c.mu.Unlock() + }() + + // Local cache of claimed protocols. + protos := make(map[uint16]*Protocol) + + for { + // Read the next frame header. + c.fd.SetReadDeadline(time.Now().Add(c.cfg.readIdleTimeout())) + fsize, hdr, err := c.rw.readFrameHeader() + if err != nil { + return err + } + // Grab the protocol, checking the local cache before + // interacting with the claims machinery in Conn. + proto := protos[hdr.protocol] + if proto == nil { + if proto = c.waitForProtocol(hdr.protocol); proto == nil { + return errProtocolClaimTimeout + } + protos[proto.id] = proto + } + // Wait until there is enough buffer space for the body + // before reading it. + err = proto.readBufSema.waitAcquire(fsize, c.cfg.readBufferWaitTimeout()) + if err != nil { + return err + } + // Read the body of the frame. + c.fd.SetReadDeadline(time.Now().Add(c.cfg.readTimeout())) + body, err := c.rw.readFrameBody(fsize) + if err != nil { + return err + } + // Dispatch the frame to the protocol. + // This shouldn't block. + if pr := proto.xfers[hdr.contextID]; pr != nil { + if hdr.chunkStart { + return errUnexpectedChunkStart + } + end, err := pr.feed(body) + if end { + delete(proto.xfers, hdr.contextID) + } + if err != nil { + return err + } + } else { + pr, err := frameToPacket(proto, hdr, body) + if err != nil { + return err + } + if pr.bufN > 0 { + // Track as ongoing transfer if there is still something + // to buffer after the initial frame. + proto.xfers[hdr.contextID] = pr + } + proto.feedPacket(pr) + } + } +} + +// frameToPacket handles the initial frame for a new packet. +func frameToPacket(proto *Protocol, hdr frameHeader, frame frameBuffer) (pr *packetReader, err error) { + if hdr.chunkStart { + if uint32(len(frame)) > hdr.totalSize { + return nil, fmt.Errorf("initial chunk size %d larger than total size %d", len(frame), hdr.totalSize) + } + if uint32(len(frame)) < hdr.totalSize { + return newPacketReader(proto.readBufSema, hdr.totalSize, frame), nil + } + } + return newPacketReader(proto.readBufSema, uint32(len(frame)), frame), nil +} + +// packetReader is the payload of a packet. +// frames are appended to it as they are read from the connection. +type packetReader struct { + // all of these can be accessed without locking + // because Read is not safe for concurrent use. + readBufs []frameBuffer + origBufs []frameBuffer + bufSema *bufSema + readN uint32 // how much can still be read + + // these fields are protected by cond.L + cond *sync.Cond // wakes waitFrame + newBufs []frameBuffer // buffer inbox + err error // error inbox + bufN uint32 // how much still needs to be buffered +} + +func newPacketReader(bsem *bufSema, psize uint32, initialFrame frameBuffer) *packetReader { + pr := &packetReader{ + bufSema: bsem, + cond: sync.NewCond(new(sync.Mutex)), + readN: psize, + bufN: psize, + } + if len(initialFrame) > 0 { + pr.bufN -= uint32(len(initialFrame)) + pr.readBufs = []frameBuffer{initialFrame} + pr.origBufs = []frameBuffer{initialFrame} + } + return pr +} + +func (pr *packetReader) Read(rslice []byte) (int, error) { + if err := pr.waitFrame(); err != nil { + return 0, err + } + n := 0 + for i := 0; i < len(pr.readBufs) && n < len(rslice); i++ { + nn, _ := pr.readBufs[i].Read(rslice[n:]) + n += nn + } + pr.afterRead(n) + return n, nil +} + +func (pr *packetReader) ReadByte() (byte, error) { + if err := pr.waitFrame(); err != nil { + return 0, err + } + b, _ := pr.readBufs[0].ReadByte() + pr.afterRead(1) + return b, nil +} + +// blocks until at least one frame is available, +// then transfers any new frame buffers that have appeared +// to readBufs/origBufs. +func (pr *packetReader) waitFrame() error { + if len(pr.readBufs) > 0 { + return nil + } + if pr.readN == 0 { + return io.EOF + } + pr.cond.L.Lock() + defer pr.cond.L.Unlock() + for len(pr.newBufs) == 0 && pr.err == nil { + pr.cond.Wait() + } + pr.readBufs = append(pr.readBufs, pr.newBufs...) + pr.origBufs = append(pr.origBufs, pr.newBufs...) + pr.newBufs = pr.newBufs[:0] + return pr.err +} + +// removes drained buffers and decrements the read buffer semaphore. +func (pr *packetReader) afterRead(n int) { + pr.readN -= uint32(n) + drained := 0 + drainedLen := uint32(0) + for i, buf := range pr.readBufs { + if len(buf) != 0 { + break + } + drained++ + drainedLen += uint32(len(pr.origBufs[i])) + } + if drained > 0 { + pr.readBufs = pr.readBufs[:copy(pr.readBufs, pr.readBufs[drained:])] + pr.origBufs = pr.origBufs[:copy(pr.origBufs, pr.origBufs[drained:])] + pr.bufSema.release(drainedLen) + } +} + +func (pr *packetReader) close(err error) { + pr.cond.L.Lock() + pr.err = err + pr.cond.Signal() // wake up waitFrame + pr.cond.L.Unlock() +} + +func (pr *packetReader) feed(frame frameBuffer) (end bool, err error) { + pr.cond.L.Lock() + defer pr.cond.L.Unlock() + if uint32(len(frame)) > pr.bufN { + pr.err = errChunkTooLarge + end = true + } else { + pr.bufN -= uint32(len(frame)) + pr.newBufs = append(pr.newBufs, frame) + end = pr.bufN == 0 + } + pr.cond.Signal() // wake up waitFrame + return end, pr.err +} + +// represents a frame header that has been read. +type frameHeader struct { + protocol, contextID uint16 + chunkStart bool // initial frame of chunked message + totalSize uint32 // total number of bytes of chunked message +} + +// header types for sending +type chunkStartHeader struct { + Protocol, ContextID uint16 + TotalSize uint32 +} +type regularHeader struct { + Protocol, ContextID uint16 +} + +func decodeHeader(b []byte) (fsize uint32, h frameHeader, err error) { + fsize = readInt24(b) + if fsize == 0 { + return 0, h, errors.New("zero-sized frame") + } + b = b[3:] + lc, rest, err := rlp.SplitList(b) + if err != nil { + return fsize, h, err + } + // This is silly. rlp.DecodeBytes errors for data + // after the value, so we need to pass a slice + // containing just the value. + hlist := b[:len(b)-len(rest)] + + switch cnt, _ := rlp.CountValues(lc); cnt { + case 1: + var in struct{ Protocol uint16 } + err = rlp.DecodeBytes(hlist, &in) + h.protocol = in.Protocol + case 2: + var in regularHeader + err = rlp.DecodeBytes(hlist, &in) + h.protocol = in.Protocol + h.contextID = in.ContextID + case 3: + var in chunkStartHeader + err = rlp.DecodeBytes(hlist, &in) + h.protocol = in.Protocol + h.contextID = in.ContextID + h.totalSize = in.TotalSize + h.chunkStart = true + default: + err = fmt.Errorf("too many list elements") + } + return fsize, h, err +} + +// frameRW implements the framed wire protocol. +type frameRW struct { + conn io.ReadWriter + // for reading + headbuf []byte + dec cipher.Stream + ingressMacCipher cipher.Block + ingressMac hash.Hash + // for writing + enc cipher.Stream + egressMacCipher cipher.Block + egressMac hash.Hash +} + +func newFrameRW(conn io.ReadWriter, ingress, egress secrets) *frameRW { + return &frameRW{ + conn: conn, + headbuf: make([]byte, 32), + enc: cipher.NewCTR(mustBlockCipher("egress.encKey", egress.encKey), egress.encIV), + egressMacCipher: mustBlockCipher("egress.macKey", egress.macKey), + egressMac: egress.mac, + dec: cipher.NewCTR(mustBlockCipher("ingress.encKey", ingress.encKey), ingress.encIV), + ingressMacCipher: mustBlockCipher("ingress.macKey", ingress.macKey), + ingressMac: ingress.mac, + } +} + +func mustBlockCipher(what string, key []byte) cipher.Block { + c, err := aes.NewCipher(key) + if err != nil { + panic(fmt.Sprintf("invalid %s: %v", what, err)) + } + return c +} + +// sends a frame on the connection. the body buffer must placeholder bytes +// for the encoded frame header and its MAC. +func (rw *frameRW) sendFrame(hdr interface{}, body *frameBuffer) error { + wbuf := *body + usize := uint32(len(wbuf)) + if usize < frameHeaderFullSize { + panic(fmt.Sprintf("invalid body buffer, size < %d", frameHeaderFullSize)) + } + if usize-frameHeaderFullSize > maxUint24 { + return errors.New("frame size overflows uint24") + } + + // Write and encrypt the frame header to the buffer. + headbuf := wbuf[:frameHeaderSize] + putInt24(headbuf, usize-frameHeaderFullSize) + headbufAfterSize := headbuf[3:3] + rlp.Encode(&headbufAfterSize, hdr) + rw.enc.XORKeyStream(headbuf, headbuf) + copy(wbuf[frameHeaderSize:], updateMAC(rw.egressMac, rw.egressMacCipher, headbuf)) + + // Write and encrypt frame data to the buffer. + wbuf.pad16() + rw.enc.XORKeyStream(wbuf[frameHeaderFullSize:], wbuf[frameHeaderFullSize:]) + rw.egressMac.Write(wbuf[frameHeaderFullSize:]) + fmacseed := rw.egressMac.Sum(nil) + wbuf = append(wbuf, zero[:frameHeaderSize]...) + copy(wbuf[len(wbuf)-16:], updateMAC(rw.egressMac, rw.egressMacCipher, fmacseed)) + + // Send the whole buffered frame on the socket. + _, err := rw.conn.Write(wbuf) + *body = wbuf + return err +} + +func (rw *frameRW) readFrameHeader() (fsize uint32, hdr frameHeader, err error) { + // Read the header and verify its MAC. + if _, err := io.ReadFull(rw.conn, rw.headbuf); err != nil { + return 0, hdr, err + } + shouldMAC := updateMAC(rw.ingressMac, rw.ingressMacCipher, rw.headbuf[:16]) + if !hmac.Equal(shouldMAC, rw.headbuf[16:]) { + return 0, hdr, errors.New("bad header MAC") + } + rw.dec.XORKeyStream(rw.headbuf[:16], rw.headbuf[:16]) + + // Parse the header. + fsize, hdr, err = decodeHeader(rw.headbuf) + if err != nil { + err = fmt.Errorf("can't decode frame header: %v", err) + } + return fsize, hdr, err +} + +func (rw *frameRW) readFrameBody(fsize uint32) (frameBuffer, error) { + // Grab a buffer for the content. + var rsize = fsize + if padding := fsize % 16; padding > 0 { + rsize += 16 - padding // frame size rounded up to 16 byte boundary + } + fb := makeFrameReadBuffer(rsize + 16) + if _, err := io.ReadFull(rw.conn, fb); err != nil { + return nil, err + } + + // Verify the body MAC and decrypt the content. + mac, bb := fb[len(fb)-16:], fb[:len(fb)-16] + rw.ingressMac.Write(bb) + fmacseed := rw.ingressMac.Sum(nil) + shouldMAC := updateMAC(rw.ingressMac, rw.ingressMacCipher, fmacseed) + if !hmac.Equal(shouldMAC, mac) { + return nil, errors.New("bad frame body MAC") + } + rw.dec.XORKeyStream(bb, bb) + return bb[:fsize], nil +} + +// updateMAC reseeds the given hash with encrypted seed. +// it returns the first 16 bytes of the hash sum after seeding. +func updateMAC(mac hash.Hash, block cipher.Block, seed []byte) []byte { + aesbuf := make([]byte, aes.BlockSize) + block.Encrypt(aesbuf, mac.Sum(aesbuf[:0])) + for i := range aesbuf { + aesbuf[i] ^= seed[i] + } + mac.Write(aesbuf) + return mac.Sum(nil)[:16] +} + +type frameBuffer []byte + +func makeFrameWriteBuffer() *frameBuffer { + buf := make(frameBuffer, frameHeaderFullSize, frameHeaderFullSize+staticFrameSize) + return &buf +} + +func makeFrameReadBuffer(size uint32) frameBuffer { + return make(frameBuffer, size) +} + +// resetForWrite truncates the buffer so it contains just enough space +// for an encoded frame header. it must be called before writing +// payload content for a new frame. +func (buf *frameBuffer) resetForWrite() { + *buf = append((*buf)[:0], zero[:frameHeaderFullSize]...) +} + +func (buf *frameBuffer) Write(s []byte) (n int, err error) { + *buf = append(*buf, s...) + return len(s), nil +} + +func (buf *frameBuffer) Read(s []byte) (int, error) { + if buf == nil || len(*buf) == 0 { + return 0, io.EOF + } + n := copy(s, *buf) + *buf = (*buf)[n:] + return n, nil +} + +func (buf *frameBuffer) ReadByte() (byte, error) { + if buf == nil || len(*buf) == 0 { + return 0, io.EOF + } + b := (*buf)[0] + *buf = (*buf)[1:] + return b, nil +} + +func (buf *frameBuffer) pad16() { + if padding := len(*buf) % 16; padding > 0 { + *buf = append(*buf, zero[:16-padding]...) + } +} + +func readInt24(b []byte) uint32 { + return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16 +} + +func putInt24(s []byte, v uint32) { + s[0] = byte(v >> 16) + s[1] = byte(v >> 8) + s[2] = byte(v) +} diff --git a/p2p/rlpx/framing_test.go b/p2p/rlpx/framing_test.go new file mode 100644 index 0000000000..bfbcf02627 --- /dev/null +++ b/p2p/rlpx/framing_test.go @@ -0,0 +1,156 @@ +// 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 . + +package rlpx + +import ( + "crypto/ecdsa" + "encoding/hex" + "fmt" + "io" + "math/rand" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/crypto" +) + +func TestPacketReader(t *testing.T) { + feed := func(pr *packetReader, n uint32) { + for sent := uint32(0); sent < n; { + chunk := randomChunk(sent, n-sent) + sent += uint32(len(chunk)) + if err := pr.bufSema.waitAcquire(uint32(len(chunk)), 200*time.Millisecond); err != nil { + panic(err.Error()) + } + end, err := pr.feed(chunk) + if err != nil { + panic(fmt.Errorf("pr.feed returned error: %v", err)) + } + if end && sent != n { + panic(fmt.Errorf("pr.feed returned end=true with %d/%d bytes of input", sent, n)) + } + } + } + for size := uint32(1); size < 2<<17; size *= 2 { + sem := newBufSema(staticFrameSize * 2) + pr := newPacketReader(sem, size, nil) + go feed(pr, size) + if err := checkSeq(pr, size); err != nil { + t.Fatalf("size %d: read error: %v", size, err) + } + if val := sem.get(); val != staticFrameSize*2 { + t.Fatalf("size %d: wrong semaphore value after reading all data. got %d, want %d", size, val, staticFrameSize*2) + } + } +} + +func checkSeq(r io.Reader, size uint32) error { + content := make([]byte, size) + if _, err := io.ReadFull(r, content); err != nil { + return err + } + for i, b := range content { + if b != byte(i) { + return fmt.Errorf("mismatch at index %d: have %d, want %d", i, b, byte(i)) + } + } + return nil +} + +func randomChunk(seed uint32, maxSize uint32) frameBuffer { + size := rand.Uint32()%staticFrameSize + 1 + if size > maxSize { + size = maxSize + } + chunk := make(frameBuffer, size) + for i := range chunk { + chunk[i] = byte(uint32(i) + seed) + } + return chunk +} + +/* +func TestFrameFakeGolden(t *testing.T) { + buf := new(bytes.Buffer) + hash := fakeHash{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + rw := newFrameRW(buf, secrets{ + AES: crypto.Sha3(), + MAC: crypto.Sha3(), + IngressMAC: hash, + EgressMAC: hash, + }) + + golden := hexb(` +00828ddae471818bb0bfa6b551d1cb42 +01010101010101010101010101010101 +ba628a4ba590cb43f7848f41c4382885 +01010101010101010101010101010101 +`) + body := hexb(`08C401020304`) + + // Check sendFrame. This encodes the frame to buf. + fwbuf := makeFrameWriteBuffer() + fwbuf.Write(body) + if err := rw.sendFrame(regularHeader{0, 0}, fwbuf); err != nil { + t.Fatalf("sendFrame error: %v", err) + } + written := buf.Bytes() + if !bytes.Equal(written, golden) { + t.Fatalf("output mismatch:\n got: %x\n want: %x", written, golden) + } + + // Check readFrame. It reads the message encoded by sendFrame, which + // must be equivalent to the golden message above. + fsize, hdr, err := rw.readFrameHeader() + if err != nil { + t.Fatalf("readFrameHeader error: %v", err) + } + if (hdr != frameHeader{}) { + t.Errorf("read header mismatch: got %v, want zero header", hdr) + } + if int(fsize) != len(body) { + t.Errorf("read size mismatch: got %d, want %d", fsize, len(body)) + } + // if !bytes.Equal(bodybuf.Bytes(), body) { + // t.Errorf("read body mismatch:\ngot %x\nwant %x", bodybuf.Bytes(), body) + // } +} + +type fakeHash []byte + +func (fakeHash) Write(p []byte) (int, error) { return len(p), nil } +func (fakeHash) Reset() {} +func (fakeHash) BlockSize() int { return 0 } + +func (h fakeHash) Size() int { return len(h) } +func (h fakeHash) Sum(b []byte) []byte { return append(b, h...) } + +*/ + +func hexb(str string) []byte { + unspace := strings.NewReplacer("\n", "", "\t", "", " ", "") + b, err := hex.DecodeString(unspace.Replace(str)) + if err != nil { + panic(fmt.Sprintf("invalid hex string: %q", str)) + } + return b +} + +func hexkey(str string) *ecdsa.PrivateKey { + return crypto.ToECDSA(hexb(str)) +} diff --git a/p2p/rlpx/handshake.go b/p2p/rlpx/handshake.go new file mode 100644 index 0000000000..7b5328a947 --- /dev/null +++ b/p2p/rlpx/handshake.go @@ -0,0 +1,339 @@ +// 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 . + +package rlpx + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/binary" + "fmt" + "hash" + "io" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/ethereum/go-ethereum/crypto/sha3" +) + +const ( + maxUint24 = ^uint32(0) >> 8 + kdfSharedDataPrefix = "rlpx handshake\x05" + + // Handshake Sizes + sskLen = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 + sigLen = 65 // elliptic S256 + pubLen = 64 // 512 bit pubkey in uncompressed representation without format byte + shaLen = 32 // hash length (for nonce etc) + nonceLen = 24 + authMsgLen = sigLen + shaLen + pubLen + shaLen + 1 + authRespLen = pubLen + shaLen + 1 + eciesBytes = 65 + 16 + 32 + encAuthMsgLen = authMsgLen + eciesBytes // size of the final ECIES payload sent as initiator's handshake + encAuthRespLen = authRespLen + eciesBytes // size of the final ECIES payload sent as receiver's handshake +) + +var zero [32]byte + +// encHandshake contains the state of the encryption handshake. +type handshake struct { + conn io.ReadWriter + initiator bool + localPrivKey *ecdsa.PrivateKey + remotePub *ecies.PublicKey // remote-pubk + initNonce, respNonce []byte // nonce + randomPrivKey *ecies.PrivateKey // ecdhe-random + remoteRandomPub *ecies.PublicKey // ecdhe-random-pubk +} + +// secrets represents the derived secrets for authenticated encryption. +type secrets struct { + encKey, encIV, macKey []byte + mac hash.Hash +} + +type handshakeRandSource interface { + generateNonce(b []byte) error + generateKey() (*ecies.PrivateKey, error) +} + +type realRandSource struct{} + +func (realRandSource) generateNonce(b []byte) error { + _, err := io.ReadFull(rand.Reader, b) + return err +} + +func (realRandSource) generateKey() (*ecies.PrivateKey, error) { + return ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil) +} + +func (h *handshake) deriveSecrets(forceV4 bool, auth, authResp []byte) (vsn uint, ingress, egress secrets, err error) { + remoteNonce := h.initNonce + if h.initiator { + remoteNonce = h.respNonce + } + remoteVersion := binary.BigEndian.Uint64(remoteNonce[nonceLen:]) + if forceV4 || remoteVersion > 255 { + return h.deriveSecretsV4(auth, authResp) + } + return h.deriveSecretsV5() +} + +func (h *handshake) deriveSecretsV4(auth, authResp []byte) (vsn uint, ingress, egress secrets, err error) { + vsn = 4 + ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen) + if err != nil { + return vsn, ingress, egress, err + } + sharedSecret := crypto.Sha3(ecdheSecret, crypto.Sha3(h.respNonce, h.initNonce)) + aesSecret := crypto.Sha3(ecdheSecret, sharedSecret) + macSecret := crypto.Sha3(ecdheSecret, aesSecret) + + egress = secrets{encKey: aesSecret, encIV: zero[:16], macKey: macSecret} + egress.mac = sha3.NewKeccak256() + egress.mac.Write(xor(h.initNonce, macSecret)) + egress.mac.Write(authResp) + ingress = secrets{encKey: aesSecret, encIV: zero[:16], macKey: macSecret} + ingress.mac = sha3.NewKeccak256() + ingress.mac.Write(xor(h.respNonce, macSecret)) + ingress.mac.Write(auth) + if h.initiator { + ingress, egress = egress, ingress + } + return vsn, ingress, egress, nil +} + +func (h *handshake) deriveSecretsV5() (vsn uint, ingress, egress secrets, err error) { + vsn = 5 + ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen) + if err != nil { + return vsn, ingress, egress, err + } + initPub := exportPubkey(h.remotePub) + respPub := elliptic.Marshal(h.localPrivKey.Curve, h.localPrivKey.X, h.localPrivKey.Y)[1:] + if h.initiator { + initPub, respPub = respPub, initPub + } + sharedData := make([]byte, len(kdfSharedDataPrefix)+nonceLen*2+pubLen*2) + n := copy(sharedData, kdfSharedDataPrefix) + n += copy(sharedData[n:], h.initNonce[:nonceLen]) + n += copy(sharedData[n:], h.respNonce[:nonceLen]) + n += copy(sharedData[n:], initPub) + n += copy(sharedData[n:], respPub) + derived, err := ecies.ConcatKDF(sha3.NewKeccak256(), ecdheSecret, sharedData, 160) + if err != nil { + return vsn, ingress, egress, err + } + + ingress = secrets{encKey: derived[0:32], encIV: derived[64:80], macKey: derived[96:128]} + ingress.mac = sha3.NewKeccak256() + ingress.mac.Write(ingress.macKey) + egress = secrets{encKey: derived[32:64], encIV: derived[80:96], macKey: derived[128:160]} + egress.mac = sha3.NewKeccak256() + egress.mac.Write(egress.macKey) + if h.initiator { + ingress, egress = egress, ingress + } + return vsn, ingress, egress, nil +} + +func (h *handshake) ecdhShared(prv *ecdsa.PrivateKey) ([]byte, error) { + return ecies.ImportECDSA(prv).GenerateShared(h.remotePub, sskLen, sskLen) +} + +func (c *Conn) fillHandshake(nonce *[]byte, key **ecies.PrivateKey) (err error) { + *nonce = make([]byte, shaLen) + if c.cfg.ForceV4 { + err = c.handshakeRand.generateNonce(*nonce) + } else { + binary.BigEndian.PutUint64((*nonce)[nonceLen:], 5) + err = c.handshakeRand.generateNonce((*nonce)[:nonceLen]) + } + if err != nil { + return err + } + *key, err = c.handshakeRand.generateKey() + return err +} + +// initiatorHandshake negotiates connection secrets on conn. +// it should be called on the dialing end of the connection. +// prv is the local client's private key. +func (c *Conn) initiatorHandshake() (vsn uint, ingress, egress secrets, err error) { + h := &handshake{initiator: true, localPrivKey: c.cfg.Key, remotePub: ecies.ImportECDSAPublic(c.remoteID)} + if err := c.fillHandshake(&h.initNonce, &h.randomPrivKey); err != nil { + return 0, ingress, egress, err + } + auth, err := h.authMsg() + if err != nil { + return 0, ingress, egress, err + } + if _, err := c.fd.Write(auth); err != nil { + return 0, ingress, egress, err + } + + response := make([]byte, encAuthRespLen) + if _, err := io.ReadFull(c.fd, response); err != nil { + return 0, ingress, egress, err + } + if err := h.decodeAuthResp(response); err != nil { + return 0, ingress, egress, err + } + return h.deriveSecrets(c.cfg.ForceV4, auth, response) +} + +// authMsg creates an encrypted initiator handshake message. +func (h *handshake) authMsg() ([]byte, error) { + staticSharedSecret, err := h.ecdhShared(h.localPrivKey) + if err != nil { + return nil, err + } + // sign static-shared-secret^nonce + signed := xor(staticSharedSecret, h.initNonce) + signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA()) + if err != nil { + return nil, err + } + // encode auth message: sig || sha3(ecdhe-random-pubk) || pubk || nonce || token-flag + msg := make([]byte, authMsgLen) + n := copy(msg, signature) + n += copy(msg[n:], crypto.Sha3(exportPubkey(&h.randomPrivKey.PublicKey))) + n += copy(msg[n:], crypto.FromECDSAPub(&h.localPrivKey.PublicKey)[1:]) + n += copy(msg[n:], h.initNonce) + msg[n] = 0 + // encrypt auth message using remote-pubk + return ecies.Encrypt(rand.Reader, h.remotePub, msg, nil, nil) +} + +// decodeAuthResp decode an encrypted authentication response message. +func (h *handshake) decodeAuthResp(auth []byte) error { + msg, err := crypto.Decrypt(h.localPrivKey, auth) + if err != nil { + return fmt.Errorf("could not decrypt auth response (%v)", err) + } + h.respNonce = msg[pubLen : pubLen+shaLen] + h.remoteRandomPub, err = importPublicKey(msg[:pubLen]) + if err != nil { + return err + } + return nil +} + +// recipientHandshake negotiates connection secrets on conn. +// it should be called on the listening side of the connection. +// prv is the local client's private key. +func (c *Conn) recipientHandshake() (vsn uint, remoteID *ecdsa.PublicKey, ingress, egress secrets, err error) { + auth := make([]byte, encAuthMsgLen) + if _, err := io.ReadFull(c.fd, auth); err != nil { + return 0, nil, ingress, egress, err + } + h := &handshake{localPrivKey: c.cfg.Key} + if err := h.decodeAuthMsg(auth); err != nil { + return 0, nil, ingress, egress, fmt.Errorf("invalid auth: %v", err) + } + if err := c.fillHandshake(&h.respNonce, &h.randomPrivKey); err != nil { + return 0, nil, ingress, egress, err + } + + resp, err := h.authResp() + if err != nil { + return 0, nil, ingress, egress, fmt.Errorf("can't create auth resp: %v", err) + } + if _, err := c.fd.Write(resp); err != nil { + return 0, nil, ingress, egress, err + } + vsn, ingress, egress, err = h.deriveSecrets(c.cfg.ForceV4, auth, resp) + if h.remotePub != nil { + remoteID = h.remotePub.ExportECDSA() + } + return vsn, remoteID, ingress, egress, err +} + +func (h *handshake) decodeAuthMsg(auth []byte) error { + msg, err := crypto.Decrypt(h.localPrivKey, auth) + if err != nil { + return err + } + // signature || sha3(ecdhe-random-pubk) || pubk || nonce || token-flag + h.initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1] + h.remotePub, err = importPublicKey(msg[sigLen+shaLen : sigLen+shaLen+pubLen]) + if err != nil { + return fmt.Errorf("invalid remote identity: %v", err) + } + // recover remote random pubkey from signed message. + staticSharedSecret, err := h.ecdhShared(h.localPrivKey) + if err != nil { + return err + } + signed := xor(staticSharedSecret, h.initNonce) + remoteRandomPub, err := secp256k1.RecoverPubkey(signed, msg[:sigLen]) + if err != nil { + return err + } + // validate the sha3 of recovered pubkey + remoteRandomPubMAC := msg[sigLen : sigLen+shaLen] + shaRemoteRandomPub := crypto.Sha3(remoteRandomPub[1:]) + if !bytes.Equal(remoteRandomPubMAC, shaRemoteRandomPub) { + return fmt.Errorf("recovered pubkey hash mismatch") + } + h.remoteRandomPub, _ = importPublicKey(remoteRandomPub) + return nil +} + +// authResp generates the encrypted authentication response message. +func (h *handshake) authResp() ([]byte, error) { + // E(remote-pubk, ecdhe-random-pubk || nonce || token-flag) + resp := make([]byte, authRespLen) + n := copy(resp, exportPubkey(&h.randomPrivKey.PublicKey)) + n += copy(resp[n:], h.respNonce) + resp[n] = 0 + return ecies.Encrypt(rand.Reader, h.remotePub, resp, nil, nil) +} + +// importPublicKey unmarshals 512 bit public keys. +func importPublicKey(pubKey []byte) (*ecies.PublicKey, error) { + var pubKey65 []byte + switch len(pubKey) { + case 64: + // add 'uncompressed key' flag + pubKey65 = append([]byte{0x04}, pubKey...) + case 65: + pubKey65 = pubKey + default: + return nil, fmt.Errorf("invalid public key length %v (expect 64/65)", len(pubKey)) + } + // TODO: fewer pointless conversions + return ecies.ImportECDSAPublic(crypto.ToECDSAPub(pubKey65)), nil +} + +func exportPubkey(pub *ecies.PublicKey) []byte { + if pub == nil { + panic("nil pubkey") + } + return elliptic.Marshal(pub.Curve, pub.X, pub.Y)[1:] +} + +func xor(one, other []byte) (xor []byte) { + xor = make([]byte, len(one)) + for i := 0; i < len(one); i++ { + xor[i] = one[i] ^ other[i] + } + return xor +} diff --git a/p2p/rlpx/handshake_test.go b/p2p/rlpx/handshake_test.go new file mode 100644 index 0000000000..560f4c0500 --- /dev/null +++ b/p2p/rlpx/handshake_test.go @@ -0,0 +1,255 @@ +// 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 . + +package rlpx + +import ( + "bytes" + "crypto/ecdsa" + "fmt" + "io/ioutil" + "net" + "reflect" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" +) + +func init() { + spew.Config.Indent = "\t" +} + +func TestSharedSecret(t *testing.T) { + prv0, _ := crypto.GenerateKey() + pub0 := &prv0.PublicKey + prv1, _ := crypto.GenerateKey() + pub1 := &prv1.PublicKey + ss0, err := ecies.ImportECDSA(prv0).GenerateShared(ecies.ImportECDSAPublic(pub1), sskLen, sskLen) + if err != nil { + return + } + ss1, err := ecies.ImportECDSA(prv1).GenerateShared(ecies.ImportECDSAPublic(pub0), sskLen, sskLen) + if err != nil { + return + } + if !bytes.Equal(ss0, ss1) { + t.Errorf("secret mismatch") + } +} + +// This test does random V5 handshakes and compares the secrets. +func TestHandshake(t *testing.T) { + for i := 0; i < 10 && !t.Failed(); i++ { + start := time.Now() + doTestHandshake(t) + t.Logf("%d %v\n", i+1, time.Since(start)) + } +} + +func doTestHandshake(t *testing.T) { + var ( + prv0, _ = crypto.GenerateKey() + prv1, _ = crypto.GenerateKey() + p1, p2 = net.Pipe() + c1 = Server(p1, &Config{Key: prv0}) + c2 = Client(p2, &prv0.PublicKey, &Config{Key: prv1}) + ) + shake := func(conn *Conn, rkey *ecdsa.PublicKey) error { + defer conn.Close() + if err := conn.Handshake(); err != nil { + return err + } + if !reflect.DeepEqual(conn.RemoteID(), rkey) { + return fmt.Errorf("remote ID mismatch: got %v, want: %v", conn.RemoteID(), rkey) + } + return nil + } + run(t, rig{ + "initiator": func() error { return shake(c1, &prv1.PublicKey) }, + "recipient": func() error { return shake(c2, &prv0.PublicKey) }, + }) + + // compare derived secrets + if !reflect.DeepEqual(c1.rw.egressMac, c2.rw.ingressMac) { + t.Errorf("egress mac mismatch:\n c1.rw: %#v\n c2.rw: %#v", c1.rw.egressMac, c2.rw.ingressMac) + } + if !reflect.DeepEqual(c1.rw.ingressMac, c2.rw.egressMac) { + t.Errorf("ingress mac mismatch:\n c1.rw: %#v\n c2.rw: %#v", c1.rw.ingressMac, c2.rw.egressMac) + } + if !reflect.DeepEqual(c1.rw.enc, c2.rw.dec) { + t.Errorf("enc cipher mismatch:\n c1.rw: %#v\n c2.rw: %#v", c1.rw.enc, c2.rw.dec) + } + if !reflect.DeepEqual(c1.rw.dec, c2.rw.enc) { + t.Errorf("dec cipher mismatch:\n c1.rw: %#v\n c2.rw: %#v", c1.rw.dec, c2.rw.enc) + } +} + +// This test runs initator/recipient against each other for each test vector. +func TestHandshakeTV(t *testing.T) { + for i, ht := range handshakeTV { + p1, p2 := net.Pipe() + run(t, rig{ + "initiator": func() error { return checkInitiator(p1, ht) }, + "recipient": func() error { return checkRecipient(p2, ht) }, + }) + if t.Failed() { + t.Fatalf("failed test case %d:\n%s", i, spew.Sdump(ht)) + } + } +} + +// This test runs the encrypted auth packets from the test vectors against +// the recipient code. +func TestHandshakePacketsRecipientTV(t *testing.T) { + for i, ht := range handshakeTV { + p1, p2 := net.Pipe() + run(t, rig{ + "recipient": func() error { + defer p1.Close() + return checkRecipient(p1, ht) + }, + "auth packet send": func() error { + _, err := p2.Write(ht.encAuth) + return err + }, + "authResp packet recv": func() error { + ioutil.ReadAll(p2) + return nil + }, + }) + if t.Failed() { + t.Fatalf("failed test case %d:\n%s", i, spew.Sdump(ht)) + } + } +} + +// This test runs the encrypted authResp packets from the test vectors against +// the initiator code. +func TestHandshakePacketsInitiatorTV(t *testing.T) { + for i, ht := range handshakeTV { + p1, p2 := net.Pipe() + run(t, rig{ + "initiator": func() error { + defer p1.Close() + return checkInitiator(p1, ht) + }, + "authResp packet send": func() error { + _, err := p2.Write(ht.encAuthResp) + return err + }, + "auth packet recv": func() error { + ioutil.ReadAll(p2) + return nil + }, + }) + if t.Failed() { + t.Fatalf("failed test case %d:\n%s", i, spew.Sdump(ht)) + } + } +} + +// This test checks that secrets.mac is initialized correctly. +func TestHandshakeDeriveMacTV(t *testing.T) { + for i, ht := range handshakeTV { + h := handshake{ + initiator: true, + localPrivKey: ht.initiator.Key, + remotePub: ecies.ImportECDSAPublic(&ht.recipient.Key.PublicKey), + initNonce: ht.initiatorNonce, + respNonce: ht.recipientNonce, + randomPrivKey: ecies.ImportECDSA(ht.initiatorEphemeralKey), + remoteRandomPub: ecies.ImportECDSAPublic(&ht.recipientEphemeralKey.PublicKey), + } + vsn, ingress, egress, err := h.deriveSecrets(ht.initiator.ForceV4, ht.encAuth, ht.encAuthResp) + if err != nil { + t.Error("deriveSecrets error: %v", err) + } + if sum := ingress.mac.Sum(nil); !bytes.Equal(sum, ht.initiatorIngressMacDigest) { + t.Errorf("ingress mac mismatch: got %x, want %x", sum, ht.initiatorIngressMacDigest) + } + if sum := egress.mac.Sum(nil); !bytes.Equal(sum, ht.initiatorEgressMacDigest) { + t.Errorf("egress mac mismatch: got %x, want %x", sum, ht.initiatorEgressMacDigest) + } + if err := ht.checkSecrets(vsn, nil, ingress, egress); err != nil { + t.Error(err) + } + if t.Failed() { + t.Fatalf("failed test case %d:\n%s", i, spew.Sdump(ht)) + } + } +} + +func checkInitiator(pipe net.Conn, ht handshakeTest) error { + remotePub := &ht.recipient.Key.PublicKey + conn := Client(pipe, remotePub, ht.initiator) + conn.handshakeRand = fakeRandSource{key: ht.initiatorEphemeralKey, nonce: ht.initiatorNonce} + vsn, ingress, egress, err := conn.initiatorHandshake() + if err != nil { + return err + } + return ht.checkSecrets(vsn, nil, ingress, egress) +} + +func checkRecipient(pipe net.Conn, ht handshakeTest) error { + conn := Server(pipe, ht.recipient) + conn.handshakeRand = fakeRandSource{key: ht.recipientEphemeralKey, nonce: ht.recipientNonce} + vsn, remoteID, ingress, egress, err := conn.recipientHandshake() + if err != nil { + return err + } + return ht.checkSecrets(vsn, remoteID, egress, ingress) +} + +func (ht handshakeTest) checkSecrets(vsn uint, remoteID *ecdsa.PublicKey, ingress, egress secrets) error { + if remoteID != nil && !reflect.DeepEqual(remoteID, &ht.initiator.Key.PublicKey) { + return fmt.Errorf("remoteID mismatch:\ngot %x\nwant %x", + crypto.FromECDSAPub(remoteID), crypto.FromECDSAPub(&ht.initiator.Key.PublicKey)) + } + if vsn != ht.negotiatedVersion { + return fmt.Errorf("version mismatch: got %d, want %d", vsn, ht.negotiatedVersion) + } + // Remove the MACs so secrets can be compared with DeepEqual. + ingress.mac, egress.mac = nil, nil + if !reflect.DeepEqual(ingress, ht.initiatorIngressSecrets) { + return fmt.Errorf("initiatorIngressSecrets mismatch:\ngot %swant %s", + spew.Sdump(ingress), spew.Sdump(ht.initiatorEgressSecrets)) + } + if !reflect.DeepEqual(egress, ht.initiatorEgressSecrets) { + return fmt.Errorf("initiatorEgressSecrets mismatch:\ngot %swant %s", + spew.Sdump(egress), spew.Sdump(ht.initiatorIngressSecrets)) + } + return nil +} + +type fakeRandSource struct { + key *ecdsa.PrivateKey + nonce []byte +} + +func (ht fakeRandSource) generateNonce(b []byte) error { + if len(b) > len(ht.nonce) { + panic(fmt.Sprintf("requested %d bytes of nonce data, have %d", len(b), len(ht.nonce))) + } + copy(b, ht.nonce) + return nil +} + +func (ht fakeRandSource) generateKey() (*ecies.PrivateKey, error) { + return ecies.ImportECDSA(ht.key), nil +} diff --git a/p2p/rlpx/handshake_tv_test.go b/p2p/rlpx/handshake_tv_test.go new file mode 100644 index 0000000000..8e8834660d --- /dev/null +++ b/p2p/rlpx/handshake_tv_test.go @@ -0,0 +1,254 @@ +// 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 . + +package rlpx + +import "crypto/ecdsa" + +type handshakeTest struct { + // Inputs + initiator, recipient *Config + initiatorEphemeralKey, recipientEphemeralKey *ecdsa.PrivateKey + initiatorNonce, recipientNonce []byte + + // Derived Values: These must match exactly in all Test*TV + // functions and do not depend on any random values apart from the + // ones above. + negotiatedVersion uint + initiatorEgressSecrets, initiatorIngressSecrets secrets + + // Encrypted Packets: We can't check them directly because both + // encryption and signing introduce random values. + // TestHandshakePacketsRecipientTV and + // TestHandshakePacketsInitiatorTV check that each 'side' accepts + // the other packet and computes the right secrets. + encAuth, encAuthResp []byte + + // Digests of the empty string created with each MAC hash. These + // are checked TestHandshakeDeriveMacTV with the packets above + // because RLPx V4 includes the ciphertext in the hash. + initiatorIngressMacDigest, initiatorEgressMacDigest []byte +} + +var handshakeTV = []handshakeTest{ + // initiator V5, recipient V5 + { + initiator: &Config{ + Key: hexkey("5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"), + ForceV4: false, + }, + recipient: &Config{ + Key: hexkey("c45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"), + ForceV4: false, + }, + initiatorEphemeralKey: hexkey("19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"), + recipientEphemeralKey: hexkey("d25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"), + initiatorNonce: hexb("cd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e10000000000000005"), + recipientNonce: hexb("f37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d0000000000000005"), + + encAuth: hexb(` + 04f0849817e9483c39b1eead6f5a0dfb09cbc4c43151172c2549c5b07c9364f7 + 853cbae79e7d2a3b9a79d042ddbdf1d95db1f8c7428989123afb02fcad93bfac + 413ad9a9f2bf9b2a621a09fe804229f31f9a71ae6776f10bc8d13bb372fa4af5 + 9a39fd0ae546cb5fce9fd55d59b13e6cbcc234421ab089f1f08932d4622e460c + b0f63b3375b8388e25f84db55a415c764386e00bc19da675baf8e643f48d14c9 + 89432062ed3495943bb6b3f46e8a5011edc3648a0396bccbaa0fe164bd2b8919 + df542da5fd34f24e2d5b84082a9be5fce2e17625d90078eafa8d3125314553e7 + 008dedd9fd5d9d082811a08581d596f7605eba7500aafd2f3c5c8b8cfdce2ac3 + 417559c3d52d6d326a832f0c43077d0697c06db24a5a28c1d67033ecda5d4ff8 + 74831427bcda3ef422b8cc9f34b1eabf39607a + `), + encAuthResp: hexb(` + 0496117783823744f0f58cd952ed34a1866e4ade1a2d66cdfd041f877c4e4216 + d45645ad1dedee1d54d5a767d87231fbadbc6dcb2b48c75b3ab46cb18b224a7f + cd3d9619e03d24813aaa0cc37adb32aa2fa7fbc13aa1fbc01d24d402715fe213 + 62a457a986ec649983f0ff81f5f207799849bce8061dab17b491ac7f0090c426 + f7e63c31f11917e8e33c65d74bd2094435e73ffab1dfbaff368de079244d4ebd + 7f8b542f7081756a1e94b4ed26fb1c3bddabbd642064a15ad597a4f63894ea31 + 13cab7533eec3b8ae163f8ebd61d7bac71e4 + `), + + negotiatedVersion: 5, + initiatorIngressSecrets: secrets{ + encKey: hexb("5d268dbeede1c3ce4e7cd1f900543f671467284d53c6f6fd6b284789652bd1f6"), + encIV: hexb("e5703e8952a6eafcdb2c1940c7615843"), + macKey: hexb("09726cd8b6414cb1f5858b0339badeeed377a48cbe5f3d28a4f74ae41e610c4d"), + }, + initiatorEgressSecrets: secrets{ + encKey: hexb("40bfcb0da6d30512f57187f61b4816fcdc9aaaf107184e29467fe6f6ccefe4a7"), + encIV: hexb("0e906055c0ca86940626d0fde4f3a9c2"), + macKey: hexb("c676534122bd3a555ca8f2d924b63222b1b5b5efccb7b37a52795c1c49450fdc"), + }, + initiatorIngressMacDigest: hexb("de72d7161bc7a9ddda4a70a48d08eda55d6fc4d90ef80a4b6645f81d6373e66b"), + initiatorEgressMacDigest: hexb("47bb77bff168de73c7ae34473f4d085abdf97cce7e01cab6ee3a4f69a021645f"), + }, + + // initiator V5, recipient V4 + { + initiator: &Config{ + Key: hexkey("5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"), + ForceV4: false, + }, + recipient: &Config{ + Key: hexkey("c45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"), + ForceV4: true, + }, + initiatorEphemeralKey: hexkey("19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"), + recipientEphemeralKey: hexkey("d25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"), + initiatorNonce: hexb("cd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e10000000000000005"), + recipientNonce: hexb("f37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d8c7453c0089687a7"), + + encAuth: hexb(` + 04e96a95f95ca7188aedd8abcf58f5f99efc7efed406923084655ce9b197cfe1 + 0921159e105d1cf335e2f4755813598b868784130f4f0ae0bf1776b70d9e3061 + 9205869af0963e0503133dabd7e5ed8ddf531ac3ac4d243f131252b8fd5a1638 + 7296236b9b6b23432dcf02064284522690b8a07cf7e8cd9935409693203f31b9 + 4fe5b2fba7e49a90711597e13ab318230a5fa3c89965569c9da21aa20686df25 + a39bd3393c87ae0f9d11bd6b270480f3544be771fdca8fd2cebb161cc508ee38 + f1eedb793d75f7e081fdf837be699fee2af1f00e2e8924d2ec5c64dabe445d0f + 14caae3c20583252fa8adace052a5f5832ebb957d3324cf12d27232934193806 + a4bfdf9adc3a0921e0bdc7c7457cf35b5d99e729d7e0fd9aa09ab1217ff5ceca + 768fb1fc636499eecab58bb01f46eea652ccbd + `), + encAuthResp: hexb(` + 04d0f8e56113ee9402ab4fed101fe03842f265e13e9bb76af2ac1ffba11d8892 + 524e59f1906eb2e6e35f774ccb3449d2f5084b96063e668fb73d90a94b0114dc + c14364a087f270adc65421741d87c492eafe1ca3b86a76313d026564e1abbb48 + 6d03727c9baf8d0314af54296ea829aa086174a7836113f1dd420750af98f0b8 + 802940ab16af05421d6b812054b285fdf1ae82ae0c08f1dbee3d60691979e8bd + 0b31599ac47138ba24d404699ae4558fec8bb94f120e63362e4b94a50894021e + 70e69101820018472823a48bc0d61c617c21 + `), + + negotiatedVersion: 4, + initiatorIngressSecrets: secrets{ + encKey: hexb("3ca5db8d7d13af7bb3763fee9cef628925a4abda5961d7392fae731c02278377"), + encIV: hexb("00000000000000000000000000000000"), + macKey: hexb("cb2cd684639c1b64b80687b977c4140bea8c953a1f3975aca6f1589a850879ba"), + }, + initiatorEgressSecrets: secrets{ + encKey: hexb("3ca5db8d7d13af7bb3763fee9cef628925a4abda5961d7392fae731c02278377"), + encIV: hexb("00000000000000000000000000000000"), + macKey: hexb("cb2cd684639c1b64b80687b977c4140bea8c953a1f3975aca6f1589a850879ba"), + }, + initiatorIngressMacDigest: hexb("d835ba6c6ac42d4c686a2e3cee6b2ee0190a7da79d6275f2b0b4bdc71fb66709"), + initiatorEgressMacDigest: hexb("1901e950288f010d005ccafede47d1dc177c442605a9702fcd6c5a0e717dc130"), + }, + + // initiator V4, recipient V5 + { + initiator: &Config{ + Key: hexkey("5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"), + ForceV4: true, + }, + recipient: &Config{ + Key: hexkey("c45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"), + ForceV4: false, + }, + initiatorEphemeralKey: hexkey("19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"), + recipientEphemeralKey: hexkey("d25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"), + initiatorNonce: hexb("cd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e1c6b7ec66f077bb11"), + recipientNonce: hexb("f37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d0000000000000005"), + + encAuth: hexb(` + 0461109a208261e0bedca9b3d474e991409fe0f5be7fd757d33c3a2934be7819 + 269509b86229f36677f23aebe239c21e0c2b244811f9ee0d5370bc4cef1510a0 + 77e1d4d295a6219a9393d7493a52b9e98ccca17a196b43efb30cdfaf2ea99fff + bd29c44b5b417924ad3afad6c436e85a0024e24f55f27bb8f86735a76586093f + 4d496348363cb2fb9905e62786c18030a8b4e3fe4a621439ae1c10598a09f9f5 + e61315cea0cd09fb0438d9c76b1d516e183a8df2bdc2d7e3a51f4011a990999c + b5fb737b9dd16181f44253b313811286004efe9298d4ff49eefd28dc095ed362 + 8b56551f052c4d94c0c0108d08656a0201eb29d66702acb06e2894fab08a684a + 394258171fc3f099c4f58a075ecde74c8084731639e19b194ff9eef6824a1330 + 59bd884d81ef14541fd9475b9f3d8bcb613eb6 + `), + encAuthResp: hexb(` + 04c77decb1d4abe500fb924a5972d495b6ca6782122e1e795d8282575302ed32 + 85b0d4bae57ac07c2f455e67cf73d2c77b9dcd295252ca146a65ec3a7e9d6336 + a1ed212843cafac42831c5a785fe7fc6e18a1ce7ae3d9603c439cdd991ec2f7a + 5197838efbd8c0ad68e31559c8a711ca3368bb6f4ed6de53db86df7a56eb2897 + bbb251a2c2e86af3198e87bd98af9f3d96ae7f0777656a3a0c9dec4718f49377 + 0f78b6fecc51f398c0e36e696da3b482584c00581b6b7e596b71580777972bf4 + e158ce46acf49c893a509c00415996948df2 + `), + + negotiatedVersion: 4, + initiatorIngressSecrets: secrets{ + encKey: hexb("fc8e46d37d756d53af5f6cebb35d94118bf305fe1c73fc9e672350cbc1dedd75"), + encIV: hexb("00000000000000000000000000000000"), + macKey: hexb("274693e239751ff505004ed5ab680fd80823d49a12139554bffce549b32d048c"), + }, + initiatorEgressSecrets: secrets{ + encKey: hexb("fc8e46d37d756d53af5f6cebb35d94118bf305fe1c73fc9e672350cbc1dedd75"), + encIV: hexb("00000000000000000000000000000000"), + macKey: hexb("274693e239751ff505004ed5ab680fd80823d49a12139554bffce549b32d048c"), + }, + initiatorIngressMacDigest: hexb("395908f0f3da7e588aad3e6ec04fab504f0a65664bf8fe135b67ebaf4cc41daf"), + initiatorEgressMacDigest: hexb("09cc2e837d5cf048f41ea39dccc4b07814a125dc5d47e416ae13dac8d4523239"), + }, + + // old V4 test vector from https://gist.github.com/fjl/3a78780d17c755d22df2 + { + initiator: &Config{ + Key: hexkey("5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"), + ForceV4: true, + }, + recipient: &Config{ + Key: hexkey("c45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"), + ForceV4: true, + }, + initiatorEphemeralKey: hexkey("19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"), + recipientEphemeralKey: hexkey("d25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"), + initiatorNonce: hexb("cd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e1c6b7ec66f077bb11"), + recipientNonce: hexb("f37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d8c7453c0089687a7"), + + encAuth: hexb(` + 04a0274c5951e32132e7f088c9bdfdc76c9d91f0dc6078e848f8e3361193dbdc + 43b94351ea3d89e4ff33ddcefbc80070498824857f499656c4f79bbd97b6c51a + 514251d69fd1785ef8764bd1d262a883f780964cce6a14ff206daf1206aa073a + 2d35ce2697ebf3514225bef186631b2fd2316a4b7bcdefec8d75a1025ba2c540 + 4a34e7795e1dd4bc01c6113ece07b0df13b69d3ba654a36e35e69ff9d482d88d + 2f0228e7d96fe11dccbb465a1831c7d4ad3a026924b182fc2bdfe016a6944312 + 021da5cc459713b13b86a686cf34d6fe6615020e4acf26bf0d5b7579ba813e77 + 23eb95b3cef9942f01a58bd61baee7c9bdd438956b426a4ffe238e61746a8c93 + d5e10680617c82e48d706ac4953f5e1c4c4f7d013c87d34a06626f498f34576d + c017fdd3d581e83cfd26cf125b6d2bda1f1d56 + `), + encAuthResp: hexb(` + 049934a7b2d7f9af8fd9db941d9da281ac9381b5740e1f64f7092f3588d4f87f + 5ce55191a6653e5e80c1c5dd538169aa123e70dc6ffc5af1827e546c0e958e42 + dad355bcc1fcb9cdf2cf47ff524d2ad98cbf275e661bf4cf00960e74b5956b79 + 9771334f426df007350b46049adb21a6e78ab1408d5e6ccde6fb5e69f0f4c92b + b9c725c02f99fa72b9cdc8dd53cff089e0e73317f61cc5abf6152513cb7d833f + 09d2851603919bf0fbe44d79a09245c6e8338eb502083dc84b846f2fee1cc310 + d2cc8b1b9334728f97220bb799376233e113 + `), + + negotiatedVersion: 4, + initiatorIngressSecrets: secrets{ + encKey: hexb("c0458fa97a5230830e05f4f20b7c755c1d4e54b1ce5cf43260bb191eef4e418d"), + encIV: hexb("00000000000000000000000000000000"), + macKey: hexb("48c938884d5067a1598272fcddaa4b833cd5e7d92e8228c0ecdfabbe68aef7f1"), + }, + initiatorEgressSecrets: secrets{ + encKey: hexb("c0458fa97a5230830e05f4f20b7c755c1d4e54b1ce5cf43260bb191eef4e418d"), + encIV: hexb("00000000000000000000000000000000"), + macKey: hexb("48c938884d5067a1598272fcddaa4b833cd5e7d92e8228c0ecdfabbe68aef7f1"), + }, + initiatorIngressMacDigest: hexb("75823d96e23136c89666ee025fb21a432be906512b3dd4a3049e898adb433847"), + initiatorEgressMacDigest: hexb("09771e93b1a6109e97074cbe2d2b0cf3d3878efafe68f53c41bb60c0ec49097e"), + }, +} diff --git a/p2p/rlpx/rlpx.go b/p2p/rlpx/rlpx.go new file mode 100644 index 0000000000..49d78aeffa --- /dev/null +++ b/p2p/rlpx/rlpx.go @@ -0,0 +1,408 @@ +// 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 . + +// Package rlpx implements the RLPx secure transport protocol. +// +// RLPx multiplexes packet streams over an authenticated and encrypted +// network connection. +// +// The wire protocol specification lives at https://github.com/ethereum/devp2p. +// +// Protocols +// +// RLPx transports packet streams for multiple protocols on the same +// connection, ensuring that available bandwidth is fairly distributed +// among them. Negotiation of protocol identifiers is not part of the +// transport layer and is typically done by sending messages with +// protocol identifier 0. +package rlpx + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "io" + "net" + "sync" + "time" +) + +const ( + defaultHandshakeTimeout = 5 * time.Second + defaultReadTimeout = 10 * time.Second + defaultReadIdleTimeout = 25 * time.Second + defaultWriteTimeout = 10 * time.Second + defaultReadBufferSize = 2 * 1024 * 1024 + defaultReadBufferWaitTimeout = 5 * time.Second +) + +// A Config structure is used to configure an RLPx client or server +// connection. After one has been passed to any function in package +// rlpx, it must not be modified. A Config may be reused; the rlpx +// package will also not modify it. +type Config struct { + // Key is the private key of the server. The key must use the + // secp256k1 curve, other curves are not supported. + // This field is required for both client and server connections. + Key *ecdsa.PrivateKey + + HandshakeTimeout time.Duration // for the key negotiation handshake (default 5s) + ReadIdleTimeout time.Duration // applies while waiting for a new frame (default 25s) + ReadTimeout time.Duration // for reading the payload data of a single frame (default 10s) + WriteTimeout time.Duration // for writing one frame of data (default 10s) + + // ReadBufferSize controls how much data can be buffered for each + // protocol. The default is 2MB for compatibility with legacy + // peers. + // + // If the read buffer is full, the implementation waits for + // buffer space to become available. The connection is closed if + // no space becomes available within the timeout (default 5s). + ReadBufferSize uint32 + ReadBufferWaitTimeout time.Duration + + // Forces use of the version 4 handshake. + ForceV4 bool +} + +func (cfg *Config) handshakeTimeout() time.Duration { + if cfg.HandshakeTimeout != 0 { + return cfg.HandshakeTimeout + } + return defaultHandshakeTimeout +} + +func (cfg *Config) readTimeout() time.Duration { + if cfg.ReadTimeout != 0 { + return cfg.ReadTimeout + } + return defaultReadTimeout +} + +func (cfg *Config) readIdleTimeout() time.Duration { + if cfg.ReadIdleTimeout != 0 { + return cfg.ReadIdleTimeout + } + return defaultReadIdleTimeout +} + +func (cfg *Config) writeTimeout() time.Duration { + if cfg.WriteTimeout != 0 { + return cfg.WriteTimeout + } + return defaultWriteTimeout +} + +func (cfg *Config) readBufferWaitTimeout() time.Duration { + if cfg.ReadBufferWaitTimeout != 0 { + return cfg.ReadBufferWaitTimeout + } + return defaultReadBufferWaitTimeout +} + +func (cfg *Config) readBufferSize() uint32 { + if cfg.ReadBufferSize != 0 { + return cfg.ReadBufferSize + } + return defaultReadBufferSize +} + +// Conn represents an RLPx connection. +type Conn struct { + // readonly fields + cfg *Config + isServer bool + fd net.Conn + handshake sync.Once + handshakeRand handshakeRandSource // for testing + + wmu sync.Mutex // excludes writes on rw + rw *frameRW // set after handshake + remoteID *ecdsa.PublicKey + vsn uint // negotiated version + + mu sync.Mutex + proto map[uint16]*Protocol + readErr error +} + +// Client returns a new client side RLPx connection using fd as the +// underlying transport. The public key of the remote end must be +// known in advance. +// +// config must not be nil and must contain a +// valid private key. +func Client(fd net.Conn, remotePubkey *ecdsa.PublicKey, config *Config) *Conn { + c := newConn(fd, config) + c.remoteID = remotePubkey + return c +} + +// Server returns a new server side RLPx connection using fd as the +// underlying transport. The configuration config must be non-nil and +// must contain a valid private key +func Server(fd net.Conn, config *Config) *Conn { + c := newConn(fd, config) + c.isServer = true + return c +} + +func newConn(fd net.Conn, config *Config) *Conn { + return &Conn{ + fd: fd, + cfg: config, + proto: make(map[uint16]*Protocol), + } +} + +// Handshake runs the client or server handshake protocol if it has +// not yet been run. Most uses of this package need not call Handshake +// explicitly: the first Read or Write will call it automatically. +func (c *Conn) Handshake() (err error) { + // TODO: check cfg.Key curve, maybe panic earlier + c.handshake.Do(func() { + if c.handshakeRand == nil { + c.handshakeRand = realRandSource{} + } + var ( + ingress, egress secrets + rid *ecdsa.PublicKey + vsn uint + ) + c.fd.SetDeadline(time.Now().Add(c.cfg.handshakeTimeout())) + if c.isServer { + vsn, rid, ingress, egress, err = c.recipientHandshake() + } else { + vsn, ingress, egress, err = c.initiatorHandshake() + } + if err != nil { + return + } + + c.mu.Lock() + c.vsn = vsn + if rid != nil { + c.remoteID = rid + } + c.mu.Unlock() + c.rw = newFrameRW(c.fd, ingress, egress) + go readLoop(c) + }) + if err == nil && c.rw == nil { + return errors.New("handshake failed") + } + return err +} + +// LocalAddr returns the local network address of the underlying net.Conn. +func (c *Conn) LocalAddr() net.Addr { + return c.fd.LocalAddr() +} + +// RemoteAddr returns the remote network address of the underlying net.Conn. +func (c *Conn) RemoteAddr() net.Addr { + return c.fd.RemoteAddr() +} + +// RemoteID returns the public key of the remote end. +// If the remote identity is not yet known, it returns nil. +func (c *Conn) RemoteID() *ecdsa.PublicKey { + c.mu.Lock() + id := c.remoteID + c.mu.Unlock() + return id +} + +// Version returns the negotiated RLPx version of the connection. +// The return value is zero before the handshake has executed and +// can be 4 or 5 afterwards. +func (c *Conn) Version() uint { + c.mu.Lock() + vsn := c.vsn + c.mu.Unlock() + return vsn +} + +// Close closes the connection. +func (c *Conn) Close() error { + // TODO: shut down reader/wr + return c.fd.Close() +} + +// Protocol returns a handle for the given protocol id. +// It can be called at most once for any given id, +// subsequent call with the same id will panic. +func (c *Conn) Protocol(id uint16) *Protocol { + p := c.getProtocol(id) + close(p.claimSignal) // panics when claimed twice + return p +} + +// waits until the given protocol is claimed by a call to Protocol. +func (c *Conn) waitForProtocol(id uint16) *Protocol { + p := c.getProtocol(id) + timeout := time.NewTimer(5 * time.Second) + defer timeout.Stop() + select { + case <-timeout.C: + return nil + case <-p.claimSignal: + return p + } +} + +func (c *Conn) getProtocol(id uint16) *Protocol { + c.mu.Lock() + defer c.mu.Unlock() + if c.proto[id] == nil { + c.proto[id] = newProtocol(c, id) + } + return c.proto[id] +} + +// Protocol is a handle for the given protocol. +type Protocol struct { + c *Conn + claimed bool + id uint16 + claimSignal chan struct{} + + // for readLoop + xfers map[uint16]*packetReader + readBufSema *bufSema + + // for ReadPacket + readCond *sync.Cond // unblocks ReadPacket + newPackets []*packetReader + readErr error + + // for writing + contextidSeq uint16 +} + +func newProtocol(c *Conn, id uint16) *Protocol { + return &Protocol{ + c: c, + id: id, + claimSignal: make(chan struct{}), + xfers: make(map[uint16]*packetReader), + readBufSema: newBufSema(c.cfg.readBufferSize()), + readCond: sync.NewCond(new(sync.Mutex)), + } +} + +func (p *Protocol) feedPacket(pr *packetReader) { + p.readCond.L.Lock() + p.newPackets = append(p.newPackets, pr) + p.readCond.Signal() + p.readCond.L.Unlock() +} + +func (p *Protocol) readClose(err error) { + p.readCond.L.Lock() + p.readErr = err + p.readCond.Broadcast() + p.readCond.L.Unlock() +} + +// ReadHeader waits for a packet to appear. The content of the packet +// can be read from r as it is received. More packets can be read +// immediately, r does not need to be consumed before the next call. +func (p *Protocol) ReadPacket() (totalSize uint32, r io.Reader, err error) { + // Lazy handshake. + if err := p.c.Handshake(); err != nil { + return 0, nil, err + } + // Wait for a packet or error. + p.readCond.L.Lock() + defer p.readCond.L.Unlock() + for len(p.newPackets) == 0 && p.readErr == nil { + p.readCond.Wait() + } + if p.readErr != nil { + return 0, nil, p.readErr + } + pr := p.newPackets[0] + p.newPackets = p.newPackets[:copy(p.newPackets, p.newPackets[1:])] + return pr.readN, pr, nil +} + +// SendPacket sends len bytes from the payload reader on the connection. +func (p *Protocol) SendPacket(len uint32, payload io.Reader) error { + if err := p.c.Handshake(); err != nil { + return err + } + if len <= staticFrameSize { + // The message is small enough and can be sent in a single frame. + buf := makeFrameWriteBuffer() + if n, err := io.CopyN(buf, payload, int64(len)); err != nil { + return fmt.Errorf("read from packet payload failed at pos %d: %v", n, err) + } + return p.c.sendFrame(regularHeader{p.id, 0}, buf) + } + return p.sendChunked(len, payload) +} + +func (p *Protocol) sendChunked(size uint32, payload io.Reader) error { + contextid := p.nextContextID() + initial := true + buf := makeFrameWriteBuffer() + var rpos int64 + for seq := uint16(0); size > 0; seq++ { + var header interface{} + if initial { + header = chunkStartHeader{p.id, contextid, size} + initial = false + } else { + header = regularHeader{p.id, contextid} + } + + fsize := staticFrameSize + if size < fsize { + fsize = size + } + if !initial { + buf.resetForWrite() + } + if n, err := io.CopyN(buf, payload, int64(fsize)); err != nil { + // The remote end is waiting for the rest of the packet + // but we can't provide it. Since there is no way to cancel + // partial transfers, our only option is closing the connection. + // TODO: close the connection + return fmt.Errorf("read from packet payload failed at pos %d: %v", rpos+n, err) + } + rpos += int64(fsize) + if err := p.c.sendFrame(header, buf); err != nil { + return err + } + size -= fsize + } + return nil +} + +// returns the next context ID for a chunked transfer. +// never returns 0, which is reserved for single-frame transfers. +func (p *Protocol) nextContextID() uint16 { + p.contextidSeq++ + return p.contextidSeq +} + +func (c *Conn) sendFrame(header interface{}, body *frameBuffer) error { + c.wmu.Lock() + defer c.wmu.Unlock() + c.fd.SetWriteDeadline(time.Now().Add(c.cfg.writeTimeout())) + return c.rw.sendFrame(header, body) +} diff --git a/p2p/rlpx/rlpx_test.go b/p2p/rlpx/rlpx_test.go new file mode 100644 index 0000000000..3c6aad97af --- /dev/null +++ b/p2p/rlpx/rlpx_test.go @@ -0,0 +1,186 @@ +// 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 . + +package rlpx + +import ( + "bytes" + "crypto/ecdsa" + "fmt" + "io" + "io/ioutil" + "net" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/crypto" +) + +func TestSequentialTransfer(t *testing.T) { + var ( + p1, p2 = net.Pipe() + k1, k2 = newkey(), newkey() + sc = Server(p1, &Config{Key: k1}) + cc = Client(p2, &k1.PublicKey, &Config{Key: k2}) + ) + run(t, rig{ + "server": func() error { return testProtoReaders(t, sc, 1) }, + "client": func() error { return testProtoWriters(t, cc, 1) }, + }) +} + +func TestConcurrentTransfer(t *testing.T) { + var ( + p1, p2 = net.Pipe() + k1, k2 = newkey(), newkey() + sc = Server(p1, &Config{Key: k1}) + cc = Client(p2, &k1.PublicKey, &Config{Key: k2}) + ) + run(t, rig{ + "server": func() error { return testProtoReaders(t, cc, 10) }, + "client": func() error { return testProtoWriters(t, sc, 10) }, + }) +} + +func TestConcurrentTransferReadError(t *testing.T) { + var ( + p1, p2 = net.Pipe() + k1, k2 = newkey(), newkey() + sc = Server(p1, &Config{Key: k1}) + cc = Client(p2, &k1.PublicKey, &Config{Key: k2}) + badPacketSize = uint32(16 * 8 * 1024) + ) + run(t, rig{ + "client": func() error { return testProtoWriters(t, sc, 10) }, + "server": func() error { return testProtoReaders(t, cc, 10) }, + + // This sends a bad frame after a two sane ones. + "badFrameWrite": func() error { + if err := cc.Handshake(); err != nil { + return fmt.Errorf("handshake error: %v", err) + } + heads := []interface{}{ + chunkStartHeader{Protocol: 11, ContextID: 1, TotalSize: badPacketSize}, + regularHeader{Protocol: 11, ContextID: 1}, + // The bad frame is a chunk start header with a context id + // that is already in use. + chunkStartHeader{Protocol: 11, ContextID: 1, TotalSize: 22}, + } + for i, h := range heads { + buf := makeFrameWriteBuffer() + buf.Write(make([]byte, 1024)) + if err := cc.sendFrame(h, buf); err != nil { + return fmt.Errorf("error sending frame %d: %v", i, err) + } + } + return nil + }, + + // The other end should receive an error from Read. + "badFrameRead": func() error { + proto := sc.Protocol(11) + _, r, err := proto.ReadPacket() + if err != nil { + return fmt.Errorf("unexpected ReadPacket error: %v", err) + } + _, err = io.CopyN(ioutil.Discard, r, int64(badPacketSize)) + if err == nil { + return fmt.Errorf("no error received") + } + if err != errUnexpectedChunkStart { + return fmt.Errorf("wrong error: got %q want %q", err, errUnexpectedChunkStart) + } + // TODO: shouldn't all transfers fail? + return nil + }, + }) +} + +func testProtoWriters(t *testing.T, conn *Conn, nprotos uint16) error { + defer conn.Close() + writers := rig{} + for i := uint16(0); i < nprotos; i++ { + i := i + writers[fmt.Sprint("protocol ", i)] = func() error { return testWriter(t, conn.Protocol(i)) } + } + run(t, writers) + return nil +} + +func testProtoReaders(t *testing.T, conn *Conn, nprotos uint16) error { + defer conn.Close() + readers := rig{} + for i := uint16(0); i < nprotos; i++ { + i := i + readers[fmt.Sprint("protocol ", i)] = func() error { return testReader(t, conn.Protocol(i)) } + } + run(t, readers) + return nil +} + +func testWriter(t *testing.T, p *Protocol) error { + for size := 1; size < 8*1024*1024; size *= 2 { + if err := sendBytes(p, make([]byte, size)); err != nil { + return fmt.Errorf("error sending %d bytes: %v", size, err) + } + } + return nil +} + +func testReader(t *testing.T, p *Protocol) error { + for size := 1; size < 8*1024*1024; size *= 2 { + len, r, err := p.ReadPacket() + if err != nil { + return fmt.Errorf("ReadPacket error with size %d: %v", size, err) + } + if len != uint32(size) { + return fmt.Errorf("len mismatch, got %d want %d", len, size) + } + if n, err := io.CopyN(ioutil.Discard, r, int64(size)); err != nil { + return fmt.Errorf("body read error at %d of %d bytes: %v", n, size, err) + } + } + return nil +} + +type rig map[string]func() error + +func run(t *testing.T, rig rig) { + var wg sync.WaitGroup + wg.Add(len(rig)) + for name, fn := range rig { + name, fn := name, fn + go func() { + if err := fn(); err != nil { + t.Error(name, err) + } + wg.Done() + }() + } + wg.Wait() +} + +func sendBytes(p *Protocol, data []byte) error { + return p.SendPacket(uint32(len(data)), bytes.NewReader(data)) +} + +func newkey() *ecdsa.PrivateKey { + key, err := crypto.GenerateKey() + if err != nil { + panic("couldn't generate key: " + err.Error()) + } + return key +} From d462a906ee294a248170c123c4d9072965eb046e Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Sat, 19 Sep 2015 02:24:38 +0200 Subject: [PATCH 4/6] p2p: integrate p2p/rlpx --- p2p/devp2p.go | 245 +++++++++++++++ p2p/devp2p_test.go | 125 ++++++++ p2p/dial.go | 12 +- p2p/dial_test.go | 157 +++++----- p2p/discover/database.go | 2 +- p2p/message.go | 35 +-- p2p/message_test.go | 10 - p2p/peer.go | 193 ++++++------ p2p/peer_error.go | 61 ---- p2p/peer_test.go | 158 +++++----- p2p/rlpx.go | 635 --------------------------------------- p2p/rlpx_test.go | 376 ----------------------- p2p/server.go | 67 ++--- p2p/server_test.go | 174 +++++------ 14 files changed, 747 insertions(+), 1503 deletions(-) create mode 100644 p2p/devp2p.go create mode 100644 p2p/devp2p_test.go delete mode 100644 p2p/rlpx.go delete mode 100644 p2p/rlpx_test.go diff --git a/p2p/devp2p.go b/p2p/devp2p.go new file mode 100644 index 0000000000..9868ef0f4f --- /dev/null +++ b/p2p/devp2p.go @@ -0,0 +1,245 @@ +// 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 . + +package p2p + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "io" + "math" + "net" + "time" + + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/rlpx" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + // devp2p message codes + handshakeMsg = 0x00 + discMsg = 0x01 + pingMsg = 0x02 + pongMsg = 0x03 + getPeersMsg = 0x04 + peersMsg = 0x05 +) + +const ( + baseProtocolVersion = 4 + baseProtocolLength = uint64(16) + baseProtocolMaxMsgSize = 2 * 1024 +) + +var errMsgTooBig = errors.New("encoded message size exceeds uint32") + +// DiscReason indicates why a connection is being disconnected. +type DiscReason uint + +const ( + DiscRequested DiscReason = iota + DiscNetworkError + DiscProtocolError + DiscUselessPeer + DiscTooManyPeers + DiscAlreadyConnected + DiscIncompatibleVersion + DiscInvalidIdentity + DiscQuitting + DiscUnexpectedIdentity + DiscSelf + DiscReadTimeout + DiscSubprotocolError = 0x10 +) + +var discReasonToString = [...]string{ + DiscRequested: "Disconnect requested", + DiscNetworkError: "Network error", + DiscProtocolError: "Breach of protocol", + DiscUselessPeer: "Useless peer", + DiscTooManyPeers: "Too many peers", + DiscAlreadyConnected: "Already connected", + DiscIncompatibleVersion: "Incompatible P2P protocol version", + DiscInvalidIdentity: "Invalid node identity", + DiscQuitting: "Client quitting", + DiscUnexpectedIdentity: "Unexpected identity", + DiscSelf: "Connected to self", + DiscReadTimeout: "Read timeout", + DiscSubprotocolError: "Subprotocol error", +} + +func (d DiscReason) String() string { + if len(discReasonToString) < int(d) { + return fmt.Sprintf("Unknown Reason(%d)", d) + } + return discReasonToString[d] +} + +func (d DiscReason) Error() string { + return d.String() +} + +func discReasonForError(err error) DiscReason { + if reason, ok := err.(DiscReason); ok { + return reason + } + peerError, ok := err.(*peerError) + if ok { + switch peerError.code { + case errInvalidMsgCode, errInvalidMsg: + return DiscProtocolError + default: + return DiscSubprotocolError + } + } + return DiscSubprotocolError +} + +// protoHandshake is the RLP structure of the protocol handshake. +type protoHandshake struct { + Version uint64 + Name string + Caps []Cap + ListenPort uint64 + ID discover.NodeID +} + +// devConn implements the devp2p the messaging layer atop RLPx. +type devConn struct { + *rlpx.Conn + // contains negotiated protocol sessions. + // protocol zero is pre-negotiated and carries the + // built-in devp2p packets. + protocols []*devProtocol +} + +// devProtocol represents a running subprotocol. +type devProtocol struct { + p *rlpx.Protocol +} + +func newDevConn(fd net.Conn, key *ecdsa.PrivateKey, remote *ecdsa.PublicKey) *devConn { + c := new(devConn) + if remote == nil { + c.Conn = rlpx.Server(fd, &rlpx.Config{Key: key}) + } else { + c.Conn = rlpx.Client(fd, remote, &rlpx.Config{Key: key}) + } + c.protocols = []*devProtocol{{c.Conn.Protocol(0)}} + return c +} + +func (t *devConn) addProtocols(n int) { + for i := 0; i < n; i++ { + p := t.Conn.Protocol(uint16(len(t.protocols))) + t.protocols = append(t.protocols, &devProtocol{p}) + } +} + +// protoHandshake negotiates RLPx subprotocols. +// the protocol handshake is the first authenticated message +// and also verifies whether the RLPx encryption handshake 'worked' and the +// remote side actually provided the right public key. +func (t *devConn) 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.protocols[0], handshakeMsg, our) }() + if their, err = readProtocolHandshake(t.protocols[0], our); 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) + } + return their, nil +} + +func readProtocolHandshake(rw MsgReader, our *protoHandshake) (*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 posthanshake 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 + } + // validate handshake info + if hs.Version != our.Version { + return nil, DiscIncompatibleVersion + } + if (hs.ID == discover.NodeID{}) { + return nil, DiscInvalidIdentity + } + return &hs, nil +} + +func (t *devConn) close(err error) { + // Tell the remote end why we're disconnecting if possible. + // TODO: if t.DidHandshake() + if r, ok := err.(DiscReason); ok && r != DiscNetworkError { + SendItems(t.protocols[0], discMsg, r) + } + t.Close() +} + +func (p *devProtocol) WriteMsg(msg Msg) error { + codelen, code, _ := rlp.EncodeToReader(msg.Code) + if msg.Size > math.MaxUint32-uint32(codelen) { + return errMsgTooBig + } + plen := msg.Size + uint32(codelen) + return p.p.SendPacket(plen, io.MultiReader(code, msg.Payload)) +} + +func (p *devProtocol) ReadMsg() (msg Msg, err error) { + len, r, err := p.p.ReadPacket() + if err != nil { + return msg, err + } + // Parse the message code, which is prepended to the protocol payload. + // r must be recognized as buffered by package rlp to prevent it from + // reading into the payload. The interface assertion ensures that it is. + // The input limit is 9, which is as large as an encoded uint64 can get. + s := rlp.NewStream(r.(rlp.ByteReader), 9) + if err := s.Decode(&msg.Code); err != nil { + return msg, err + } + // Remaining data in r belongs to the protocol. + msg.Payload = r + msg.Size = len - uint32(rlp.IntSize(msg.Code)) + msg.ReceivedAt = time.Now() + return msg, nil +} diff --git a/p2p/devp2p_test.go b/p2p/devp2p_test.go new file mode 100644 index 0000000000..8bf906c8f9 --- /dev/null +++ b/p2p/devp2p_test.go @@ -0,0 +1,125 @@ +// 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 . + +package p2p + +import ( + "errors" + "net" + "reflect" + "sync" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/ethereum/go-ethereum/p2p/discover" +) + +func TestProtocolHandshake(t *testing.T) { + var ( + prv0, prv1 = newkey(), newkey() + fd0, fd1 = net.Pipe() + hs0 = &protoHandshake{Version: 3, ID: discover.PubkeyID(&prv0.PublicKey), Caps: []Cap{{"a", 0}, {"b", 2}}} + hs1 = &protoHandshake{Version: 3, ID: discover.PubkeyID(&prv1.PublicKey), Caps: []Cap{{"c", 1}, {"d", 3}}} + wg sync.WaitGroup + ) + + wg.Add(2) + go func() { + defer wg.Done() + conn := newDevConn(fd0, prv0, nil) + + phs, err := conn.doProtoHandshake(hs0) + if err != nil { + t.Errorf("dial side proto handshake error: %v", err) + return + } + if !reflect.DeepEqual(phs, hs1) { + t.Errorf("dial side proto handshake mismatch:\ngot: %s\nwant: %s\n", spew.Sdump(phs), spew.Sdump(hs1)) + return + } + conn.close(DiscQuitting) + }() + go func() { + defer wg.Done() + conn := newDevConn(fd1, prv1, &prv0.PublicKey) + + phs, err := conn.doProtoHandshake(hs1) + if err != nil { + t.Errorf("listen side proto handshake error: %v", err) + return + } + 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(conn.protocols[0], discMsg, []DiscReason{DiscQuitting}); err != nil { + t.Errorf("error receiving disconnect: %v", err) + } + }() + wg.Wait() +} + +func TestProtocolHandshakeErrors(t *testing.T) { + our := &protoHandshake{Version: 3, Caps: []Cap{{"foo", 2}, {"bar", 3}}, Name: "quux"} + id := randomID() + 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: 9944, ID: id}, + err: DiscIncompatibleVersion, + }, + { + 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, our) + if !reflect.DeepEqual(err, test.err) { + t.Errorf("test %d: error mismatch: got %q, want %q", i, err, test.err) + } + p1.Close() + } +} diff --git a/p2p/dial.go b/p2p/dial.go index 0fd3a4cf52..32290a3d83 100644 --- a/p2p/dial.go +++ b/p2p/dial.go @@ -133,7 +133,7 @@ func (s *dialstate) newTasks(nRunning int, peers map[discover.NodeID]*Peer, now // Compute number of dynamic dials necessary at this point. needDynDials := s.maxDynDials for _, p := range peers { - if p.rw.is(dynDialedConn) { + if p.conn.is(dynDialedConn) { needDynDials-- } } @@ -205,6 +205,11 @@ func (s *dialstate) taskDone(t task, now time.Time) { } func (t *dialTask) Do(srv *Server) { + remotePubkey, err := t.dest.ID.Pubkey() + if err != nil { + glog.V(logger.Warn).Infof("aborted dialing (invalid pubkey) %v\n", t.dest) + return + } addr := &net.TCPAddr{IP: t.dest.IP, Port: int(t.dest.TCP)} glog.V(logger.Debug).Infof("dialing %v\n", t.dest) fd, err := srv.Dialer.Dial("tcp", addr.String()) @@ -213,9 +218,10 @@ func (t *dialTask) Do(srv *Server) { return } mfd := newMeteredConn(fd, false) - - srv.setupConn(mfd, t.flags, t.dest) + dc := newDevConn(mfd, srv.PrivateKey, remotePubkey) + srv.setupConn(dc, t.flags, t.dest) } + func (t *dialTask) String() string { return fmt.Sprintf("%v %x %v:%d", t.flags, t.dest.ID[:8], t.dest.IP, t.dest.TCP) } diff --git a/p2p/dial_test.go b/p2p/dial_test.go index d24e03e292..99bc016de3 100644 --- a/p2p/dial_test.go +++ b/p2p/dial_test.go @@ -28,6 +28,9 @@ import ( func init() { spew.Config.Indent = "\t" + spew.Config.DisableMethods = true + // glog.SetV(8) + // glog.SetToStderr(true) } type dialtest struct { @@ -49,7 +52,7 @@ func runDialTest(t *testing.T, test dialtest) { pm := func(ps []*Peer) map[discover.NodeID]*Peer { m := make(map[discover.NodeID]*Peer) for _, p := range ps { - m[p.rw.id] = p + m[p.conn.id] = p } return m } @@ -94,18 +97,18 @@ func TestDialStateDynDial(t *testing.T) { // A discovery query is launched. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(0)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, }, new: []task{&discoverTask{bootstrap: true}}, }, // Dynamic dials are launched when it completes. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(0)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, }, done: []task{ &discoverTask{bootstrap: true, results: []*discover.Node{ @@ -127,11 +130,11 @@ func TestDialStateDynDial(t *testing.T) { // the sum of active dial count and dynamic peer count is == maxDynDials. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(3)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(4)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(0)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(3)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(4)}}, }, done: []task{ &dialTask{dynDialedConn, &discover.Node{ID: uintID(3)}}, @@ -142,12 +145,12 @@ func TestDialStateDynDial(t *testing.T) { // maxDynDials has been reached. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(3)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(4)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(0)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(3)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(4)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(5)}}, }, done: []task{ &dialTask{dynDialedConn, &discover.Node{ID: uintID(5)}}, @@ -160,11 +163,11 @@ func TestDialStateDynDial(t *testing.T) { // results from last discovery lookup are reused. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(3)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(4)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(0)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(3)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(4)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(5)}}, }, new: []task{ &dialTask{dynDialedConn, &discover.Node{ID: uintID(6)}}, @@ -175,9 +178,9 @@ func TestDialStateDynDial(t *testing.T) { // and a new one is spawned because more candidates are needed. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(0)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(5)}}, }, done: []task{ &dialTask{dynDialedConn, &discover.Node{ID: uintID(6)}}, @@ -192,10 +195,10 @@ func TestDialStateDynDial(t *testing.T) { // no new is started. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(7)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(0)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(5)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(7)}}, }, done: []task{ &dialTask{dynDialedConn, &discover.Node{ID: uintID(7)}}, @@ -205,10 +208,10 @@ func TestDialStateDynDial(t *testing.T) { // should be immediately requested. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(7)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(0)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(5)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(7)}}, }, done: []task{ &discoverTask{}, @@ -259,8 +262,8 @@ func TestDialStateDynDialFromTable(t *testing.T) { // Dialing nodes 1,2 succeeds. Dials from the lookup are launched. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, }, done: []task{ &dialTask{dynDialedConn, &discover.Node{ID: uintID(1)}}, @@ -281,11 +284,11 @@ func TestDialStateDynDialFromTable(t *testing.T) { // Dialing nodes 3,4,5 fails. The dials from the lookup succeed. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(10)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(11)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(12)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(10)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(11)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(12)}}, }, done: []task{ &dialTask{dynDialedConn, &discover.Node{ID: uintID(3)}}, @@ -300,11 +303,11 @@ func TestDialStateDynDialFromTable(t *testing.T) { // discovery query is still running. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(10)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(11)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(12)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(10)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(11)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(12)}}, }, }, // Nodes 3,4 are not tried again because only the first two @@ -312,11 +315,11 @@ func TestDialStateDynDialFromTable(t *testing.T) { // already connected. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(10)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(11)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(12)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(10)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(11)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(12)}}, }, }, }, @@ -340,8 +343,8 @@ func TestDialStateStaticDial(t *testing.T) { // aren't yet connected. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, }, new: []task{ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}}, @@ -353,9 +356,9 @@ func TestDialStateStaticDial(t *testing.T) { // nodes are either connected or still being dialed. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(3)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(3)}}, }, done: []task{ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}}, @@ -365,11 +368,11 @@ func TestDialStateStaticDial(t *testing.T) { // nodes are now connected. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(3)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(4)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(5)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(3)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(4)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(5)}}, }, done: []task{ &dialTask{staticDialedConn, &discover.Node{ID: uintID(4)}}, @@ -382,20 +385,20 @@ func TestDialStateStaticDial(t *testing.T) { // Wait a round for dial history to expire, no new tasks should spawn. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(3)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(4)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(5)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(3)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(4)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(5)}}, }, }, // If a static node is dropped, it should be immediately redialed, // irrespective whether it was originally static or dynamic. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(3)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(5)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(3)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(5)}}, }, new: []task{ &dialTask{staticDialedConn, &discover.Node{ID: uintID(2)}}, @@ -431,8 +434,8 @@ func TestDialStateCache(t *testing.T) { // nodes are either connected or still being dialed. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(1)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(2)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(1)}}, + {conn: &conn{flags: staticDialedConn, id: uintID(2)}}, }, done: []task{ &dialTask{staticDialedConn, &discover.Node{ID: uintID(1)}}, @@ -443,8 +446,8 @@ func TestDialStateCache(t *testing.T) { // entry to expire. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, }, done: []task{ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}}, @@ -456,15 +459,15 @@ func TestDialStateCache(t *testing.T) { // Still waiting for node 3's entry to expire in the cache. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, }, }, // The cache entry for node 3 has expired and is retried. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(1)}}, + {conn: &conn{flags: dynDialedConn, id: uintID(2)}}, }, new: []task{ &dialTask{staticDialedConn, &discover.Node{ID: uintID(3)}}, diff --git a/p2p/discover/database.go b/p2p/discover/database.go index e8e3371ff9..daa7af99c7 100644 --- a/p2p/discover/database.go +++ b/p2p/discover/database.go @@ -180,7 +180,7 @@ func (db *nodeDB) storeInt64(key []byte, n int64) error { func (db *nodeDB) node(id NodeID) *Node { blob, err := db.lvl.Get(makeKey(id, nodeDBDiscoverRoot), nil) if err != nil { - glog.V(logger.Detail).Infof("failed to retrieve node %v: %v", id, err) + glog.V(logger.Detail).Infof("node %x: %v", id[:8], err) return nil } node := new(Node) diff --git a/p2p/message.go b/p2p/message.go index 1292d21213..7758796a4e 100644 --- a/p2p/message.go +++ b/p2p/message.go @@ -22,8 +22,6 @@ import ( "fmt" "io" "io/ioutil" - "net" - "sync" "sync/atomic" "time" @@ -38,9 +36,10 @@ import ( // structure, encode the payload into a byte array and create a // separate Msg with a bytes.Reader as Payload for each send. type Msg struct { - Code uint64 - Size uint32 // size of the paylod - Payload io.Reader + Code uint64 + Size uint32 // size of the paylod + Payload io.Reader + ReceivedAt time.Time } @@ -66,10 +65,12 @@ func (msg Msg) Discard() error { return err } +// MsgReader wraps the ReadMsg operation. type MsgReader interface { ReadMsg() (Msg, error) } +// MsgWriter wraps the WriteMsg operation. type MsgWriter interface { // WriteMsg sends a message. It will block until the message's // Payload has been consumed by the other end. @@ -110,30 +111,6 @@ func SendItems(w MsgWriter, msgcode uint64, elems ...interface{}) error { return Send(w, msgcode, elems) } -// netWrapper wraps a MsgReadWriter with locks around -// ReadMsg/WriteMsg and applies read/write deadlines. -type netWrapper struct { - rmu, wmu sync.Mutex - - rtimeout, wtimeout time.Duration - conn net.Conn - wrapped MsgReadWriter -} - -func (rw *netWrapper) ReadMsg() (Msg, error) { - rw.rmu.Lock() - defer rw.rmu.Unlock() - rw.conn.SetReadDeadline(time.Now().Add(rw.rtimeout)) - return rw.wrapped.ReadMsg() -} - -func (rw *netWrapper) WriteMsg(msg Msg) error { - rw.wmu.Lock() - defer rw.wmu.Unlock() - rw.conn.SetWriteDeadline(time.Now().Add(rw.wtimeout)) - return rw.wrapped.WriteMsg(msg) -} - // eofSignal wraps a reader with eof signaling. the eof channel is // closed when the wrapped reader returns an error or when count bytes // have been read. diff --git a/p2p/message_test.go b/p2p/message_test.go index 8599b7e87c..5b58654aee 100644 --- a/p2p/message_test.go +++ b/p2p/message_test.go @@ -18,11 +18,9 @@ package p2p import ( "bytes" - "encoding/hex" "fmt" "io" "runtime" - "strings" "testing" "time" ) @@ -141,11 +139,3 @@ func TestEOFSignal(t *testing.T) { default: } } - -func unhex(str string) []byte { - b, err := hex.DecodeString(strings.Replace(str, "\n", "", -1)) - if err != nil { - panic(fmt.Sprintf("invalid hex string: %q", str)) - } - return b -} diff --git a/p2p/peer.go b/p2p/peer.go index 72ed4069c9..aeecc11b21 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -25,44 +25,21 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/rlp" ) -const ( - baseProtocolVersion = 4 - baseProtocolLength = uint64(16) - baseProtocolMaxMsgSize = 2 * 1024 - - pingInterval = 15 * time.Second -) - -const ( - // devp2p message codes - handshakeMsg = 0x00 - discMsg = 0x01 - pingMsg = 0x02 - pongMsg = 0x03 - getPeersMsg = 0x04 - peersMsg = 0x05 -) - -// protoHandshake is the RLP structure of the protocol handshake. -type protoHandshake struct { - Version uint64 - Name string - Caps []Cap - ListenPort uint64 - ID discover.NodeID -} +const pingInterval = 15 * time.Second // Peer represents a connected remote node. type Peer struct { - rw *conn - running map[string]*protoRW + // contains an element for each running subprotocol (excluding devp2p). + running []*protoRW + conn *conn wg sync.WaitGroup protoErr chan error closed chan struct{} @@ -72,7 +49,9 @@ type Peer struct { // NewPeer returns a peer for testing purposes. func NewPeer(id discover.NodeID, name string, caps []Cap) *Peer { pipe, _ := net.Pipe() - conn := &conn{fd: pipe, transport: nil, id: id, caps: caps, name: name} + randomPriv, _ := crypto.GenerateKey() + dc := newDevConn(pipe, randomPriv, nil) + conn := &conn{transport: dc, id: id, caps: caps, name: name} peer := newPeer(conn, nil) close(peer.closed) // ensures Disconnect doesn't block return peer @@ -80,28 +59,28 @@ func NewPeer(id discover.NodeID, name string, caps []Cap) *Peer { // ID returns the node's public key. func (p *Peer) ID() discover.NodeID { - return p.rw.id + return p.conn.id } // Name returns the node name that the remote node advertised. func (p *Peer) Name() string { - return p.rw.name + return p.conn.name } // Caps returns the capabilities (supported subprotocols) of the remote peer. func (p *Peer) Caps() []Cap { // TODO: maybe return copy - return p.rw.caps + return p.conn.caps } // RemoteAddr returns the remote address of the network connection. func (p *Peer) RemoteAddr() net.Addr { - return p.rw.fd.RemoteAddr() + return p.conn.RemoteAddr() } // LocalAddr returns the local address of the network connection. func (p *Peer) LocalAddr() net.Addr { - return p.rw.fd.LocalAddr() + return p.conn.LocalAddr() } // Disconnect terminates the peer connection with the given reason. @@ -115,13 +94,13 @@ func (p *Peer) Disconnect(reason DiscReason) { // String implements fmt.Stringer. func (p *Peer) String() string { - return fmt.Sprintf("Peer %x %v", p.rw.id[:8], p.RemoteAddr()) + return fmt.Sprintf("Peer %x %v", p.conn.id[:8], p.RemoteAddr()) } func newPeer(conn *conn, protocols []Protocol) *Peer { - protomap := matchProtocols(protocols, conn.caps, conn) + protomap := matchProtocols(protocols, conn.caps) p := &Peer{ - rw: conn, + conn: conn, running: protomap, disc: make(chan DiscReason), protoErr: make(chan error, len(protomap)+1), // protocols + pingLoop @@ -132,33 +111,40 @@ func newPeer(conn *conn, protocols []Protocol) *Peer { func (p *Peer) run() DiscReason { var ( - writeStart = make(chan struct{}, 1) - writeErr = make(chan error, 1) - readErr = make(chan error, 1) - reason DiscReason - requested bool + writeErr = make(chan error, len(p.running)) + readErr = make(chan error, 1) + reason DiscReason + requested bool + // While most of the code works with the transport interface so it + // can be tested, using the connection requires an actual + // *devConn. + devconn = p.conn.transport.(*devConn) ) + + // Ensure that the RLPx handshake is done. The only time this will + // actually do anything is while testing because the tests don't + // trigger the handshake explicitly. + if err := devconn.Handshake(); err != nil { + return DiscProtocolError + } + p.wg.Add(2) - go p.readLoop(readErr) - go p.pingLoop() + go p.readLoop(devconn.protocols[0], readErr) + go p.pingLoop(devconn.protocols[0]) // Start all protocol handlers. - writeStart <- struct{}{} - p.startProtocols(writeStart, writeErr) + p.startProtocols(devconn, writeErr) // Wait for an error or disconnect. loop: for { select { case err := <-writeErr: - // A write finished. Allow the next write to start if - // there was no error. if err != nil { glog.V(logger.Detail).Infof("%v: write error: %v\n", p, err) reason = DiscNetworkError break loop } - writeStart <- struct{}{} case err := <-readErr: if r, ok := err.(DiscReason); ok { glog.V(logger.Debug).Infof("%v: remote requested disconnect: %v\n", p, r) @@ -180,7 +166,7 @@ loop: } close(p.closed) - p.rw.close(reason) + p.conn.close(reason) p.wg.Wait() if requested { reason = DiscRequested @@ -188,14 +174,14 @@ loop: return reason } -func (p *Peer) pingLoop() { +func (p *Peer) pingLoop(devp2p *devProtocol) { ping := time.NewTicker(pingInterval) defer p.wg.Done() defer ping.Stop() for { select { case <-ping.C: - if err := SendItems(p.rw, pingMsg); err != nil { + if err := SendItems(devp2p, pingMsg); err != nil { p.protoErr <- err return } @@ -205,27 +191,27 @@ func (p *Peer) pingLoop() { } } -func (p *Peer) readLoop(errc chan<- error) { +func (p *Peer) readLoop(devp2p *devProtocol, errc chan<- error) { defer p.wg.Done() for { - msg, err := p.rw.ReadMsg() + msg, err := devp2p.ReadMsg() if err != nil { errc <- err return } - msg.ReceivedAt = time.Now() - if err = p.handle(msg); err != nil { + if err = p.handle(devp2p, msg); err != nil { errc <- err return } } } -func (p *Peer) handle(msg Msg) error { +func (p *Peer) handle(devp2p *devProtocol, msg Msg) (err error) { switch { case msg.Code == pingMsg: msg.Discard() - go SendItems(p.rw, pongMsg) + go SendItems(devp2p, pongMsg) + return case msg.Code == discMsg: var reason [1]DiscReason // This is the last message. We don't need to discard or @@ -236,8 +222,10 @@ func (p *Peer) handle(msg Msg) error { // ignore other base protocol messages return msg.Discard() default: - // it's a subprotocol message + // Dispatch as subprotocol message by message code offset. + // This is how dispatch worked before chunking was implemented. proto, err := p.getProto(msg.Code) + msg.Code -= proto.offset if err != nil { return fmt.Errorf("msg code out of range: %v", msg.Code) } @@ -248,7 +236,6 @@ func (p *Peer) handle(msg Msg) error { return io.EOF } } - return nil } func countMatchingProtocols(protocols []Protocol, caps []Cap) int { @@ -263,24 +250,27 @@ func countMatchingProtocols(protocols []Protocol, caps []Cap) int { return n } -// matchProtocols creates structures for matching named subprotocols. -func matchProtocols(protocols []Protocol, caps []Cap, rw MsgReadWriter) map[string]*protoRW { +// matchProtocols creates protoRWs for matching named subprotocols. +func matchProtocols(protocols []Protocol, caps []Cap) []*protoRW { sort.Sort(capsByNameAndVersion(caps)) + i := 0 offset := baseProtocolLength - result := make(map[string]*protoRW) - + var result []*protoRW outer: for _, cap := range caps { for _, proto := range protocols { if proto.Name == cap.Name && proto.Version == cap.Version { - // If an old protocol version matched, revert it - if old := result[cap.Name]; old != nil { - offset -= old.Length + if i > 0 && result[i-1].Name == cap.Name { + // If the previous match was for the same protocol + // (with a lower version), reset the offset and replace it. + offset -= result[i-1].Protocol.Length + } else { + // Otherwise, append a new protocol. + result = append(result, nil) + i++ } - // Assign the new match - result[cap.Name] = &protoRW{Protocol: proto, offset: offset, in: make(chan Msg), w: rw} + result[i-1] = &protoRW{Protocol: proto, offset: offset} offset += proto.Length - continue outer } } @@ -288,13 +278,33 @@ outer: return result } -func (p *Peer) startProtocols(writeStart <-chan struct{}, writeErr chan<- error) { +func (p *Peer) startProtocols(dc *devConn, writeErr chan<- error) { + switch dc.Version() { + case 5: + // Acknowledge the protocols on the RLPx layer. This creates + // *devProtocol wrappers, dc.protocols[i] contains entries in + // range 1..len(p.running). + dc.addProtocols(len(p.running)) + for i, proto := range p.running { + proto.offset = 0 + proto.werr = writeErr + proto.rw = dc.protocols[i+1] + } + case 4: + // This is a legacy connection with offset-based dispatch. + for _, proto := range p.running { + proto.closed = p.closed + proto.in = make(chan Msg) + proto.werr = writeErr + proto.rw = dc.protocols[0] + } + default: + panic("conn has no version") + } + // Spawn Run for all protocols. p.wg.Add(len(p.running)) for _, proto := range p.running { proto := proto - proto.closed = p.closed - proto.wstart = writeStart - proto.werr = writeErr glog.V(logger.Detail).Infof("%v: Starting protocol %s/%d\n", p, proto.Name, proto.Version) go func() { err := proto.Run(p, proto) @@ -314,7 +324,7 @@ func (p *Peer) startProtocols(writeStart <-chan struct{}, writeErr chan<- error) // the given message code. func (p *Peer) getProto(code uint64) (*protoRW, error) { for _, proto := range p.running { - if code >= proto.offset && code < proto.offset+proto.Length { + if proto.offset > 0 && code >= proto.offset && code < proto.offset+proto.Length { return proto, nil } } @@ -323,37 +333,40 @@ func (p *Peer) getProto(code uint64) (*protoRW, error) { type protoRW struct { Protocol + offset uint64 + rw MsgReadWriter + werr chan<- error // for write results + + // for RLPx V4 offset-based dispatch in chan Msg // receices read messages closed <-chan struct{} // receives when peer is shutting down - wstart <-chan struct{} // receives when write may start - werr chan<- error // for write results - offset uint64 - w MsgWriter + index uint16 } -func (rw *protoRW) WriteMsg(msg Msg) (err error) { +func (rw *protoRW) WriteMsg(msg Msg) error { if msg.Code >= rw.Length { return newPeerError(errInvalidMsgCode, "not handled") } msg.Code += rw.offset - select { - case <-rw.wstart: - err = rw.w.WriteMsg(msg) - // Report write status back to Peer.run. It will initiate - // shutdown if the error is non-nil and unblock the next write - // otherwise. The calling protocol code should exit for errors - // as well but we don't want to rely on that. + err := rw.rw.WriteMsg(msg) + // Report write status back to Peer.run. It will initiate shutdown + // if the error is non-nil otherwise. The calling protocol should + // exit soon after, but might not return the error correctly. + if err != nil { rw.werr <- err - case <-rw.closed: - err = fmt.Errorf("shutting down") } + // TODO: maybe make the error sticky to prevent further writes return err } func (rw *protoRW) ReadMsg() (Msg, error) { + if rw.offset == 0 { + // RLPx version 5 + return rw.rw.ReadMsg() + } + // RLPx version 4 select { case msg := <-rw.in: - msg.Code -= rw.offset return msg, nil case <-rw.closed: return Msg{}, io.EOF diff --git a/p2p/peer_error.go b/p2p/peer_error.go index 62c7b665dd..e0fd5778d2 100644 --- a/p2p/peer_error.go +++ b/p2p/peer_error.go @@ -50,64 +50,3 @@ func newPeerError(code int, format string, v ...interface{}) *peerError { func (self *peerError) Error() string { return self.message } - -type DiscReason uint - -const ( - DiscRequested DiscReason = iota - DiscNetworkError - DiscProtocolError - DiscUselessPeer - DiscTooManyPeers - DiscAlreadyConnected - DiscIncompatibleVersion - DiscInvalidIdentity - DiscQuitting - DiscUnexpectedIdentity - DiscSelf - DiscReadTimeout - DiscSubprotocolError = 0x10 -) - -var discReasonToString = [...]string{ - DiscRequested: "Disconnect requested", - DiscNetworkError: "Network error", - DiscProtocolError: "Breach of protocol", - DiscUselessPeer: "Useless peer", - DiscTooManyPeers: "Too many peers", - DiscAlreadyConnected: "Already connected", - DiscIncompatibleVersion: "Incompatible P2P protocol version", - DiscInvalidIdentity: "Invalid node identity", - DiscQuitting: "Client quitting", - DiscUnexpectedIdentity: "Unexpected identity", - DiscSelf: "Connected to self", - DiscReadTimeout: "Read timeout", - DiscSubprotocolError: "Subprotocol error", -} - -func (d DiscReason) String() string { - if len(discReasonToString) < int(d) { - return fmt.Sprintf("Unknown Reason(%d)", d) - } - return discReasonToString[d] -} - -func (d DiscReason) Error() string { - return d.String() -} - -func discReasonForError(err error) DiscReason { - if reason, ok := err.(DiscReason); ok { - return reason - } - peerError, ok := err.(*peerError) - if ok { - switch peerError.code { - case errInvalidMsgCode, errInvalidMsg: - return DiscProtocolError - default: - return DiscSubprotocolError - } - } - return DiscSubprotocolError -} diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 6f96a823b4..5617a92b33 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -24,6 +24,8 @@ import ( "reflect" "testing" "time" + + "github.com/davecgh/go-spew/spew" ) var discard = Protocol{ @@ -43,21 +45,20 @@ var discard = Protocol{ }, } -func testPeer(protos []Protocol) (func(), *conn, *Peer, <-chan DiscReason) { +func testPeer(protos []Protocol) (*devConn, *Peer, <-chan DiscReason) { fd1, fd2 := net.Pipe() - c1 := &conn{fd: fd1, transport: newTestTransport(randomID(), fd1)} - c2 := &conn{fd: fd2, transport: newTestTransport(randomID(), fd2)} + k1, k2 := newkey(), newkey() + c1 := &conn{transport: newDevConn(fd1, k1, &k2.PublicKey)} for _, p := range protos { c1.caps = append(c1.caps, p.cap()) - c2.caps = append(c2.caps, p.cap()) } - peer := newPeer(c1, protos) errc := make(chan DiscReason, 1) go func() { errc <- peer.run() }() - closer := func() { c2.close(errors.New("close func called")) } - return closer, c2, peer, errc + c2 := newDevConn(fd2, k2, nil) + c2.addProtocols(len(protos)) + return c2, peer, errc } func TestPeerProtoReadMsg(t *testing.T) { @@ -80,12 +81,12 @@ func TestPeerProtoReadMsg(t *testing.T) { }, } - closer, rw, _, errc := testPeer([]Protocol{proto}) - defer closer() + conn, _, errc := testPeer([]Protocol{proto}) + defer conn.Close() - Send(rw, baseProtocolLength+2, []uint{1}) - Send(rw, baseProtocolLength+3, []uint{2}) - Send(rw, baseProtocolLength+4, []uint{3}) + Send(conn.protocols[1], 2, []uint{1}) + Send(conn.protocols[1], 3, []uint{2}) + Send(conn.protocols[1], 4, []uint{3}) select { case <-done: @@ -110,29 +111,29 @@ func TestPeerProtoEncodeMsg(t *testing.T) { return nil }, } - closer, rw, _, _ := testPeer([]Protocol{proto}) - defer closer() + conn, _, _ := testPeer([]Protocol{proto}) + defer conn.Close() - if err := ExpectMsg(rw, 17, []string{"foo", "bar"}); err != nil { + if err := ExpectMsg(conn.protocols[1], 1, []string{"foo", "bar"}); err != nil { t.Error(err) } } func TestPeerPing(t *testing.T) { - closer, rw, _, _ := testPeer(nil) - defer closer() - if err := SendItems(rw, pingMsg); err != nil { + conn, _, _ := testPeer(nil) + defer conn.Close() + if err := SendItems(conn.protocols[0], pingMsg); err != nil { t.Fatal(err) } - if err := ExpectMsg(rw, pongMsg, nil); err != nil { + if err := ExpectMsg(conn.protocols[0], pongMsg, nil); err != nil { t.Error(err) } } func TestPeerDisconnect(t *testing.T) { - closer, rw, _, disc := testPeer(nil) - defer closer() - if err := SendItems(rw, discMsg, DiscQuitting); err != nil { + conn, _, disc := testPeer(nil) + defer conn.Close() + if err := SendItems(conn.protocols[0], discMsg, DiscQuitting); err != nil { t.Fatal(err) } select { @@ -150,10 +151,10 @@ func TestPeerDisconnect(t *testing.T) { func TestPeerDisconnectRace(t *testing.T) { maybe := func() bool { return rand.Intn(1) == 1 } - for i := 0; i < 1000; i++ { - protoclose := make(chan error) - protodisc := make(chan DiscReason) - closer, rw, p, disc := testPeer([]Protocol{ + for i := 0; i < 100; i++ { + protoclose := make(chan error, 1) + protodisc := make(chan DiscReason, 1) + conn, p, disc := testPeer([]Protocol{ { Name: "closereq", Run: func(p *Peer, rw MsgReadWriter) error { return <-protoclose }, @@ -165,12 +166,13 @@ func TestPeerDisconnectRace(t *testing.T) { Length: 1, }, }) + conn.Handshake() // Simulate incoming messages. - go SendItems(rw, baseProtocolLength+1) - go SendItems(rw, baseProtocolLength+2) + go SendItems(conn.protocols[1], 1) + go SendItems(conn.protocols[2], 2) // Close the network connection. - go closer() + go conn.Close() // Make protocol "closereq" return. protoclose <- errors.New("protocol closed") // Make protocol "disconnect" call peer.Disconnect @@ -181,7 +183,7 @@ func TestPeerDisconnectRace(t *testing.T) { } // In some cases, simulate remote requesting a disconnect. if maybe() { - go SendItems(rw, discMsg, DiscQuitting) + go SendItems(conn.protocols[0], discMsg, DiscQuitting) } select { @@ -214,96 +216,78 @@ func TestNewPeer(t *testing.T) { } func TestMatchProtocols(t *testing.T) { - tests := []struct { + tests := map[string]struct { Remote []Cap Local []Protocol - Match map[string]protoRW + Match []*protoRW }{ - { - // No remote capabilities + "no remote caps": { Local: []Protocol{{Name: "a"}}, }, - { - // No local protocols + "no local protocols": { Remote: []Cap{{Name: "a"}}, }, - { - // No mutual protocols + "no mutual protocols": { Remote: []Cap{{Name: "a"}}, Local: []Protocol{{Name: "b"}}, }, - { - // Some matches, some differences + "some matches": { Remote: []Cap{{Name: "local"}, {Name: "match1"}, {Name: "match2"}}, Local: []Protocol{{Name: "match1"}, {Name: "match2"}, {Name: "remote"}}, - Match: map[string]protoRW{"match1": {Protocol: Protocol{Name: "match1"}}, "match2": {Protocol: Protocol{Name: "match2"}}}, + Match: []*protoRW{ + {Protocol: Protocol{Name: "match1"}, offset: 16}, + {Protocol: Protocol{Name: "match2"}, offset: 16}, + }, }, - { - // Various alphabetical ordering + "alphabetical ordering": { Remote: []Cap{{Name: "aa"}, {Name: "ab"}, {Name: "bb"}, {Name: "ba"}}, Local: []Protocol{{Name: "ba"}, {Name: "bb"}, {Name: "ab"}, {Name: "aa"}}, - Match: map[string]protoRW{"aa": {Protocol: Protocol{Name: "aa"}}, "ab": {Protocol: Protocol{Name: "ab"}}, "ba": {Protocol: Protocol{Name: "ba"}}, "bb": {Protocol: Protocol{Name: "bb"}}}, + Match: []*protoRW{ + {Protocol: Protocol{Name: "aa"}, offset: 16}, + {Protocol: Protocol{Name: "ab"}, offset: 16}, + {Protocol: Protocol{Name: "ba"}, offset: 16}, + {Protocol: Protocol{Name: "bb"}, offset: 16}, + }, }, - { - // No mutual versions + "no mutual versions": { Remote: []Cap{{Version: 1}}, Local: []Protocol{{Version: 2}}, }, - { - // Multiple versions, single common + "multiple versions, single common": { Remote: []Cap{{Version: 1}, {Version: 2}}, Local: []Protocol{{Version: 2}, {Version: 3}}, - Match: map[string]protoRW{"": {Protocol: Protocol{Version: 2}}}, + Match: []*protoRW{ + {Protocol: Protocol{Version: 2}, offset: 16}, + }, }, - { - // Multiple versions, multiple common + "multiple versions, multiple common": { Remote: []Cap{{Version: 1}, {Version: 2}, {Version: 3}, {Version: 4}}, Local: []Protocol{{Version: 2}, {Version: 3}}, - Match: map[string]protoRW{"": {Protocol: Protocol{Version: 3}}}, + Match: []*protoRW{ + {Protocol: Protocol{Version: 3}, offset: 16}, + }, }, - { - // Various version orderings + "version ordering": { Remote: []Cap{{Version: 4}, {Version: 1}, {Version: 3}, {Version: 2}}, Local: []Protocol{{Version: 2}, {Version: 3}, {Version: 1}}, - Match: map[string]protoRW{"": {Protocol: Protocol{Version: 3}}}, + Match: []*protoRW{ + {Protocol: Protocol{Version: 3}, offset: 16}, + }, }, - { - // Versions overriding sub-protocol lengths + "versions overriding subprotocol lengths": { Remote: []Cap{{Version: 1}, {Version: 2}, {Version: 3}, {Name: "a"}}, Local: []Protocol{{Version: 1, Length: 1}, {Version: 2, Length: 2}, {Version: 3, Length: 3}, {Name: "a"}}, - Match: map[string]protoRW{"": {Protocol: Protocol{Version: 3}}, "a": {Protocol: Protocol{Name: "a"}, offset: 3}}, + Match: []*protoRW{ + {Protocol: Protocol{Version: 3, Length: 3}, offset: 16}, + {Protocol: Protocol{Name: "a"}, offset: 19}, + }, }, } - for i, tt := range tests { - result := matchProtocols(tt.Local, tt.Remote, nil) - if len(result) != len(tt.Match) { - t.Errorf("test %d: negotiation mismatch: have %v, want %v", i, len(result), len(tt.Match)) - continue - } - // Make sure all negotiated protocols are needed and correct - for name, proto := range result { - match, ok := tt.Match[name] - if !ok { - t.Errorf("test %d, proto '%s': negotiated but shouldn't have", i, name) - continue - } - if proto.Name != match.Name { - t.Errorf("test %d, proto '%s': name mismatch: have %v, want %v", i, name, proto.Name, match.Name) - } - if proto.Version != match.Version { - t.Errorf("test %d, proto '%s': version mismatch: have %v, want %v", i, name, proto.Version, match.Version) - } - if proto.offset-baseProtocolLength != match.offset { - t.Errorf("test %d, proto '%s': offset mismatch: have %v, want %v", i, name, proto.offset-baseProtocolLength, match.offset) - } - } - // Make sure no protocols missed negotiation - for name, _ := range tt.Match { - if _, ok := result[name]; !ok { - t.Errorf("test %d, proto '%s': not negotiated, should have", i, name) - continue - } + for tname, tt := range tests { + result := matchProtocols(tt.Local, tt.Remote) + if !reflect.DeepEqual(result, tt.Match) { + t.Errorf("%s: wrong result\ngot %s\nwant: %s", tname, spew.Sdump(result), spew.Sdump(tt.Match)) } } } diff --git a/p2p/rlpx.go b/p2p/rlpx.go deleted file mode 100644 index 8f429d6ec1..0000000000 --- a/p2p/rlpx.go +++ /dev/null @@ -1,635 +0,0 @@ -// 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 . - -package p2p - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/hmac" - "crypto/rand" - "errors" - "fmt" - "hash" - "io" - "net" - "sync" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/ethereum/go-ethereum/crypto/secp256k1" - "github.com/ethereum/go-ethereum/crypto/sha3" - "github.com/ethereum/go-ethereum/p2p/discover" - "github.com/ethereum/go-ethereum/rlp" -) - -const ( - maxUint24 = ^uint32(0) >> 8 - - sskLen = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 - sigLen = 65 // elliptic S256 - pubLen = 64 // 512 bit pubkey in uncompressed representation without format byte - shaLen = 32 // hash length (for nonce etc) - - authMsgLen = sigLen + shaLen + pubLen + shaLen + 1 - authRespLen = pubLen + shaLen + 1 - - eciesBytes = 65 + 16 + 32 - encAuthMsgLen = authMsgLen + eciesBytes // size of the final ECIES payload sent as initiator's handshake - encAuthRespLen = authRespLen + eciesBytes // size of the final ECIES payload sent as receiver's handshake - - // 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 -) - -// rlpx is the transport protocol used by actual (non-test) connections. -// It wraps the frame encoder with locks and read/write deadlines. -type rlpx struct { - fd net.Conn - - rmu, wmu sync.Mutex - rw *rlpxFrameRW -} - -func newRLPX(fd net.Conn) transport { - fd.SetDeadline(time.Now().Add(handshakeTimeout)) - return &rlpx{fd: fd} -} - -func (t *rlpx) ReadMsg() (Msg, error) { - t.rmu.Lock() - defer t.rmu.Unlock() - t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout)) - return t.rw.ReadMsg() -} - -func (t *rlpx) WriteMsg(msg Msg) error { - t.wmu.Lock() - defer t.wmu.Unlock() - t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout)) - return t.rw.WriteMsg(msg) -} - -func (t *rlpx) close(err error) { - t.wmu.Lock() - defer t.wmu.Unlock() - // Tell the remote end why we're disconnecting if possible. - if t.rw != nil { - if r, ok := err.(DiscReason); ok && r != DiscNetworkError { - t.fd.SetWriteDeadline(time.Now().Add(discWriteTimeout)) - SendItems(t.rw, discMsg, r) - } - } - t.fd.Close() -} - -// doEncHandshake runs the protocol handshake using authenticated -// messages. the protocol handshake is the first authenticated message -// and also verifies whether the encryption handshake 'worked' and the -// remote side actually provided the right public key. -func (t *rlpx) 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.rw, handshakeMsg, our) }() - if their, err = readProtocolHandshake(t.rw, our); 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) - } - return their, nil -} - -func readProtocolHandshake(rw MsgReader, our *protoHandshake) (*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 posthanshake 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 - } - // validate handshake info - if hs.Version != our.Version { - return nil, DiscIncompatibleVersion - } - if (hs.ID == discover.NodeID{}) { - return nil, DiscInvalidIdentity - } - return &hs, nil -} - -func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *discover.Node) (discover.NodeID, error) { - var ( - sec secrets - err error - ) - if dial == nil { - sec, err = receiverEncHandshake(t.fd, prv, nil) - } else { - sec, err = initiatorEncHandshake(t.fd, prv, dial.ID, nil) - } - if err != nil { - return discover.NodeID{}, err - } - t.wmu.Lock() - t.rw = newRLPXFrameRW(t.fd, sec) - t.wmu.Unlock() - return sec.RemoteID, nil -} - -// encHandshake contains the state of the encryption handshake. -type encHandshake struct { - initiator bool - remoteID discover.NodeID - - remotePub *ecies.PublicKey // remote-pubk - initNonce, respNonce []byte // nonce - randomPrivKey *ecies.PrivateKey // ecdhe-random - remoteRandomPub *ecies.PublicKey // ecdhe-random-pubk -} - -// secrets represents the connection secrets -// which are negotiated during the encryption handshake. -type secrets struct { - RemoteID discover.NodeID - AES, MAC []byte - EgressMAC, IngressMAC hash.Hash - Token []byte -} - -// secrets is called after the handshake is completed. -// It extracts the connection secrets from the handshake values. -func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) { - ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen) - if err != nil { - return secrets{}, err - } - - // derive base secrets from ephemeral key agreement - sharedSecret := crypto.Sha3(ecdheSecret, crypto.Sha3(h.respNonce, h.initNonce)) - aesSecret := crypto.Sha3(ecdheSecret, sharedSecret) - s := secrets{ - RemoteID: h.remoteID, - AES: aesSecret, - MAC: crypto.Sha3(ecdheSecret, aesSecret), - Token: crypto.Sha3(sharedSecret), - } - - // setup sha3 instances for the MACs - mac1 := sha3.NewKeccak256() - mac1.Write(xor(s.MAC, h.respNonce)) - mac1.Write(auth) - mac2 := sha3.NewKeccak256() - mac2.Write(xor(s.MAC, h.initNonce)) - mac2.Write(authResp) - if h.initiator { - s.EgressMAC, s.IngressMAC = mac1, mac2 - } else { - s.EgressMAC, s.IngressMAC = mac2, mac1 - } - - return s, nil -} - -func (h *encHandshake) ecdhShared(prv *ecdsa.PrivateKey) ([]byte, error) { - return ecies.ImportECDSA(prv).GenerateShared(h.remotePub, sskLen, sskLen) -} - -// initiatorEncHandshake negotiates a session token on conn. -// it should be called on the dialing side of the connection. -// -// prv is the local client's private key. -// token is the token from a previous session with this node. -func initiatorEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, remoteID discover.NodeID, token []byte) (s secrets, err error) { - h, err := newInitiatorHandshake(remoteID) - if err != nil { - return s, err - } - auth, err := h.authMsg(prv, token) - if err != nil { - return s, err - } - if _, err = conn.Write(auth); err != nil { - return s, err - } - - response := make([]byte, encAuthRespLen) - if _, err = io.ReadFull(conn, response); err != nil { - return s, err - } - if err := h.decodeAuthResp(response, prv); err != nil { - return s, err - } - return h.secrets(auth, response) -} - -func newInitiatorHandshake(remoteID discover.NodeID) (*encHandshake, error) { - rpub, err := remoteID.Pubkey() - if err != nil { - return nil, fmt.Errorf("bad remoteID: %v", err) - } - // generate random initiator nonce - n := make([]byte, shaLen) - if _, err := rand.Read(n); err != nil { - return nil, err - } - // generate random keypair to use for signing - randpriv, err := ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil) - if err != nil { - return nil, err - } - h := &encHandshake{ - initiator: true, - remoteID: remoteID, - remotePub: ecies.ImportECDSAPublic(rpub), - initNonce: n, - randomPrivKey: randpriv, - } - return h, nil -} - -// authMsg creates an encrypted initiator handshake message. -func (h *encHandshake) authMsg(prv *ecdsa.PrivateKey, token []byte) ([]byte, error) { - var tokenFlag byte - if token == nil { - // no session token found means we need to generate shared secret. - // ecies shared secret is used as initial session token for new peers - // generate shared key from prv and remote pubkey - var err error - if token, err = h.ecdhShared(prv); err != nil { - return nil, err - } - } else { - // for known peers, we use stored token from the previous session - tokenFlag = 0x01 - } - - // sign known message: - // ecdh-shared-secret^nonce for new peers - // token^nonce for old peers - signed := xor(token, h.initNonce) - signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA()) - if err != nil { - return nil, err - } - - // encode auth message - // signature || sha3(ecdhe-random-pubk) || pubk || nonce || token-flag - msg := make([]byte, authMsgLen) - n := copy(msg, signature) - n += copy(msg[n:], crypto.Sha3(exportPubkey(&h.randomPrivKey.PublicKey))) - n += copy(msg[n:], crypto.FromECDSAPub(&prv.PublicKey)[1:]) - n += copy(msg[n:], h.initNonce) - msg[n] = tokenFlag - - // encrypt auth message using remote-pubk - return ecies.Encrypt(rand.Reader, h.remotePub, msg, nil, nil) -} - -// decodeAuthResp decode an encrypted authentication response message. -func (h *encHandshake) decodeAuthResp(auth []byte, prv *ecdsa.PrivateKey) error { - msg, err := crypto.Decrypt(prv, auth) - if err != nil { - return fmt.Errorf("could not decrypt auth response (%v)", err) - } - h.respNonce = msg[pubLen : pubLen+shaLen] - h.remoteRandomPub, err = importPublicKey(msg[:pubLen]) - if err != nil { - return err - } - // ignore token flag for now - return nil -} - -// receiverEncHandshake negotiates a session token on conn. -// it should be called on the listening side of the connection. -// -// prv is the local client's private key. -// token is the token from a previous session with this node. -func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, token []byte) (s secrets, err error) { - // read remote auth sent by initiator. - auth := make([]byte, encAuthMsgLen) - if _, err := io.ReadFull(conn, auth); err != nil { - return s, err - } - h, err := decodeAuthMsg(prv, token, auth) - if err != nil { - return s, err - } - - // send auth response - resp, err := h.authResp(prv, token) - if err != nil { - return s, err - } - if _, err = conn.Write(resp); err != nil { - return s, err - } - - return h.secrets(auth, resp) -} - -func decodeAuthMsg(prv *ecdsa.PrivateKey, token []byte, auth []byte) (*encHandshake, error) { - var err error - h := new(encHandshake) - // generate random keypair for session - h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil) - if err != nil { - return nil, err - } - // generate random nonce - h.respNonce = make([]byte, shaLen) - if _, err = rand.Read(h.respNonce); err != nil { - return nil, err - } - - msg, err := crypto.Decrypt(prv, auth) - if err != nil { - return nil, fmt.Errorf("could not decrypt auth message (%v)", err) - } - - // decode message parameters - // signature || sha3(ecdhe-random-pubk) || pubk || nonce || token-flag - h.initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1] - copy(h.remoteID[:], msg[sigLen+shaLen:sigLen+shaLen+pubLen]) - rpub, err := h.remoteID.Pubkey() - if err != nil { - return nil, fmt.Errorf("bad remoteID: %#v", err) - } - h.remotePub = ecies.ImportECDSAPublic(rpub) - - // recover remote random pubkey from signed message. - if token == nil { - // TODO: it is an error if the initiator has a token and we don't. check that. - - // no session token means we need to generate shared secret. - // ecies shared secret is used as initial session token for new peers. - // generate shared key from prv and remote pubkey. - if token, err = h.ecdhShared(prv); err != nil { - return nil, err - } - } - signedMsg := xor(token, h.initNonce) - remoteRandomPub, err := secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]) - if err != nil { - return nil, err - } - - // validate the sha3 of recovered pubkey - remoteRandomPubMAC := msg[sigLen : sigLen+shaLen] - shaRemoteRandomPub := crypto.Sha3(remoteRandomPub[1:]) - if !bytes.Equal(remoteRandomPubMAC, shaRemoteRandomPub) { - return nil, fmt.Errorf("sha3 of recovered ephemeral pubkey does not match checksum in auth message") - } - - h.remoteRandomPub, _ = importPublicKey(remoteRandomPub) - return h, nil -} - -// authResp generates the encrypted authentication response message. -func (h *encHandshake) authResp(prv *ecdsa.PrivateKey, token []byte) ([]byte, error) { - // responder auth message - // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - resp := make([]byte, authRespLen) - n := copy(resp, exportPubkey(&h.randomPrivKey.PublicKey)) - n += copy(resp[n:], h.respNonce) - if token == nil { - resp[n] = 0 - } else { - resp[n] = 1 - } - // encrypt using remote-pubk - return ecies.Encrypt(rand.Reader, h.remotePub, resp, nil, nil) -} - -// importPublicKey unmarshals 512 bit public keys. -func importPublicKey(pubKey []byte) (*ecies.PublicKey, error) { - var pubKey65 []byte - switch len(pubKey) { - case 64: - // add 'uncompressed key' flag - pubKey65 = append([]byte{0x04}, pubKey...) - case 65: - pubKey65 = pubKey - default: - return nil, fmt.Errorf("invalid public key length %v (expect 64/65)", len(pubKey)) - } - // TODO: fewer pointless conversions - return ecies.ImportECDSAPublic(crypto.ToECDSAPub(pubKey65)), nil -} - -func exportPubkey(pub *ecies.PublicKey) []byte { - if pub == nil { - panic("nil pubkey") - } - return elliptic.Marshal(pub.Curve, pub.X, pub.Y)[1:] -} - -func xor(one, other []byte) (xor []byte) { - xor = make([]byte, len(one)) - for i := 0; i < len(one); i++ { - xor[i] = one[i] ^ other[i] - } - return xor -} - -var ( - // this is used in place of actual frame header data. - // TODO: replace this when Msg contains the protocol type code. - zeroHeader = []byte{0xC2, 0x80, 0x80} - // sixteen zero bytes - zero16 = make([]byte, 16) -) - -// rlpxFrameRW implements a simplified version of RLPx framing. -// chunked messages are not supported and all headers are equal to -// zeroHeader. -// -// rlpxFrameRW is not safe for concurrent use from multiple goroutines. -type rlpxFrameRW struct { - conn io.ReadWriter - enc cipher.Stream - dec cipher.Stream - - macCipher cipher.Block - egressMAC hash.Hash - ingressMAC hash.Hash -} - -func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW { - macc, err := aes.NewCipher(s.MAC) - if err != nil { - panic("invalid MAC secret: " + err.Error()) - } - encc, err := aes.NewCipher(s.AES) - if err != nil { - panic("invalid AES secret: " + err.Error()) - } - // we use an all-zeroes IV for AES because the key used - // for encryption is ephemeral. - iv := make([]byte, encc.BlockSize()) - return &rlpxFrameRW{ - conn: conn, - enc: cipher.NewCTR(encc, iv), - dec: cipher.NewCTR(encc, iv), - macCipher: macc, - egressMAC: s.EgressMAC, - ingressMAC: s.IngressMAC, - } -} - -func (rw *rlpxFrameRW) WriteMsg(msg Msg) error { - ptype, _ := rlp.EncodeToBytes(msg.Code) - - // write header - headbuf := make([]byte, 32) - fsize := uint32(len(ptype)) + msg.Size - if fsize > maxUint24 { - return errors.New("message size overflows uint24") - } - putInt24(fsize, headbuf) // TODO: check overflow - copy(headbuf[3:], zeroHeader) - rw.enc.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now encrypted - - // write header MAC - copy(headbuf[16:], updateMAC(rw.egressMAC, rw.macCipher, headbuf[:16])) - if _, err := rw.conn.Write(headbuf); err != nil { - return err - } - - // write encrypted frame, updating the egress MAC hash with - // the data written to conn. - tee := cipher.StreamWriter{S: rw.enc, W: io.MultiWriter(rw.conn, rw.egressMAC)} - if _, err := tee.Write(ptype); err != nil { - return err - } - if _, err := io.Copy(tee, msg.Payload); err != nil { - return err - } - if padding := fsize % 16; padding > 0 { - if _, err := tee.Write(zero16[:16-padding]); err != nil { - return err - } - } - - // write frame MAC. egress MAC hash is up to date because - // frame content was written to it as well. - fmacseed := rw.egressMAC.Sum(nil) - mac := updateMAC(rw.egressMAC, rw.macCipher, fmacseed) - _, err := rw.conn.Write(mac) - return err -} - -func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { - // read the header - headbuf := make([]byte, 32) - if _, err := io.ReadFull(rw.conn, headbuf); err != nil { - return msg, err - } - // verify header mac - shouldMAC := updateMAC(rw.ingressMAC, rw.macCipher, headbuf[:16]) - if !hmac.Equal(shouldMAC, headbuf[16:]) { - return msg, errors.New("bad header MAC") - } - rw.dec.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now decrypted - fsize := readInt24(headbuf) - // ignore protocol type for now - - // read the frame content - var rsize = fsize // frame size rounded up to 16 byte boundary - if padding := fsize % 16; padding > 0 { - rsize += 16 - padding - } - framebuf := make([]byte, rsize) - if _, err := io.ReadFull(rw.conn, framebuf); err != nil { - return msg, err - } - - // read and validate frame MAC. we can re-use headbuf for that. - rw.ingressMAC.Write(framebuf) - fmacseed := rw.ingressMAC.Sum(nil) - if _, err := io.ReadFull(rw.conn, headbuf[:16]); err != nil { - return msg, err - } - shouldMAC = updateMAC(rw.ingressMAC, rw.macCipher, fmacseed) - if !hmac.Equal(shouldMAC, headbuf[:16]) { - return msg, errors.New("bad frame MAC") - } - - // decrypt frame content - rw.dec.XORKeyStream(framebuf, framebuf) - - // decode message code - content := bytes.NewReader(framebuf[:fsize]) - if err := rlp.Decode(content, &msg.Code); err != nil { - return msg, err - } - msg.Size = uint32(content.Len()) - msg.Payload = content - return msg, nil -} - -// updateMAC reseeds the given hash with encrypted seed. -// it returns the first 16 bytes of the hash sum after seeding. -func updateMAC(mac hash.Hash, block cipher.Block, seed []byte) []byte { - aesbuf := make([]byte, aes.BlockSize) - block.Encrypt(aesbuf, mac.Sum(nil)) - for i := range aesbuf { - aesbuf[i] ^= seed[i] - } - mac.Write(aesbuf) - return mac.Sum(nil)[:16] -} - -func readInt24(b []byte) uint32 { - return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16 -} - -func putInt24(v uint32, b []byte) { - b[0] = byte(v >> 16) - b[1] = byte(v >> 8) - b[2] = byte(v) -} diff --git a/p2p/rlpx_test.go b/p2p/rlpx_test.go deleted file mode 100644 index 7cc7548e2b..0000000000 --- a/p2p/rlpx_test.go +++ /dev/null @@ -1,376 +0,0 @@ -// 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 . - -package p2p - -import ( - "bytes" - "crypto/rand" - "errors" - "fmt" - "io/ioutil" - "net" - "reflect" - "strings" - "sync" - "testing" - "time" - - "github.com/davecgh/go-spew/spew" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/ethereum/go-ethereum/crypto/sha3" - "github.com/ethereum/go-ethereum/p2p/discover" - "github.com/ethereum/go-ethereum/rlp" -) - -func TestSharedSecret(t *testing.T) { - prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) - pub0 := &prv0.PublicKey - prv1, _ := crypto.GenerateKey() - pub1 := &prv1.PublicKey - - ss0, err := ecies.ImportECDSA(prv0).GenerateShared(ecies.ImportECDSAPublic(pub1), sskLen, sskLen) - if err != nil { - return - } - ss1, err := ecies.ImportECDSA(prv1).GenerateShared(ecies.ImportECDSAPublic(pub0), sskLen, sskLen) - if err != nil { - return - } - t.Logf("Secret:\n%v %x\n%v %x", len(ss0), ss0, len(ss0), ss1) - if !bytes.Equal(ss0, ss1) { - t.Errorf("dont match :(") - } -} - -func TestEncHandshake(t *testing.T) { - for i := 0; i < 10; i++ { - start := time.Now() - if err := testEncHandshake(nil); err != nil { - t.Fatalf("i=%d %v", i, err) - } - t.Logf("(without token) %d %v\n", i+1, time.Since(start)) - } - for i := 0; i < 10; i++ { - tok := make([]byte, shaLen) - rand.Reader.Read(tok) - start := time.Now() - if err := testEncHandshake(tok); err != nil { - t.Fatalf("i=%d %v", i, err) - } - t.Logf("(with token) %d %v\n", i+1, time.Since(start)) - } -} - -func testEncHandshake(token []byte) error { - type result struct { - side string - id discover.NodeID - err error - } - var ( - prv0, _ = crypto.GenerateKey() - prv1, _ = crypto.GenerateKey() - fd0, fd1 = net.Pipe() - c0, c1 = newRLPX(fd0).(*rlpx), newRLPX(fd1).(*rlpx) - output = make(chan result) - ) - - go func() { - r := result{side: "initiator"} - defer func() { output <- r }() - defer fd0.Close() - - dest := &discover.Node{ID: discover.PubkeyID(&prv1.PublicKey)} - r.id, r.err = c0.doEncHandshake(prv0, dest) - if r.err != nil { - return - } - id1 := discover.PubkeyID(&prv1.PublicKey) - if r.id != id1 { - r.err = fmt.Errorf("remote ID mismatch: got %v, want: %v", r.id, id1) - } - }() - go func() { - r := result{side: "receiver"} - defer func() { output <- r }() - defer fd1.Close() - - r.id, r.err = c1.doEncHandshake(prv1, nil) - if r.err != nil { - return - } - id0 := discover.PubkeyID(&prv0.PublicKey) - if r.id != id0 { - r.err = fmt.Errorf("remote ID mismatch: got %v, want: %v", r.id, id0) - } - }() - - // wait for results from both sides - r1, r2 := <-output, <-output - if r1.err != nil { - return fmt.Errorf("%s side error: %v", r1.side, r1.err) - } - if r2.err != nil { - return fmt.Errorf("%s side error: %v", r2.side, r2.err) - } - - // compare derived secrets - if !reflect.DeepEqual(c0.rw.egressMAC, c1.rw.ingressMAC) { - return fmt.Errorf("egress mac mismatch:\n c0.rw: %#v\n c1.rw: %#v", c0.rw.egressMAC, c1.rw.ingressMAC) - } - if !reflect.DeepEqual(c0.rw.ingressMAC, c1.rw.egressMAC) { - return fmt.Errorf("ingress mac mismatch:\n c0.rw: %#v\n c1.rw: %#v", c0.rw.ingressMAC, c1.rw.egressMAC) - } - if !reflect.DeepEqual(c0.rw.enc, c1.rw.enc) { - return fmt.Errorf("enc cipher mismatch:\n c0.rw: %#v\n c1.rw: %#v", c0.rw.enc, c1.rw.enc) - } - if !reflect.DeepEqual(c0.rw.dec, c1.rw.dec) { - return fmt.Errorf("dec cipher mismatch:\n c0.rw: %#v\n c1.rw: %#v", c0.rw.dec, c1.rw.dec) - } - return nil -} - -func TestProtocolHandshake(t *testing.T) { - var ( - prv0, _ = crypto.GenerateKey() - node0 = &discover.Node{ID: discover.PubkeyID(&prv0.PublicKey), IP: net.IP{1, 2, 3, 4}, TCP: 33} - hs0 = &protoHandshake{Version: 3, ID: node0.ID, Caps: []Cap{{"a", 0}, {"b", 2}}} - - prv1, _ = crypto.GenerateKey() - node1 = &discover.Node{ID: discover.PubkeyID(&prv1.PublicKey), IP: net.IP{5, 6, 7, 8}, TCP: 44} - hs1 = &protoHandshake{Version: 3, ID: node1.ID, Caps: []Cap{{"c", 1}, {"d", 3}}} - - fd0, fd1 = net.Pipe() - wg sync.WaitGroup - ) - - wg.Add(2) - go func() { - defer wg.Done() - rlpx := newRLPX(fd0) - remid, err := rlpx.doEncHandshake(prv0, node1) - if err != nil { - t.Errorf("dial side enc handshake failed: %v", err) - return - } - if remid != node1.ID { - t.Errorf("dial side remote id mismatch: got %v, want %v", remid, node1.ID) - return - } - - phs, err := rlpx.doProtoHandshake(hs0) - if err != nil { - t.Errorf("dial side proto handshake error: %v", err) - return - } - if !reflect.DeepEqual(phs, hs1) { - t.Errorf("dial side proto handshake mismatch:\ngot: %s\nwant: %s\n", spew.Sdump(phs), spew.Sdump(hs1)) - return - } - rlpx.close(DiscQuitting) - }() - go func() { - defer wg.Done() - rlpx := newRLPX(fd1) - remid, err := rlpx.doEncHandshake(prv1, nil) - if err != nil { - t.Errorf("listen side enc handshake failed: %v", err) - return - } - if remid != node0.ID { - t.Errorf("listen side remote id mismatch: got %v, want %v", remid, node0.ID) - return - } - - phs, err := rlpx.doProtoHandshake(hs1) - if err != nil { - t.Errorf("listen side proto handshake error: %v", err) - return - } - 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) { - our := &protoHandshake{Version: 3, Caps: []Cap{{"foo", 2}, {"bar", 3}}, Name: "quux"} - id := randomID() - 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: 9944, ID: id}, - err: DiscIncompatibleVersion, - }, - { - 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, our) - if !reflect.DeepEqual(err, test.err) { - t.Errorf("test %d: error mismatch: got %q, want %q", i, err, test.err) - } - } -} - -func TestRLPXFrameFake(t *testing.T) { - buf := new(bytes.Buffer) - hash := fakeHash([]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) - rw := newRLPXFrameRW(buf, secrets{ - AES: crypto.Sha3(), - MAC: crypto.Sha3(), - IngressMAC: hash, - EgressMAC: hash, - }) - - golden := unhex(` -00828ddae471818bb0bfa6b551d1cb42 -01010101010101010101010101010101 -ba628a4ba590cb43f7848f41c4382885 -01010101010101010101010101010101 -`) - - // Check WriteMsg. This puts a message into the buffer. - if err := Send(rw, 8, []uint{1, 2, 3, 4}); err != nil { - t.Fatalf("WriteMsg error: %v", err) - } - written := buf.Bytes() - if !bytes.Equal(written, golden) { - t.Fatalf("output mismatch:\n got: %x\n want: %x", written, golden) - } - - // Check ReadMsg. It reads the message encoded by WriteMsg, which - // is equivalent to the golden message above. - msg, err := rw.ReadMsg() - if err != nil { - t.Fatalf("ReadMsg error: %v", err) - } - if msg.Size != 5 { - t.Errorf("msg size mismatch: got %d, want %d", msg.Size, 5) - } - if msg.Code != 8 { - t.Errorf("msg code mismatch: got %d, want %d", msg.Code, 8) - } - payload, _ := ioutil.ReadAll(msg.Payload) - wantPayload := unhex("C401020304") - if !bytes.Equal(payload, wantPayload) { - t.Errorf("msg payload mismatch:\ngot %x\nwant %x", payload, wantPayload) - } -} - -type fakeHash []byte - -func (fakeHash) Write(p []byte) (int, error) { return len(p), nil } -func (fakeHash) Reset() {} -func (fakeHash) BlockSize() int { return 0 } - -func (h fakeHash) Size() int { return len(h) } -func (h fakeHash) Sum(b []byte) []byte { return append(b, h...) } - -func TestRLPXFrameRW(t *testing.T) { - var ( - aesSecret = make([]byte, 16) - macSecret = make([]byte, 16) - egressMACinit = make([]byte, 32) - ingressMACinit = make([]byte, 32) - ) - for _, s := range [][]byte{aesSecret, macSecret, egressMACinit, ingressMACinit} { - rand.Read(s) - } - conn := new(bytes.Buffer) - - s1 := secrets{ - AES: aesSecret, - MAC: macSecret, - EgressMAC: sha3.NewKeccak256(), - IngressMAC: sha3.NewKeccak256(), - } - s1.EgressMAC.Write(egressMACinit) - s1.IngressMAC.Write(ingressMACinit) - rw1 := newRLPXFrameRW(conn, s1) - - s2 := secrets{ - AES: aesSecret, - MAC: macSecret, - EgressMAC: sha3.NewKeccak256(), - IngressMAC: sha3.NewKeccak256(), - } - s2.EgressMAC.Write(ingressMACinit) - s2.IngressMAC.Write(egressMACinit) - rw2 := newRLPXFrameRW(conn, s2) - - // send some messages - for i := 0; i < 10; i++ { - // write message into conn buffer - wmsg := []interface{}{"foo", "bar", strings.Repeat("test", i)} - err := Send(rw1, uint64(i), wmsg) - if err != nil { - t.Fatalf("WriteMsg error (i=%d): %v", i, err) - } - - // read message that rw1 just wrote - msg, err := rw2.ReadMsg() - if err != nil { - t.Fatalf("ReadMsg error (i=%d): %v", i, err) - } - if msg.Code != uint64(i) { - t.Fatalf("msg code mismatch: got %d, want %d", msg.Code, i) - } - payload, _ := ioutil.ReadAll(msg.Payload) - wantPayload, _ := rlp.EncodeToBytes(wmsg) - if !bytes.Equal(payload, wantPayload) { - t.Fatalf("msg payload mismatch:\ngot %x\nwant %x", payload, wantPayload) - } - } -} diff --git a/p2p/server.go b/p2p/server.go index 7991585f17..071e0e4647 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -123,8 +123,7 @@ type Server struct { // Hooks for testing. These are useful because we can inhibit // the whole protocol stack. - newTransport func(net.Conn) transport - newPeerHook func(*Peer) + peerRunFunction func(*Peer) DiscReason lock sync.Mutex // protects running running bool @@ -157,30 +156,27 @@ const ( trustedConn ) +// used in place of devConn so server tests can substitute a mock. +type transport interface { + // devConn + doProtoHandshake(our *protoHandshake) (their *protoHandshake, err error) + close(err error) + // rlpx.Conn + Handshake() error + RemoteAddr() net.Addr + LocalAddr() net.Addr + RemoteID() *ecdsa.PublicKey +} + // conn wraps a network connection with information gathered // during the two handshakes. type conn struct { - fd net.Conn transport + id discover.NodeID flags connFlag - cont chan error // The run loop uses cont to signal errors to setupConn. - id discover.NodeID // valid after the encryption handshake - caps []Cap // valid after the protocol handshake - name string // valid after the protocol handshake -} - -type transport interface { - // The two handshakes. - doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node) (discover.NodeID, error) - doProtoHandshake(our *protoHandshake) (*protoHandshake, error) - // The MsgReadWriter can only be used after the encryption - // handshake has completed. The code uses conn.id to track this - // by setting it to a non-nil value after the encryption handshake. - MsgReadWriter - // transports must provide Close because we use MsgPipe in some of - // the tests. Closing the actual network connection doesn't do - // anything in those tests because NsgPipe doesn't use it. - close(err error) + cont chan error // The run loop uses cont to signal errors to setupConn. + caps []Cap // valid after the protocol handshake + name string // valid after the protocol handshake } func (c *conn) String() string { @@ -188,7 +184,7 @@ func (c *conn) String() string { if (c.id != discover.NodeID{}) { s += fmt.Sprintf(" %x", c.id[:8]) } - s += " " + c.fd.RemoteAddr().String() + s += " " + c.RemoteAddr().String() return s } @@ -314,9 +310,6 @@ func (srv *Server) Start() (err error) { if srv.PrivateKey == nil { return fmt.Errorf("Server.PrivateKey must be set to a non-nil key") } - if srv.newTransport == nil { - srv.newTransport = newRLPX - } if srv.Dialer == nil { srv.Dialer = &net.Dialer{Timeout: defaultDialTimeout} } @@ -505,7 +498,7 @@ running: } // Disconnect all peers. for _, p := range peers { - p.Disconnect(DiscQuitting) + p.conn.close(DiscQuitting) } // Wait for peers to shut down. Pending connections and tasks are // not handled here and will terminate soon-ish because srv.quit @@ -588,7 +581,7 @@ func (srv *Server) listenLoop() { // Spawn the handler. It will give the slot back when the connection // has been established. go func() { - srv.setupConn(fd, inboundConn, nil) + srv.setupConn(newDevConn(fd, srv.PrivateKey, nil), inboundConn, nil) slots <- struct{}{} }() } @@ -597,24 +590,24 @@ func (srv *Server) listenLoop() { // setupConn runs the handshakes and attempts to add the connection // as a peer. It returns when the connection has been added as a peer // or the handshakes have failed. -func (srv *Server) setupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) { +func (srv *Server) setupConn(t transport, flags connFlag, dialDest *discover.Node) { // Prevent leftover pending conns from entering the handshake. srv.lock.Lock() running := srv.running srv.lock.Unlock() - c := &conn{fd: fd, transport: srv.newTransport(fd), flags: flags, cont: make(chan error)} + c := &conn{transport: t, flags: flags, cont: make(chan error)} if !running { c.close(errServerStopped) return } // Run the encryption handshake. - var err error - if c.id, err = c.doEncHandshake(srv.PrivateKey, dialDest); err != nil { - glog.V(logger.Debug).Infof("%v faild enc handshake: %v", c, err) + if err := c.Handshake(); err != nil { + glog.V(logger.Debug).Infof("%v failed enc handshake: %v", c, err) c.close(err) return } // For dialed connections, check that the remote public key matches. + c.id = discover.PubkeyID(c.RemoteID()) if dialDest != nil && c.id != dialDest.ID { c.close(DiscUnexpectedIdentity) glog.V(logger.Debug).Infof("%v dialed identity mismatch, want %x", c, dialDest.ID[:8]) @@ -675,15 +668,17 @@ func (srv *Server) runPeer(p *Peer) { NumConnections: srv.PeerCount(), }) - if srv.newPeerHook != nil { - srv.newPeerHook(p) + var reason DiscReason + if srv.peerRunFunction != nil { + reason = srv.peerRunFunction(p) + } else { + reason = p.run() } - discreason := p.run() // Note: run waits for existing peers to be sent on srv.delpeer // before returning, so this send should not select on srv.quit. srv.delpeer <- p - glog.V(logger.Debug).Infof("Removed %v (%v)\n", p, discreason) + glog.V(logger.Debug).Infof("Removed %v (%v)\n", p, reason) srvjslog.LogJson(&logger.P2PDisconnected{ RemoteId: p.ID().String(), NumConnections: srv.PeerCount(), diff --git a/p2p/server_test.go b/p2p/server_test.go index 976d5baf59..be74e89d7e 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -26,54 +26,17 @@ import ( "time" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/sha3" "github.com/ethereum/go-ethereum/p2p/discover" ) -func init() { - // glog.SetV(6) - // glog.SetToStderr(true) -} - -type testTransport struct { - id discover.NodeID - *rlpx - - closeErr error -} - -func newTestTransport(id discover.NodeID, fd net.Conn) transport { - wrapped := newRLPX(fd).(*rlpx) - wrapped.rw = newRLPXFrameRW(fd, secrets{ - MAC: zero16, - AES: zero16, - IngressMAC: sha3.NewKeccak256(), - EgressMAC: sha3.NewKeccak256(), - }) - return &testTransport{id: id, rlpx: wrapped} -} - -func (c *testTransport) doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node) (discover.NodeID, error) { - return c.id, nil -} - -func (c *testTransport) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) { - return &protoHandshake{ID: c.id, Name: "test"}, nil -} - -func (c *testTransport) close(err error) { - c.rlpx.fd.Close() - c.closeErr = err -} - -func startTestServer(t *testing.T, id discover.NodeID, pf func(*Peer)) *Server { +func startTestServer(t *testing.T, trustedNodes []*discover.Node, pf func(*Peer) DiscReason) *Server { server := &Server{ - Name: "test", - MaxPeers: 10, - ListenAddr: "127.0.0.1:0", - PrivateKey: newkey(), - newPeerHook: pf, - newTransport: func(fd net.Conn) transport { return newTestTransport(id, fd) }, + Name: "test", + MaxPeers: 10, + ListenAddr: "127.0.0.1:0", + PrivateKey: newkey(), + TrustedNodes: trustedNodes, + peerRunFunction: pf, } if err := server.Start(); err != nil { t.Fatalf("Could not start server: %v", err) @@ -84,8 +47,10 @@ func startTestServer(t *testing.T, id discover.NodeID, pf func(*Peer)) *Server { func TestServerListen(t *testing.T) { // start the test server connected := make(chan *Peer) - remid := randomID() - srv := startTestServer(t, remid, func(p *Peer) { + remkey := newkey() + remid := discover.PubkeyID(&remkey.PublicKey) + quitPeers := make(chan struct{}) + srv := startTestServer(t, nil, func(p *Peer) DiscReason { if p.ID() != remid { t.Error("peer func called with wrong node id") } @@ -93,16 +58,21 @@ func TestServerListen(t *testing.T) { t.Error("peer func called with nil conn") } connected <- p + <-quitPeers + return DiscQuitting }) defer close(connected) defer srv.Stop() + defer close(quitPeers) // dial the test server - conn, err := net.DialTimeout("tcp", srv.ListenAddr, 5*time.Second) + fd, err := net.DialTimeout("tcp", srv.ListenAddr, 5*time.Second) if err != nil { t.Fatalf("could not dial: %v", err) } - defer conn.Close() + defer fd.Close() + conn := newDevConn(fd, remkey, &srv.PrivateKey.PublicKey) + conn.doProtoHandshake(&protoHandshake{Version: baseProtocolVersion, ID: remid}) select { case peer := <-connected: @@ -120,6 +90,18 @@ func TestServerListen(t *testing.T) { } func TestServerDial(t *testing.T) { + // start the server + connected := make(chan *Peer) + quitPeers := make(chan struct{}) + srv := startTestServer(t, nil, func(p *Peer) DiscReason { + connected <- p + <-quitPeers + return DiscQuitting + }) + defer close(connected) + defer srv.Stop() + defer close(quitPeers) + // run a one-shot TCP server to handle the connection. listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -127,22 +109,19 @@ func TestServerDial(t *testing.T) { } defer listener.Close() accepted := make(chan net.Conn) + remkey := newkey() + remid := discover.PubkeyID(&remkey.PublicKey) go func() { - conn, err := listener.Accept() + fd, err := listener.Accept() if err != nil { t.Error("accept error:", err) return } - accepted <- conn + accepted <- fd + conn := newDevConn(fd, remkey, nil) + conn.doProtoHandshake(&protoHandshake{Version: baseProtocolVersion, ID: remid, Name: "test"}) }() - // start the server - connected := make(chan *Peer) - remid := randomID() - srv := startTestServer(t, remid, func(p *Peer) { connected <- p }) - defer close(connected) - defer srv.Stop() - // tell the server to connect tcpAddr := listener.Addr().(*net.TCPAddr) srv.AddPeer(&discover.Node{ID: remid, IP: tcpAddr.IP, TCP: uint16(tcpAddr.Port)}) @@ -263,21 +242,21 @@ func (t *testTask) Do(srv *Server) { // at capacity. Trusted connections should still be accepted. func TestServerAtCap(t *testing.T) { trustedID := randomID() - srv := &Server{ - PrivateKey: newkey(), - MaxPeers: 10, - NoDial: true, - TrustedNodes: []*discover.Node{{ID: trustedID}}, - } - if err := srv.Start(); err != nil { - t.Fatalf("could not start: %v", err) - } + quitPeers := make(chan struct{}) + srv := startTestServer(t, []*discover.Node{{ID: trustedID}}, func(*Peer) DiscReason { + <-quitPeers + return DiscQuitting + }) defer srv.Stop() + defer close(quitPeers) newconn := func(id discover.NodeID) *conn { - fd, _ := net.Pipe() - tx := newTestTransport(id, fd) - return &conn{fd: fd, transport: tx, flags: inboundConn, id: id, cont: make(chan error)} + return &conn{ + transport: &fakeTransport{id: id}, + flags: inboundConn, + id: id, + cont: make(chan error), + } } // Inject a few connections to fill up the peer set. @@ -300,16 +279,15 @@ func TestServerAtCap(t *testing.T) { if !c.is(trustedConn) { t.Error("Server did not set trusted flag") } - } func TestServerSetupConn(t *testing.T) { - id := randomID() - srvkey := newkey() + remkey, srvkey := newkey(), newkey() + id := discover.PubkeyID(&remkey.PublicKey) srvid := discover.PubkeyID(&srvkey.PublicKey) tests := []struct { dontstart bool - tt *setupTransport + tt *fakeTransport flags connFlag dialDest *discover.Node @@ -318,45 +296,45 @@ func TestServerSetupConn(t *testing.T) { }{ { dontstart: true, - tt: &setupTransport{id: id}, + tt: &fakeTransport{id: id}, wantCalls: "close,", wantCloseErr: errServerStopped, }, { - tt: &setupTransport{id: id, encHandshakeErr: errors.New("read error")}, + tt: &fakeTransport{id: id, encHandshakeErr: errors.New("read error")}, flags: inboundConn, wantCalls: "doEncHandshake,close,", wantCloseErr: errors.New("read error"), }, { - tt: &setupTransport{id: id}, + tt: &fakeTransport{id: id}, dialDest: &discover.Node{ID: randomID()}, flags: dynDialedConn, wantCalls: "doEncHandshake,close,", wantCloseErr: DiscUnexpectedIdentity, }, { - tt: &setupTransport{id: id, phs: &protoHandshake{ID: randomID()}}, + tt: &fakeTransport{id: id, phs: &protoHandshake{ID: randomID()}}, dialDest: &discover.Node{ID: id}, flags: dynDialedConn, wantCalls: "doEncHandshake,doProtoHandshake,close,", wantCloseErr: DiscUnexpectedIdentity, }, { - tt: &setupTransport{id: id, protoHandshakeErr: errors.New("foo")}, + tt: &fakeTransport{id: id, protoHandshakeErr: errors.New("foo")}, dialDest: &discover.Node{ID: id}, flags: dynDialedConn, wantCalls: "doEncHandshake,doProtoHandshake,close,", wantCloseErr: errors.New("foo"), }, { - tt: &setupTransport{id: srvid, phs: &protoHandshake{ID: srvid}}, + tt: &fakeTransport{id: srvid, phs: &protoHandshake{ID: srvid}}, flags: inboundConn, wantCalls: "doEncHandshake,close,", wantCloseErr: DiscSelf, }, { - tt: &setupTransport{id: id, phs: &protoHandshake{ID: id}}, + tt: &fakeTransport{id: id, phs: &protoHandshake{ID: id}}, flags: inboundConn, wantCalls: "doEncHandshake,doProtoHandshake,close,", wantCloseErr: DiscUselessPeer, @@ -365,19 +343,17 @@ func TestServerSetupConn(t *testing.T) { for i, test := range tests { srv := &Server{ - PrivateKey: srvkey, - MaxPeers: 10, - NoDial: true, - Protocols: []Protocol{discard}, - newTransport: func(fd net.Conn) transport { return test.tt }, + PrivateKey: srvkey, + MaxPeers: 10, + NoDial: true, + Protocols: []Protocol{discard}, } if !test.dontstart { if err := srv.Start(); err != nil { t.Fatalf("couldn't start server: %v", err) } } - p1, _ := net.Pipe() - srv.setupConn(p1, test.flags, test.dialDest) + srv.setupConn(test.tt, test.flags, test.dialDest) if !reflect.DeepEqual(test.tt.closeErr, test.wantCloseErr) { t.Errorf("test %d: close error mismatch: got %q, want %q", i, test.tt.closeErr, test.wantCloseErr) } @@ -387,7 +363,7 @@ func TestServerSetupConn(t *testing.T) { } } -type setupTransport struct { +type fakeTransport struct { id discover.NodeID encHandshakeErr error @@ -398,28 +374,30 @@ type setupTransport struct { closeErr error } -func (c *setupTransport) doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node) (discover.NodeID, error) { +func (c *fakeTransport) Handshake() error { c.calls += "doEncHandshake," - return c.id, c.encHandshakeErr + return c.encHandshakeErr } -func (c *setupTransport) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) { +func (c *fakeTransport) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) { c.calls += "doProtoHandshake," if c.protoHandshakeErr != nil { return nil, c.protoHandshakeErr } return c.phs, nil } -func (c *setupTransport) close(err error) { +func (c *fakeTransport) close(err error) { c.calls += "close," c.closeErr = err } - -// setupConn shouldn't write to/read from the connection. -func (c *setupTransport) WriteMsg(Msg) error { - panic("WriteMsg called on setupTransport") +func (c *fakeTransport) RemoteID() *ecdsa.PublicKey { + key, _ := c.id.Pubkey() + return key } -func (c *setupTransport) ReadMsg() (Msg, error) { - panic("ReadMsg called on setupTransport") +func (c *fakeTransport) RemoteAddr() net.Addr { + return &net.TCPAddr{Port: 33, IP: net.IP{0, 0, 0, 1}} +} +func (c *fakeTransport) LocalAddr() net.Addr { + return &net.TCPAddr{Port: 44, IP: net.IP{0, 0, 0, 2}} } func newkey() *ecdsa.PrivateKey { From e66ead8d86508f6a858c505c3e75e233828acef7 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 28 Oct 2015 20:11:36 +0100 Subject: [PATCH 5/6] crypto/ecies: fix build on 32bit --- crypto/ecies/ecies.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/ecies/ecies.go b/crypto/ecies/ecies.go index 2c4dc70a4c..1ef46b4484 100644 --- a/crypto/ecies/ecies.go +++ b/crypto/ecies/ecies.go @@ -152,7 +152,7 @@ var ( func ConcatKDF(hash hash.Hash, z, s1 []byte, kdlen int) ([]byte, error) { hashlen := hash.Size() reps := (kdlen + hashlen - 1) / hashlen - if reps > math.MaxUint32 { + if uint64(reps) > math.MaxUint32 { return nil, ErrKeyDataTooLong // prevent counter overflow } counter := []byte{0, 0, 0, 0} From f296300509a845730f74d5870bfa9a4cc0ae43ba Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 8 Dec 2015 20:29:07 +0100 Subject: [PATCH 6/6] p2p/rlpx: prefer returning new queued packets over errors This fixes intermittent failures in the p2p tests. --- p2p/rlpx/rlpx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/rlpx/rlpx.go b/p2p/rlpx/rlpx.go index 49d78aeffa..704a23324f 100644 --- a/p2p/rlpx/rlpx.go +++ b/p2p/rlpx/rlpx.go @@ -332,7 +332,7 @@ func (p *Protocol) ReadPacket() (totalSize uint32, r io.Reader, err error) { for len(p.newPackets) == 0 && p.readErr == nil { p.readCond.Wait() } - if p.readErr != nil { + if len(p.newPackets) == 0 && p.readErr != nil { return 0, nil, p.readErr } pr := p.newPackets[0]