From c8a8aa0d43336491f6aa264467968e06b489d34c Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 07:59:54 +0000 Subject: initial hook for crypto handshake (void, off by default) --- p2p/peer.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'p2p') diff --git a/p2p/peer.go b/p2p/peer.go index 2380a3285..886b95a80 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -70,6 +70,7 @@ type Peer struct { // These fields maintain the running protocols. protocols []Protocol runBaseProtocol bool // for testing + cryptoHandshake bool // for testing runlock sync.RWMutex // protects running running map[string]*proto @@ -141,6 +142,20 @@ func (p *Peer) Identity() ClientIdentity { return p.identity } +func (self *Peer) Pubkey() (pubkey []byte) { + self.infolock.Lock() + defer self.infolock.Unlock() + switch { + case self.identity != nil: + pubkey = self.identity.Pubkey() + case self.dialAddr != nil: + pubkey = self.dialAddr.Pubkey + case self.listenAddr != nil: + pubkey = self.listenAddr.Pubkey + } + return +} + // Caps returns the capabilities (supported subprotocols) of the remote peer. func (p *Peer) Caps() []Cap { p.infolock.Lock() @@ -207,6 +222,12 @@ func (p *Peer) loop() (reason DiscReason, err error) { defer close(p.closed) defer p.conn.Close() + if p.cryptoHandshake { + if err := p.handleCryptoHandshake(); err != nil { + return DiscProtocolError, err // no graceful disconnect + } + } + // read loop readMsg := make(chan Msg) readErr := make(chan error) @@ -307,6 +328,11 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) return wait, nil } +func (p *Peer) handleCryptoHandshake() (err error) { + + return nil +} + func (p *Peer) startBaseProtocol() { p.runlock.Lock() defer p.runlock.Unlock() -- cgit v1.2.3 From 88167f39a6437e282ff75a6ec30e8081d6d50441 Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 08:03:39 +0000 Subject: add privkey to clientIdentity + tests --- p2p/client_identity.go | 15 +++++++++++---- p2p/client_identity_test.go | 11 ++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) (limited to 'p2p') diff --git a/p2p/client_identity.go b/p2p/client_identity.go index f15fd01bf..fca2756bd 100644 --- a/p2p/client_identity.go +++ b/p2p/client_identity.go @@ -7,8 +7,9 @@ import ( // ClientIdentity represents the identity of a peer. type ClientIdentity interface { - String() string // human readable identity - Pubkey() []byte // 512-bit public key + String() string // human readable identity + Pubkey() []byte // 512-bit public key + PrivKey() []byte // 512-bit private key } type SimpleClientIdentity struct { @@ -17,10 +18,11 @@ type SimpleClientIdentity struct { customIdentifier string os string implementation string + privkey []byte pubkey []byte } -func NewSimpleClientIdentity(clientIdentifier string, version string, customIdentifier string, pubkey []byte) *SimpleClientIdentity { +func NewSimpleClientIdentity(clientIdentifier string, version string, customIdentifier string, privkey []byte, pubkey []byte) *SimpleClientIdentity { clientIdentity := &SimpleClientIdentity{ clientIdentifier: clientIdentifier, version: version, @@ -28,6 +30,7 @@ func NewSimpleClientIdentity(clientIdentifier string, version string, customIden os: runtime.GOOS, implementation: runtime.Version(), pubkey: pubkey, + privkey: privkey, } return clientIdentity @@ -50,8 +53,12 @@ func (c *SimpleClientIdentity) String() string { c.implementation) } +func (c *SimpleClientIdentity) Privkey() []byte { + return c.privkey +} + func (c *SimpleClientIdentity) Pubkey() []byte { - return []byte(c.pubkey) + return c.pubkey } func (c *SimpleClientIdentity) SetCustomIdentifier(customIdentifier string) { diff --git a/p2p/client_identity_test.go b/p2p/client_identity_test.go index 7248a7b1a..61c34fbf1 100644 --- a/p2p/client_identity_test.go +++ b/p2p/client_identity_test.go @@ -1,13 +1,22 @@ package p2p import ( + "bytes" "fmt" "runtime" "testing" ) func TestClientIdentity(t *testing.T) { - clientIdentity := NewSimpleClientIdentity("Ethereum(G)", "0.5.16", "test", []byte("pubkey")) + clientIdentity := NewSimpleClientIdentity("Ethereum(G)", "0.5.16", "test", []byte("privkey"), []byte("pubkey")) + key := clientIdentity.Privkey() + if !bytes.Equal(key, []byte("privkey")) { + t.Errorf("Expected Privkey to be %x, got %x", key, []byte("privkey")) + } + key = clientIdentity.Pubkey() + if !bytes.Equal(key, []byte("pubkey")) { + t.Errorf("Expected Pubkey to be %x, got %x", key, []byte("pubkey")) + } clientString := clientIdentity.String() expected := fmt.Sprintf("Ethereum(G)/v0.5.16/test/%s/%s", runtime.GOOS, runtime.Version()) if clientString != expected { -- cgit v1.2.3 From d227f6184e9d8ce21d40d032dedc910b2bd25f89 Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 09:44:49 +0000 Subject: fix protocol to accomodate privkey --- p2p/protocol.go | 4 ++++ p2p/protocol_test.go | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/protocol.go b/p2p/protocol.go index 1d121a885..62ada929d 100644 --- a/p2p/protocol.go +++ b/p2p/protocol.go @@ -64,6 +64,10 @@ func (h *handshake) Pubkey() []byte { return h.NodeID } +func (h *handshake) PrivKey() []byte { + return nil +} + // Cap is the structure of a peer capability. type Cap struct { Name string diff --git a/p2p/protocol_test.go b/p2p/protocol_test.go index b1d10ac53..6804a9d40 100644 --- a/p2p/protocol_test.go +++ b/p2p/protocol_test.go @@ -11,7 +11,7 @@ import ( ) type peerId struct { - pubkey []byte + privKey, pubkey []byte } func (self *peerId) String() string { @@ -27,6 +27,15 @@ func (self *peerId) Pubkey() (pubkey []byte) { return } +func (self *peerId) PrivKey() (privKey []byte) { + privKey = self.privKey + if len(privKey) == 0 { + privKey = crypto.GenerateNewKeyPair().PublicKey + self.privKey = privKey + } + return +} + func newTestPeer() (peer *Peer) { peer = NewPeer(&peerId{}, []Cap{}) peer.pubkeyHook = func(*peerAddr) error { return nil } -- cgit v1.2.3 From 4e52adb84a2cda50877aa92584c9d1675cf51b62 Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 09:46:08 +0000 Subject: add crypto auth logic to p2p --- p2p/crypto.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 p2p/crypto.go (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go new file mode 100644 index 000000000..9204fa9d0 --- /dev/null +++ b/p2p/crypto.go @@ -0,0 +1,174 @@ +package p2p + +import ( + "bytes" + "crypto/ecdsa" + "crypto/rand" + "fmt" + "io" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/obscuren/ecies" + "github.com/obscuren/secp256k1-go" +) + +var ( + skLen int = 32 // ecies.MaxSharedKeyLength(pubKey) / 2 + sigLen int = 32 // elliptic S256 + pubKeyLen int = 32 // ECDSA + msgLen int = sigLen + 1 + pubKeyLen + skLen // 97 +) + +//, aesSecret, macSecret, egressMac, ingress +type secretRW struct { + aesSecret, macSecret, egressMac, ingressMac []byte +} + +type cryptoId struct { + prvKey *ecdsa.PrivateKey + pubKey *ecdsa.PublicKey + pubKeyR io.ReaderAt +} + +func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { + // will be at server init + var prvKeyDER []byte = id.PrivKey() + if prvKeyDER == nil { + err = fmt.Errorf("no private key for client") + return + } + // initialise ecies private key via importing DER encoded keys (known via our own clientIdentity) + var prvKey = crypto.ToECDSA(prvKeyDER) + if prvKey == nil { + err = fmt.Errorf("invalid private key for client") + return + } + self = &cryptoId{ + prvKey: prvKey, + // initialise public key from the imported private key + pubKey: &prvKey.PublicKey, + // to be created at server init shared between peers and sessions + // for reuse, call wth ReadAt, no reset seek needed + } + self.pubKeyR = bytes.NewReader(id.Pubkey()) + return +} + +// +func (self *cryptoId) setupAuth(remotePubKeyDER, sessionToken []byte) (auth []byte, nonce []byte, sharedKnowledge []byte, err error) { + // session init, common to both parties + var remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) + if remotePubKey == nil { + err = fmt.Errorf("invalid remote public key") + return + } + var sharedSecret []byte + // generate shared key from prv and remote pubkey + sharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), skLen, skLen) + if err != nil { + return + } + // check previous session token + if sessionToken == nil { + err = fmt.Errorf("no session token for peer") + return + } + // allocate msgLen long message + var msg []byte = make([]byte, msgLen) + // generate skLen long nonce at the end + nonce = msg[msgLen-skLen:] + if _, err = rand.Read(nonce); err != nil { + return + } + // create known message + // should use + // cipher.xorBytes from crypto/cipher/xor.go for fast xor + sharedKnowledge = Xor(sharedSecret, sessionToken) + var signedMsg = Xor(sharedKnowledge, nonce) + + // generate random keypair to use for signing + var ecdsaRandomPrvKey *ecdsa.PrivateKey + if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { + return + } + // var ecdsaRandomPubKey *ecdsa.PublicKey + // ecdsaRandomPubKey= &ecdsaRandomPrvKey.PublicKey + + // message known to both parties ecdh-shared-secret^nonce^token + var signature []byte + // signature = sign(ecdhe-random, ecdh-shared-secret^nonce^token) + // uses secp256k1.Sign + if signature, err = crypto.Sign(signedMsg, ecdsaRandomPrvKey); err != nil { + return + } + // msg = signature || 0x80 || pubk || nonce + copy(msg, signature) + msg[sigLen] = 0x80 + self.pubKeyR.ReadAt(msg[sigLen+1:], int64(pubKeyLen)) // gives pubKeyLen, io.EOF (since we dont read onto the nonce) + + // auth = eciesEncrypt(remote-pubk, msg) + if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { + return + } + return +} + +func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionToken []byte, rw *secretRW, err error) { + var msg []byte + // they prove that msg is meant for me, + // I prove I possess private key if i can read it + if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { + return + } + + var remoteNonce []byte = msg[msgLen-skLen:] + // I prove that i possess prv key (to derive shared secret, and read nonce off encrypted msg) and that I posessed the earlier one , our shared history + // they prove they possess their private key to derive the same shared secret, plus the same shared history (previous session token) + var signedMsg = Xor(sharedKnowledge, remoteNonce) + var remoteRandomPubKeyDER []byte + if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:32]); err != nil { + return + } + var remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) + if remoteRandomPubKey == nil { + err = fmt.Errorf("invalid remote public key") + return + } + // 3) Now we can trust ecdhe-random-pubk to derive keys + //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) + var dhSharedSecret []byte + dhSharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remoteRandomPubKey), skLen, skLen) + if err != nil { + return + } + // shared-secret = crypto.Sha3(ecdhe-shared-secret || crypto.Sha3(nonce || initiator-nonce)) + var sharedSecret []byte = crypto.Sha3(append(dhSharedSecret, crypto.Sha3(append(nonce, remoteNonce...))...)) + // token = crypto.Sha3(shared-secret) + sessionToken = crypto.Sha3(sharedSecret) + // aes-secret = crypto.Sha3(ecdhe-shared-secret || shared-secret) + var aesSecret = crypto.Sha3(append(dhSharedSecret, sharedSecret...)) + // # destroy shared-secret + // mac-secret = crypto.Sha3(ecdhe-shared-secret || aes-secret) + var macSecret = crypto.Sha3(append(dhSharedSecret, aesSecret...)) + // # destroy ecdhe-shared-secret + // egress-mac = crypto.Sha3(mac-secret^nonce || auth) + var egressMac = crypto.Sha3(append(Xor(macSecret, nonce), auth...)) + // # destroy nonce + // ingress-mac = crypto.Sha3(mac-secret^initiator-nonce || auth), + var ingressMac = crypto.Sha3(append(Xor(macSecret, remoteNonce), auth...)) + // # destroy remote-nonce + rw = &secretRW{ + aesSecret: aesSecret, + macSecret: macSecret, + egressMac: egressMac, + ingressMac: ingressMac, + } + return +} + +func Xor(one, other []byte) (xor []byte) { + for i := 0; i < len(one); i++ { + xor[i] = one[i] ^ other[i] + } + return +} -- cgit v1.2.3 From b855f671a508a7e8160cbdd27197ba83310b264c Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 18 Jan 2015 23:53:45 +0000 Subject: rewrite to comply with latest spec - correct sizes for the blocks : sec signature 65, ecies sklen 16, keylength 32 - added allocation to Xor (should be optimized later) - no pubkey reader needed, just do with copy - restructuring now into INITIATE, RESPOND, COMPLETE -> newSession initialises the encryption/authentication layer - crypto identity can be part of client identity, some initialisation when server created --- p2p/crypto.go | 191 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 138 insertions(+), 53 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 9204fa9d0..6e3f360d9 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -1,11 +1,11 @@ package p2p import ( - "bytes" + // "bytes" "crypto/ecdsa" "crypto/rand" "fmt" - "io" + // "io" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" @@ -13,21 +13,22 @@ import ( ) var ( - skLen int = 32 // ecies.MaxSharedKeyLength(pubKey) / 2 - sigLen int = 32 // elliptic S256 - pubKeyLen int = 32 // ECDSA - msgLen int = sigLen + 1 + pubKeyLen + skLen // 97 + sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 + sigLen int = 65 // elliptic S256 + keyLen int = 32 // ECDSA + msgLen int = sigLen + 3*keyLen + 1 // 162 + resLen int = 65 ) -//, aesSecret, macSecret, egressMac, ingress +// aesSecret, macSecret, egressMac, ingress type secretRW struct { aesSecret, macSecret, egressMac, ingressMac []byte } type cryptoId struct { - prvKey *ecdsa.PrivateKey - pubKey *ecdsa.PublicKey - pubKeyR io.ReaderAt + prvKey *ecdsa.PrivateKey + pubKey *ecdsa.PublicKey + pubKeyDER []byte } func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { @@ -50,99 +51,181 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // to be created at server init shared between peers and sessions // for reuse, call wth ReadAt, no reset seek needed } - self.pubKeyR = bytes.NewReader(id.Pubkey()) + self.pubKeyDER = id.Pubkey() return } -// -func (self *cryptoId) setupAuth(remotePubKeyDER, sessionToken []byte) (auth []byte, nonce []byte, sharedKnowledge []byte, err error) { +// initAuth is called by peer if it initiated the connection +func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties - var remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) + remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) if remotePubKey == nil { err = fmt.Errorf("invalid remote public key") return } - var sharedSecret []byte - // generate shared key from prv and remote pubkey - sharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), skLen, skLen) - if err != nil { - return - } - // check previous session token + + var tokenFlag byte if sessionToken == nil { - err = fmt.Errorf("no session token for peer") - return + // 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 + if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { + return + } + fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) + // tokenFlag = 0x00 // redundant + } else { + // for known peers, we use stored token from the previous session + tokenFlag = 0x01 } - // allocate msgLen long message + + //E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) + // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) + // allocate msgLen long message, var msg []byte = make([]byte, msgLen) - // generate skLen long nonce at the end - nonce = msg[msgLen-skLen:] - if _, err = rand.Read(nonce); err != nil { + // generate sskLen long nonce + initNonce = msg[msgLen-keyLen-1 : msgLen-1] + // nonce = msg[msgLen-sskLen-1 : msgLen-1] + if _, err = rand.Read(initNonce); err != nil { return } // create known message - // should use - // cipher.xorBytes from crypto/cipher/xor.go for fast xor - sharedKnowledge = Xor(sharedSecret, sessionToken) - var signedMsg = Xor(sharedKnowledge, nonce) + // ecdh-shared-secret^nonce for new peers + // token^nonce for old peers + var sharedSecret = Xor(sessionToken, initNonce) // generate random keypair to use for signing var ecdsaRandomPrvKey *ecdsa.PrivateKey if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { return } - // var ecdsaRandomPubKey *ecdsa.PublicKey - // ecdsaRandomPubKey= &ecdsaRandomPrvKey.PublicKey - - // message known to both parties ecdh-shared-secret^nonce^token + // sign shared secret (message known to both parties): shared-secret var signature []byte - // signature = sign(ecdhe-random, ecdh-shared-secret^nonce^token) + // signature = sign(ecdhe-random, shared-secret) // uses secp256k1.Sign - if signature, err = crypto.Sign(signedMsg, ecdsaRandomPrvKey); err != nil { + if signature, err = crypto.Sign(sharedSecret, ecdsaRandomPrvKey); err != nil { return } - // msg = signature || 0x80 || pubk || nonce - copy(msg, signature) - msg[sigLen] = 0x80 - self.pubKeyR.ReadAt(msg[sigLen+1:], int64(pubKeyLen)) // gives pubKeyLen, io.EOF (since we dont read onto the nonce) + fmt.Printf("signature generated: %v %x", len(signature), signature) + // message + // signed-shared-secret || H(ecdhe-random-pubk) || pubk || nonce || 0x0 + copy(msg, signature) // copy signed-shared-secret + // H(ecdhe-random-pubk) + copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey))) + // pubkey copied to the correct segment. + copy(msg[sigLen+keyLen:sigLen+2*keyLen], self.pubKeyDER) + // nonce is already in the slice + // stick tokenFlag byte to the end + msg[msgLen-1] = tokenFlag + + fmt.Printf("plaintext message generated: %v %x", len(msg), msg) + + // encrypt using remote-pubk // auth = eciesEncrypt(remote-pubk, msg) + if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { return } + fmt.Printf("encrypted message generated: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(remotePubKey)) + return } -func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionToken []byte, rw *secretRW, err error) { +// verifyAuth is called by peer if it accepted (but not initiated) the connection +func (self *cryptoId) verifyAuth(auth, sharedSecret []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, err error) { var msg []byte + fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, // I prove I possess private key if i can read it if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { return } - var remoteNonce []byte = msg[msgLen-skLen:] - // I prove that i possess prv key (to derive shared secret, and read nonce off encrypted msg) and that I posessed the earlier one , our shared history - // they prove they possess their private key to derive the same shared secret, plus the same shared history (previous session token) - var signedMsg = Xor(sharedKnowledge, remoteNonce) + // var remoteNonce []byte = msg[msgLen-skLen-1 : msgLen-1] + initNonce = msg[msgLen-keyLen-1 : msgLen-1] + // I prove that i own prv key (to derive shared secret, and read nonce off encrypted msg) and that I own shared secret + // they prove they own the private key belonging to ecdhe-random-pubk + var signedMsg = Xor(sharedSecret, initNonce) var remoteRandomPubKeyDER []byte - if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:32]); err != nil { + if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { return } - var remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) + remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) if remoteRandomPubKey == nil { err = fmt.Errorf("invalid remote public key") return } - // 3) Now we can trust ecdhe-random-pubk to derive keys + + var resp = make([]byte, 2*keyLen+1) + // generate sskLen long nonce + respNonce = msg[msgLen-keyLen-1 : msgLen-1] + if _, err = rand.Read(respNonce); err != nil { + return + } + // generate random keypair + var ecdsaRandomPrvKey *ecdsa.PrivateKey + if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { + return + } + // var ecdsaRandomPubKey *ecdsa.PublicKey + // ecdsaRandomPubKey= &ecdsaRandomPrvKey.PublicKey + + // message + // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) + copy(resp[:keyLen], crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey)) + // pubkey copied to the correct segment. + copy(resp[keyLen:2*keyLen], self.pubKeyDER) + // nonce is already in the slice + // stick tokenFlag byte to the end + var tokenFlag byte + if sharedSecret == nil { + } else { + // for known peers, we use stored token from the previous session + tokenFlag = 0x01 + } + resp[resLen] = tokenFlag + + // encrypt using remote-pubk + // auth = eciesEncrypt(remote-pubk, msg) + // why not encrypt with ecdhe-random-remote + if authResp, err = crypto.Encrypt(remotePubKey, resp); err != nil { + return + } + return +} + +func (self *cryptoId) verifyAuthResp(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { + var msg []byte + // they prove that msg is meant for me, + // I prove I possess private key if i can read it + if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { + return + } + + respNonce = msg[resLen-keyLen-1 : resLen-1] + var remoteRandomPubKeyDER = msg[:keyLen] + remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) + if remoteRandomPubKey == nil { + err = fmt.Errorf("invalid ecdh random remote public key") + return + } + if msg[resLen-1] == 0x01 { + tokenFlag = true + } + return +} + +func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { + // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte - dhSharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remoteRandomPubKey), skLen, skLen) + dhSharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remoteRandomPubKey), sskLen, sskLen) if err != nil { return } // shared-secret = crypto.Sha3(ecdhe-shared-secret || crypto.Sha3(nonce || initiator-nonce)) - var sharedSecret []byte = crypto.Sha3(append(dhSharedSecret, crypto.Sha3(append(nonce, remoteNonce...))...)) + var sharedSecret = crypto.Sha3(append(dhSharedSecret, crypto.Sha3(append(respNonce, initNonce...))...)) // token = crypto.Sha3(shared-secret) sessionToken = crypto.Sha3(sharedSecret) // aes-secret = crypto.Sha3(ecdhe-shared-secret || shared-secret) @@ -152,10 +235,10 @@ func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionTo var macSecret = crypto.Sha3(append(dhSharedSecret, aesSecret...)) // # destroy ecdhe-shared-secret // egress-mac = crypto.Sha3(mac-secret^nonce || auth) - var egressMac = crypto.Sha3(append(Xor(macSecret, nonce), auth...)) + var egressMac = crypto.Sha3(append(Xor(macSecret, respNonce), auth...)) // # destroy nonce // ingress-mac = crypto.Sha3(mac-secret^initiator-nonce || auth), - var ingressMac = crypto.Sha3(append(Xor(macSecret, remoteNonce), auth...)) + var ingressMac = crypto.Sha3(append(Xor(macSecret, initNonce), auth...)) // # destroy remote-nonce rw = &secretRW{ aesSecret: aesSecret, @@ -166,7 +249,9 @@ func (self *cryptoId) verifyAuth(auth, nonce, sharedKnowledge []byte) (sessionTo return } +// should use cipher.xorBytes from crypto/cipher/xor.go for fast xor 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] } -- cgit v1.2.3 From 714b955d6e03a822af80b566b0fa73098761f57a Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 00:55:24 +0000 Subject: fix crash - add session token check and fallback to shared secret in responder call too - use explicit length for the types of new messages - fix typo resp[resLen-1] = tokenFlag --- p2p/crypto.go | 51 +++++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 22 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 6e3f360d9..643bd431e 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -17,7 +17,7 @@ var ( sigLen int = 65 // elliptic S256 keyLen int = 32 // ECDSA msgLen int = sigLen + 3*keyLen + 1 // 162 - resLen int = 65 + resLen int = 65 // ) // aesSecret, macSecret, egressMac, ingress @@ -133,7 +133,7 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt } // verifyAuth is called by peer if it accepted (but not initiated) the connection -func (self *cryptoId) verifyAuth(auth, sharedSecret []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, err error) { +func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, err error) { var msg []byte fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, @@ -141,50 +141,57 @@ func (self *cryptoId) verifyAuth(auth, sharedSecret []byte, remotePubKey *ecdsa. if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { return } + fmt.Printf("\nplaintext message retrieved: %v %x\n", len(msg), msg) - // var remoteNonce []byte = msg[msgLen-skLen-1 : msgLen-1] + var tokenFlag byte + if sessionToken == 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 + if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { + return + } + fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) + // tokenFlag = 0x00 // redundant + } else { + // for known peers, we use stored token from the previous session + tokenFlag = 0x01 + } + + // the initiator nonce is read off the end of the message initNonce = msg[msgLen-keyLen-1 : msgLen-1] // I prove that i own prv key (to derive shared secret, and read nonce off encrypted msg) and that I own shared secret // they prove they own the private key belonging to ecdhe-random-pubk - var signedMsg = Xor(sharedSecret, initNonce) + // we can now reconstruct the signed message and recover the peers pubkey + var signedMsg = Xor(sessionToken, initNonce) var remoteRandomPubKeyDER []byte if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { return } + // convert to ECDSA standard remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) if remoteRandomPubKey == nil { err = fmt.Errorf("invalid remote public key") return } - var resp = make([]byte, 2*keyLen+1) - // generate sskLen long nonce - respNonce = msg[msgLen-keyLen-1 : msgLen-1] + // now we find ourselves a long task too, fill it random + var resp = make([]byte, resLen) + // generate keyLen long nonce + respNonce = msg[resLen-keyLen-1 : msgLen-1] if _, err = rand.Read(respNonce); err != nil { return } - // generate random keypair + // generate random keypair for session var ecdsaRandomPrvKey *ecdsa.PrivateKey if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { return } - // var ecdsaRandomPubKey *ecdsa.PublicKey - // ecdsaRandomPubKey= &ecdsaRandomPrvKey.PublicKey - - // message + // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) copy(resp[:keyLen], crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey)) - // pubkey copied to the correct segment. - copy(resp[keyLen:2*keyLen], self.pubKeyDER) // nonce is already in the slice - // stick tokenFlag byte to the end - var tokenFlag byte - if sharedSecret == nil { - } else { - // for known peers, we use stored token from the previous session - tokenFlag = 0x01 - } - resp[resLen] = tokenFlag + resp[resLen-1] = tokenFlag // encrypt using remote-pubk // auth = eciesEncrypt(remote-pubk, msg) -- cgit v1.2.3 From 3b6385b14624faee26445a3d98fa94efdb30d29a Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 01:24:09 +0000 Subject: handshake test to crypto --- p2p/crypto.go | 2 -- p2p/crypto_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 p2p/crypto_test.go (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 643bd431e..10c82d3a1 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -1,11 +1,9 @@ package p2p import ( - // "bytes" "crypto/ecdsa" "crypto/rand" "fmt" - // "io" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go new file mode 100644 index 000000000..6b4afb16a --- /dev/null +++ b/p2p/crypto_test.go @@ -0,0 +1,54 @@ +package p2p + +import ( + // "bytes" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/crypto" +) + +func TestCryptoHandshake(t *testing.T) { + var err error + var sessionToken []byte + prvInit, _ := crypto.GenerateKey() + pubInit := &prvInit.PublicKey + prvResp, _ := crypto.GenerateKey() + pubResp := &prvResp.PublicKey + + var initiator, responder *cryptoId + if initiator, err = newCryptoId(&peerId{crypto.FromECDSA(prvInit), crypto.FromECDSAPub(pubInit)}); err != nil { + return + } + if responder, err = newCryptoId(&peerId{crypto.FromECDSA(prvResp), crypto.FromECDSAPub(pubResp)}); err != nil { + return + } + + auth, initNonce, _, _ := initiator.initAuth(responder.pubKeyDER, sessionToken) + + response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) + + respNonce, randomPubKey, _, _ := initiator.verifyAuthResp(response) + + fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey) + // initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPubKey) + // respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPubKey) + + // if !bytes.Equal(initSessionToken, respSessionToken) { + // t.Errorf("session tokens do not match") + // } + // // aesSecret, macSecret, egressMac, ingressMac + // if !bytes.Equal(initSecretRW.aesSecret, respSecretRW.aesSecret) { + // t.Errorf("AES secrets do not match") + // } + // if !bytes.Equal(initSecretRW.macSecret, respSecretRW.macSecret) { + // t.Errorf("macSecrets do not match") + // } + // if !bytes.Equal(initSecretRW.egressMac, respSecretRW.egressMac) { + // t.Errorf("egressMacs do not match") + // } + // if !bytes.Equal(initSecretRW.ingressMac, respSecretRW.ingressMac) { + // t.Errorf("ingressMacs do not match") + // } + +} -- cgit v1.2.3 From 076c382a7486ebf58f33e1e0df49e92dc877ea19 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 01:24:28 +0000 Subject: handshake test to crypto --- p2p/crypto_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 6b4afb16a..1785b5c45 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -31,7 +31,7 @@ func TestCryptoHandshake(t *testing.T) { respNonce, randomPubKey, _, _ := initiator.verifyAuthResp(response) fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey) - // initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPubKey) + initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPubKey) // respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPubKey) // if !bytes.Equal(initSessionToken, respSessionToken) { -- cgit v1.2.3 From 489d956283390b701473edd4a597afea2c426d41 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 04:53:48 +0000 Subject: completed the test. FAIL now. it crashes at diffie-hellman. ECIES -> secp256k1-go panics --- p2p/crypto.go | 45 ++++++++++++++++++++++++++++---------------- p2p/crypto_test.go | 55 +++++++++++++++++++++++++++--------------------------- 2 files changed, 57 insertions(+), 43 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 10c82d3a1..37c6e1fc9 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -53,10 +53,24 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { return } -// initAuth is called by peer if it initiated the connection -func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, remotePubKey *ecdsa.PublicKey, err error) { +/* startHandshake is called by peer if it initiated the connection. + By protocol spec, the party who initiates the connection (initiator) will send an 'auth' packet +New: authInitiator -> E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) + authRecipient -> E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) + +Known: authInitiator = E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) + authRecipient = E(remote-pubk, ecdhe-random-pubk || nonce || 0x1) // token found + authRecipient = E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) // token not found + +The caller provides the public key of the peer as conjuctured from lookup based on IP:port, given as user input or proven by signatures. The caller must have access to persistant information about the peers, and pass the previous session token as an argument to cryptoId. + +The handshake is the process by which the peers establish their connection for a session. + +*/ + +func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, randomPubKey *ecdsa.PublicKey, err error) { // session init, common to both parties - remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) + remotePubKey := crypto.ToECDSAPub(remotePubKeyDER) if remotePubKey == nil { err = fmt.Errorf("invalid remote public key") return @@ -70,6 +84,7 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } + // this will not stay here ;) fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) // tokenFlag = 0x00 // redundant } else { @@ -93,15 +108,14 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt var sharedSecret = Xor(sessionToken, initNonce) // generate random keypair to use for signing - var ecdsaRandomPrvKey *ecdsa.PrivateKey - if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { + if randomPrvKey, err = crypto.GenerateKey(); err != nil { return } // sign shared secret (message known to both parties): shared-secret var signature []byte // signature = sign(ecdhe-random, shared-secret) // uses secp256k1.Sign - if signature, err = crypto.Sign(sharedSecret, ecdsaRandomPrvKey); err != nil { + if signature, err = crypto.Sign(sharedSecret, randomPrvKey); err != nil { return } fmt.Printf("signature generated: %v %x", len(signature), signature) @@ -110,7 +124,7 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt // signed-shared-secret || H(ecdhe-random-pubk) || pubk || nonce || 0x0 copy(msg, signature) // copy signed-shared-secret // H(ecdhe-random-pubk) - copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey))) + copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(crypto.FromECDSAPub(&randomPrvKey.PublicKey))) // pubkey copied to the correct segment. copy(msg[sigLen+keyLen:sigLen+2*keyLen], self.pubKeyDER) // nonce is already in the slice @@ -131,7 +145,7 @@ func (self *cryptoId) initAuth(remotePubKeyDER, sessionToken []byte) (auth []byt } // verifyAuth is called by peer if it accepted (but not initiated) the connection -func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, err error) { +func (self *cryptoId) respondToHandshake(auth, sessionToken []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, err error) { var msg []byte fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, @@ -167,7 +181,7 @@ func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa. return } // convert to ECDSA standard - remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) + remoteRandomPubKey := crypto.ToECDSAPub(remoteRandomPubKeyDER) if remoteRandomPubKey == nil { err = fmt.Errorf("invalid remote public key") return @@ -181,13 +195,12 @@ func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa. return } // generate random keypair for session - var ecdsaRandomPrvKey *ecdsa.PrivateKey - if ecdsaRandomPrvKey, err = crypto.GenerateKey(); err != nil { + if randomPrvKey, err = crypto.GenerateKey(); err != nil { return } // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - copy(resp[:keyLen], crypto.FromECDSAPub(&ecdsaRandomPrvKey.PublicKey)) + copy(resp[:keyLen], crypto.FromECDSAPub(&randomPrvKey.PublicKey)) // nonce is already in the slice resp[resLen-1] = tokenFlag @@ -200,7 +213,7 @@ func (self *cryptoId) verifyAuth(auth, sessionToken []byte, remotePubKey *ecdsa. return } -func (self *cryptoId) verifyAuthResp(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { +func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { var msg []byte // they prove that msg is meant for me, // I prove I possess private key if i can read it @@ -221,12 +234,12 @@ func (self *cryptoId) verifyAuthResp(auth []byte) (respNonce []byte, remoteRando return } -func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { +func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte - dhSharedSecret, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remoteRandomPubKey), sskLen, sskLen) - if err != nil { + pubKey := ecies.ImportECDSAPublic(remoteRandomPubKey) + if dhSharedSecret, err = ecies.ImportECDSA(privKey).GenerateShared(pubKey, sskLen, sskLen); err != nil { return } // shared-secret = crypto.Sha3(ecdhe-shared-secret || crypto.Sha3(nonce || initiator-nonce)) diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 1785b5c45..cfb2d19d1 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -1,7 +1,7 @@ package p2p import ( - // "bytes" + "bytes" "fmt" "testing" @@ -24,31 +24,32 @@ func TestCryptoHandshake(t *testing.T) { return } - auth, initNonce, _, _ := initiator.initAuth(responder.pubKeyDER, sessionToken) - - response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) - - respNonce, randomPubKey, _, _ := initiator.verifyAuthResp(response) - - fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey) - initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPubKey) - // respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPubKey) - - // if !bytes.Equal(initSessionToken, respSessionToken) { - // t.Errorf("session tokens do not match") - // } - // // aesSecret, macSecret, egressMac, ingressMac - // if !bytes.Equal(initSecretRW.aesSecret, respSecretRW.aesSecret) { - // t.Errorf("AES secrets do not match") - // } - // if !bytes.Equal(initSecretRW.macSecret, respSecretRW.macSecret) { - // t.Errorf("macSecrets do not match") - // } - // if !bytes.Equal(initSecretRW.egressMac, respSecretRW.egressMac) { - // t.Errorf("egressMacs do not match") - // } - // if !bytes.Equal(initSecretRW.ingressMac, respSecretRW.ingressMac) { - // t.Errorf("ingressMacs do not match") - // } + auth, initNonce, randomPrvKey, randomPubKey, _ := initiator.initAuth(responder.pubKeyDER, sessionToken) + + response, remoteRespNonce, remoteInitNonce, remoteRandomPrivKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) + + respNonce, remoteRandomPubKey, _, _ := initiator.verifyAuthResp(response) + + initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPrvKey, remoteRandomPubKey) + respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPrivKey, randomPubKey) + + fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey, initSessionToken, initSecretRW) + + if !bytes.Equal(initSessionToken, respSessionToken) { + t.Errorf("session tokens do not match") + } + // aesSecret, macSecret, egressMac, ingressMac + if !bytes.Equal(initSecretRW.aesSecret, respSecretRW.aesSecret) { + t.Errorf("AES secrets do not match") + } + if !bytes.Equal(initSecretRW.macSecret, respSecretRW.macSecret) { + t.Errorf("macSecrets do not match") + } + if !bytes.Equal(initSecretRW.egressMac, respSecretRW.egressMac) { + t.Errorf("egressMacs do not match") + } + if !bytes.Equal(initSecretRW.ingressMac, respSecretRW.ingressMac) { + t.Errorf("ingressMacs do not match") + } } -- cgit v1.2.3 From 1803c65e4097b9d6cb83f72a8a09aeddcc01f685 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 11:21:13 +0000 Subject: integrate cryptoId into peer and connection lifecycle --- p2p/crypto.go | 15 +++++++++++++++ p2p/peer.go | 21 ++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 37c6e1fc9..728b8e884 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -53,6 +53,21 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { return } +func (self *cryptoId) Run(remotePubKeyDER []byte) (rw *secretRW) { + if self.initiator { + auth, initNonce, randomPrvKey, randomPubKey, err := initiator.initAuth(remotePubKeyDER, sessionToken) + + respNonce, remoteRandomPubKey, _, _ := initiator.verifyAuthResp(response) + } else { + // we are listening connection. we are responders in the haandshake. + // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. + response, remoteRespNonce, remoteInitNonce, remoteRandomPrivKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) + + } + initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPrvKey, remoteRandomPubKey) + respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPrivKey, randomPubKey) +} + /* startHandshake is called by peer if it initiated the connection. By protocol spec, the party who initiates the connection (initiator) will send an 'auth' packet New: authInitiator -> E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) diff --git a/p2p/peer.go b/p2p/peer.go index 886b95a80..e98c3d560 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -222,10 +222,14 @@ func (p *Peer) loop() (reason DiscReason, err error) { defer close(p.closed) defer p.conn.Close() + var readLoop func(chan Msg, chan error, chan bool) if p.cryptoHandshake { - if err := p.handleCryptoHandshake(); err != nil { + if readLoop, err := p.handleCryptoHandshake(); err != nil { + // from here on everything can be encrypted, authenticated return DiscProtocolError, err // no graceful disconnect } + } else { + readLoop = p.readLoop } // read loop @@ -233,7 +237,7 @@ func (p *Peer) loop() (reason DiscReason, err error) { readErr := make(chan error) readNext := make(chan bool, 1) protoDone := make(chan struct{}, 1) - go p.readLoop(readMsg, readErr, readNext) + go readLoop(readMsg, readErr, readNext) readNext <- true if p.runBaseProtocol { @@ -329,8 +333,19 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) } func (p *Peer) handleCryptoHandshake() (err error) { + // cryptoId is just created for the lifecycle of the handshake + // it is survived by an encrypted readwriter + if p.dialAddr != 0 { // this should have its own method Outgoing() bool + initiator = true + } + // create crypto layer + cryptoId := newCryptoId(p.identity, initiator, sessionToken) + // run on peer + if rw, err := cryptoId.Run(p.Pubkey()); err != nil { + return err + } + p.conn = rw.Run(p.conn) - return nil } func (p *Peer) startBaseProtocol() { -- cgit v1.2.3 From e252c634cb40c8ef7f9bcd542f5418a937929620 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 19 Jan 2015 23:42:13 +0000 Subject: first stab at integrating crypto in our p2p - abstract the entire handshake logic in cryptoId.Run() taking session-relevant parameters - changes in peer to accomodate how the encryption layer would be switched on - modify arguments of handshake components - fixed test getting the wrong pubkey but it till crashes on DH in newSession() --- p2p/crypto.go | 53 ++++++++++++++++++++++++++++++++++++++--------------- p2p/crypto_test.go | 39 +++++++++++++++++++-------------------- p2p/peer.go | 31 ++++++++++++++++++++++--------- 3 files changed, 79 insertions(+), 44 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 728b8e884..b6d600826 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "crypto/rand" "fmt" + "io" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" @@ -53,19 +54,35 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { return } -func (self *cryptoId) Run(remotePubKeyDER []byte) (rw *secretRW) { - if self.initiator { - auth, initNonce, randomPrvKey, randomPubKey, err := initiator.initAuth(remotePubKeyDER, sessionToken) - - respNonce, remoteRandomPubKey, _, _ := initiator.verifyAuthResp(response) +func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyDER []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { + var auth, initNonce, recNonce []byte + var randomPrivKey *ecdsa.PrivateKey + var remoteRandomPubKey *ecdsa.PublicKey + if initiator { + if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyDER, sessionToken); err != nil { + return + } + conn.Write(auth) + var response []byte + conn.Read(response) + // write out auth message + // wait for response, then call complete + if recNonce, remoteRandomPubKey, _, err = self.completeHandshake(response); err != nil { + return + } } else { - // we are listening connection. we are responders in the haandshake. + conn.Read(auth) + // we are listening connection. we are responders in the handshake. // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. - response, remoteRespNonce, remoteInitNonce, remoteRandomPrivKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) - + // so we read auth message first, then respond + var response []byte + if response, recNonce, initNonce, randomPrivKey, err = self.respondToHandshake(auth, remotePubKeyDER, sessionToken); err != nil { + return + } + remoteRandomPubKey = &randomPrivKey.PublicKey + conn.Write(response) } - initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPrvKey, remoteRandomPubKey) - respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPrivKey, randomPubKey) + return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } /* startHandshake is called by peer if it initiated the connection. @@ -83,9 +100,9 @@ The handshake is the process by which the peers establish their connection for a */ -func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, randomPubKey *ecdsa.PublicKey, err error) { +func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties - remotePubKey := crypto.ToECDSAPub(remotePubKeyDER) + remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) if remotePubKey == nil { err = fmt.Errorf("invalid remote public key") return @@ -160,8 +177,14 @@ func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth } // verifyAuth is called by peer if it accepted (but not initiated) the connection -func (self *cryptoId) respondToHandshake(auth, sessionToken []byte, remotePubKey *ecdsa.PublicKey) (authResp []byte, respNonce []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, err error) { +func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, err error) { var msg []byte + remotePubKey := crypto.ToECDSAPub(remotePubKeyDER) + if remotePubKey == nil { + err = fmt.Errorf("invalid public key") + return + } + fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, // I prove I possess private key if i can read it @@ -210,12 +233,12 @@ func (self *cryptoId) respondToHandshake(auth, sessionToken []byte, remotePubKey return } // generate random keypair for session - if randomPrvKey, err = crypto.GenerateKey(); err != nil { + if randomPrivKey, err = crypto.GenerateKey(); err != nil { return } // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - copy(resp[:keyLen], crypto.FromECDSAPub(&randomPrvKey.PublicKey)) + copy(resp[:keyLen], crypto.FromECDSAPub(&randomPrivKey.PublicKey)) // nonce is already in the slice resp[resLen-1] = tokenFlag diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index cfb2d19d1..fb7df6b50 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -11,44 +11,43 @@ import ( func TestCryptoHandshake(t *testing.T) { var err error var sessionToken []byte - prvInit, _ := crypto.GenerateKey() - pubInit := &prvInit.PublicKey - prvResp, _ := crypto.GenerateKey() - pubResp := &prvResp.PublicKey + prv0, _ := crypto.GenerateKey() + pub0 := &prv0.PublicKey + prv1, _ := crypto.GenerateKey() + pub1 := &prv1.PublicKey - var initiator, responder *cryptoId - if initiator, err = newCryptoId(&peerId{crypto.FromECDSA(prvInit), crypto.FromECDSAPub(pubInit)}); err != nil { + var initiator, receiver *cryptoId + if initiator, err = newCryptoId(&peerId{crypto.FromECDSA(prv0), crypto.FromECDSAPub(pub0)}); err != nil { return } - if responder, err = newCryptoId(&peerId{crypto.FromECDSA(prvResp), crypto.FromECDSAPub(pubResp)}); err != nil { + if receiver, err = newCryptoId(&peerId{crypto.FromECDSA(prv1), crypto.FromECDSAPub(pub1)}); err != nil { return } - auth, initNonce, randomPrvKey, randomPubKey, _ := initiator.initAuth(responder.pubKeyDER, sessionToken) + // simulate handshake by feeding output to input + auth, initNonce, randomPrivKey, _, _ := initiator.startHandshake(receiver.pubKeyDER, sessionToken) + response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, _ := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + recNonce, remoteRandomPubKey, _, _ := initiator.completeHandshake(response) - response, remoteRespNonce, remoteInitNonce, remoteRandomPrivKey, _ := responder.verifyAuth(auth, sessionToken, pubInit) + initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + recSessionToken, recSecretRW, _ := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, &randomPrivKey.PublicKey) - respNonce, remoteRandomPubKey, _, _ := initiator.verifyAuthResp(response) + fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, &randomPrivKey.PublicKey, initSessionToken, initSecretRW) - initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, respNonce, auth, randomPrvKey, remoteRandomPubKey) - respSessionToken, respSecretRW, _ := responder.newSession(remoteInitNonce, remoteRespNonce, auth, remoteRandomPrivKey, randomPubKey) - - fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRespNonce, remoteInitNonce, remoteRandomPubKey, respNonce, randomPubKey, initSessionToken, initSecretRW) - - if !bytes.Equal(initSessionToken, respSessionToken) { + if !bytes.Equal(initSessionToken, recSessionToken) { t.Errorf("session tokens do not match") } // aesSecret, macSecret, egressMac, ingressMac - if !bytes.Equal(initSecretRW.aesSecret, respSecretRW.aesSecret) { + if !bytes.Equal(initSecretRW.aesSecret, recSecretRW.aesSecret) { t.Errorf("AES secrets do not match") } - if !bytes.Equal(initSecretRW.macSecret, respSecretRW.macSecret) { + if !bytes.Equal(initSecretRW.macSecret, recSecretRW.macSecret) { t.Errorf("macSecrets do not match") } - if !bytes.Equal(initSecretRW.egressMac, respSecretRW.egressMac) { + if !bytes.Equal(initSecretRW.egressMac, recSecretRW.egressMac) { t.Errorf("egressMacs do not match") } - if !bytes.Equal(initSecretRW.ingressMac, respSecretRW.ingressMac) { + if !bytes.Equal(initSecretRW.ingressMac, recSecretRW.ingressMac) { t.Errorf("ingressMacs do not match") } diff --git a/p2p/peer.go b/p2p/peer.go index e98c3d560..e3e04ee65 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -222,9 +222,9 @@ func (p *Peer) loop() (reason DiscReason, err error) { defer close(p.closed) defer p.conn.Close() - var readLoop func(chan Msg, chan error, chan bool) + var readLoop func(chan<- Msg, chan<- error, <-chan bool) if p.cryptoHandshake { - if readLoop, err := p.handleCryptoHandshake(); err != nil { + if readLoop, err = p.handleCryptoHandshake(); err != nil { // from here on everything can be encrypted, authenticated return DiscProtocolError, err // no graceful disconnect } @@ -332,20 +332,33 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) return wait, nil } -func (p *Peer) handleCryptoHandshake() (err error) { +type readLoop func(chan<- Msg, chan<- error, <-chan bool) + +func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // cryptoId is just created for the lifecycle of the handshake // it is survived by an encrypted readwriter - if p.dialAddr != 0 { // this should have its own method Outgoing() bool + var initiator bool + var sessionToken []byte + if p.dialAddr != nil { // this should have its own method Outgoing() bool initiator = true } // create crypto layer - cryptoId := newCryptoId(p.identity, initiator, sessionToken) + // this could in principle run only once but maybe we want to allow + // identity switching + var crypto *cryptoId + if crypto, err = newCryptoId(p.ourID); err != nil { + return + } // run on peer - if rw, err := cryptoId.Run(p.Pubkey()); err != nil { - return err + // this bit handles the handshake and creates a secure communications channel with + // var rw *secretRW + if sessionToken, _, err = crypto.Run(p.conn, p.Pubkey(), sessionToken, initiator); err != nil { + return } - p.conn = rw.Run(p.conn) - + loop = func(msg chan<- Msg, err chan<- error, next <-chan bool) { + // this is the readloop :) + } + return } func (p *Peer) startBaseProtocol() { -- cgit v1.2.3 From 2e868566d7f4c97bd4a92c4943919ed52a55e9b1 Mon Sep 17 00:00:00 2001 From: zelig Date: Tue, 20 Jan 2015 00:23:10 +0000 Subject: add minor comments to the test --- p2p/crypto_test.go | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'p2p') diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index fb7df6b50..33cdb3f83 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -25,10 +25,14 @@ func TestCryptoHandshake(t *testing.T) { } // simulate handshake by feeding output to input + // initiator sends handshake 'auth' auth, initNonce, randomPrivKey, _, _ := initiator.startHandshake(receiver.pubKeyDER, sessionToken) + // receiver reads auth and responds with response response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, _ := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + // initiator reads receiver's response and the key exchange completes recNonce, remoteRandomPubKey, _, _ := initiator.completeHandshake(response) + // now both parties should have the same session parameters initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) recSessionToken, recSecretRW, _ := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, &randomPrivKey.PublicKey) -- cgit v1.2.3 From 923504ce3df504a843cfa775d577ac99bdbfdb70 Mon Sep 17 00:00:00 2001 From: zelig Date: Tue, 20 Jan 2015 00:41:45 +0000 Subject: add equality check for nonce and remote nonce --- p2p/crypto_test.go | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'p2p') diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 33cdb3f83..89baca084 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -38,6 +38,12 @@ func TestCryptoHandshake(t *testing.T) { fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, &randomPrivKey.PublicKey, initSessionToken, initSecretRW) + if !bytes.Equal(initNonce, remoteInitNonce) { + t.Errorf("nonces do not match") + } + if !bytes.Equal(recNonce, remoteRecNonce) { + t.Errorf("receiver nonces do not match") + } if !bytes.Equal(initSessionToken, recSessionToken) { t.Errorf("session tokens do not match") } -- cgit v1.2.3 From 58fc2c679b3d3b987aeefc98aa22db5f02717638 Mon Sep 17 00:00:00 2001 From: zelig Date: Tue, 20 Jan 2015 15:20:18 +0000 Subject: important fix for peer pubkey. when taken from identity, chop first format byte! --- p2p/peer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/peer.go b/p2p/peer.go index e3e04ee65..e44eaab34 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -147,7 +147,7 @@ func (self *Peer) Pubkey() (pubkey []byte) { defer self.infolock.Unlock() switch { case self.identity != nil: - pubkey = self.identity.Pubkey() + pubkey = self.identity.Pubkey()[1:] case self.dialAddr != nil: pubkey = self.dialAddr.Pubkey case self.listenAddr != nil: -- cgit v1.2.3 From 364b7832811c19106456b3b30a34c7097a0b526e Mon Sep 17 00:00:00 2001 From: zelig Date: Tue, 20 Jan 2015 16:47:46 +0000 Subject: changes that fix it all: - set proper public key serialisation length in pubLen = 64 - reset all sizes and offsets - rename from DER to S (we are not using DER encoding) - add remoteInitRandomPubKey as return value to respondToHandshake - add ImportPublicKey with error return to read both EC golang.elliptic style 65 byte encoding and 64 byte one - add ExportPublicKey falling back to go-ethereum/crypto.FromECDSAPub() chopping off the first byte - add Import - Export tests - all tests pass --- p2p/crypto.go | 111 +++++++++++++++++++++++++++++------------------------ p2p/crypto_test.go | 92 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 146 insertions(+), 57 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index b6d600826..dbef022cc 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -12,11 +12,12 @@ import ( ) var ( - sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 - sigLen int = 65 // elliptic S256 - keyLen int = 32 // ECDSA - msgLen int = sigLen + 3*keyLen + 1 // 162 - resLen int = 65 // + sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 + sigLen int = 65 // elliptic S256 + pubLen int = 64 // 512 bit pubkey in uncompressed representation without format byte + keyLen int = 32 // ECDSA + msgLen int = 194 // sigLen + keyLen + pubLen + keyLen + 1 = 194 + resLen int = 97 // pubLen + keyLen + 1 ) // aesSecret, macSecret, egressMac, ingress @@ -25,20 +26,21 @@ type secretRW struct { } type cryptoId struct { - prvKey *ecdsa.PrivateKey - pubKey *ecdsa.PublicKey - pubKeyDER []byte + prvKey *ecdsa.PrivateKey + pubKey *ecdsa.PublicKey + pubKeyS []byte } func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // will be at server init - var prvKeyDER []byte = id.PrivKey() - if prvKeyDER == nil { + var prvKeyS []byte = id.PrivKey() + if prvKeyS == nil { err = fmt.Errorf("no private key for client") return } - // initialise ecies private key via importing DER encoded keys (known via our own clientIdentity) - var prvKey = crypto.ToECDSA(prvKeyDER) + // initialise ecies private key via importing keys (known via our own clientIdentity) + // the key format is what elliptic package is using: elliptic.Marshal(Curve, X, Y) + var prvKey = crypto.ToECDSA(prvKeyS) if prvKey == nil { err = fmt.Errorf("invalid private key for client") return @@ -50,16 +52,16 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // to be created at server init shared between peers and sessions // for reuse, call wth ReadAt, no reset seek needed } - self.pubKeyDER = id.Pubkey() + self.pubKeyS = id.Pubkey() return } -func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyDER []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { +func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { var auth, initNonce, recNonce []byte var randomPrivKey *ecdsa.PrivateKey var remoteRandomPubKey *ecdsa.PublicKey if initiator { - if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyDER, sessionToken); err != nil { + if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyS, sessionToken); err != nil { return } conn.Write(auth) @@ -76,10 +78,9 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyDER []byte, sessionTok // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. // so we read auth message first, then respond var response []byte - if response, recNonce, initNonce, randomPrivKey, err = self.respondToHandshake(auth, remotePubKeyDER, sessionToken); err != nil { + if response, recNonce, initNonce, randomPrivKey, remoteRandomPubKey, err = self.respondToHandshake(auth, remotePubKeyS, sessionToken); err != nil { return } - remoteRandomPubKey = &randomPrivKey.PublicKey conn.Write(response) } return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) @@ -100,11 +101,29 @@ The handshake is the process by which the peers establish their connection for a */ -func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { +func ImportPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err error) { + var pubKey65 []byte + switch len(pubKey) { + case 64: + pubKey65 = append([]byte{0x04}, pubKey...) + case 65: + pubKey65 = pubKey + default: + return nil, fmt.Errorf("invalid public key length %v (expect 64/65)", len(pubKey)) + } + return crypto.ToECDSAPub(pubKey65), nil +} + +func ExportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { + if pubKeyEC == nil { + return nil, fmt.Errorf("no ECDSA public key given") + } + return crypto.FromECDSAPub(pubKeyEC)[1:], nil +} + +func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties - remotePubKey = crypto.ToECDSAPub(remotePubKeyDER) - if remotePubKey == nil { - err = fmt.Errorf("invalid remote public key") + if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { return } @@ -116,8 +135,6 @@ func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } - // this will not stay here ;) - fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) // tokenFlag = 0x00 // redundant } else { // for known peers, we use stored token from the previous session @@ -128,9 +145,7 @@ func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) // allocate msgLen long message, var msg []byte = make([]byte, msgLen) - // generate sskLen long nonce initNonce = msg[msgLen-keyLen-1 : msgLen-1] - // nonce = msg[msgLen-sskLen-1 : msgLen-1] if _, err = rand.Read(initNonce); err != nil { return } @@ -150,48 +165,45 @@ func (self *cryptoId) startHandshake(remotePubKeyDER, sessionToken []byte) (auth if signature, err = crypto.Sign(sharedSecret, randomPrvKey); err != nil { return } - fmt.Printf("signature generated: %v %x", len(signature), signature) // message // signed-shared-secret || H(ecdhe-random-pubk) || pubk || nonce || 0x0 copy(msg, signature) // copy signed-shared-secret // H(ecdhe-random-pubk) - copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(crypto.FromECDSAPub(&randomPrvKey.PublicKey))) + var randomPubKey64 []byte + if randomPubKey64, err = ExportPublicKey(&randomPrvKey.PublicKey); err != nil { + return + } + copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(randomPubKey64)) // pubkey copied to the correct segment. - copy(msg[sigLen+keyLen:sigLen+2*keyLen], self.pubKeyDER) + copy(msg[sigLen+keyLen:sigLen+keyLen+pubLen], self.pubKeyS) // nonce is already in the slice // stick tokenFlag byte to the end msg[msgLen-1] = tokenFlag - fmt.Printf("plaintext message generated: %v %x", len(msg), msg) - // encrypt using remote-pubk // auth = eciesEncrypt(remote-pubk, msg) if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { return } - fmt.Printf("encrypted message generated: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(remotePubKey)) return } // verifyAuth is called by peer if it accepted (but not initiated) the connection -func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, err error) { +func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey, err error) { var msg []byte - remotePubKey := crypto.ToECDSAPub(remotePubKeyDER) - if remotePubKey == nil { - err = fmt.Errorf("invalid public key") + var remotePubKey *ecdsa.PublicKey + if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { return } - fmt.Printf("encrypted message received: %v %x\n used pubkey: %x\n", len(auth), auth, crypto.FromECDSAPub(self.pubKey)) // they prove that msg is meant for me, // I prove I possess private key if i can read it if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { return } - fmt.Printf("\nplaintext message retrieved: %v %x\n", len(msg), msg) var tokenFlag byte if sessionToken == nil { @@ -201,7 +213,6 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []b if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } - fmt.Printf("secret generated: %v %x", len(sessionToken), sessionToken) // tokenFlag = 0x00 // redundant } else { // for known peers, we use stored token from the previous session @@ -214,21 +225,19 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []b // they prove they own the private key belonging to ecdhe-random-pubk // we can now reconstruct the signed message and recover the peers pubkey var signedMsg = Xor(sessionToken, initNonce) - var remoteRandomPubKeyDER []byte - if remoteRandomPubKeyDER, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { + var remoteRandomPubKeyS []byte + if remoteRandomPubKeyS, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { return } // convert to ECDSA standard - remoteRandomPubKey := crypto.ToECDSAPub(remoteRandomPubKeyDER) - if remoteRandomPubKey == nil { - err = fmt.Errorf("invalid remote public key") + if remoteRandomPubKey, err = ImportPublicKey(remoteRandomPubKeyS); err != nil { return } // now we find ourselves a long task too, fill it random var resp = make([]byte, resLen) // generate keyLen long nonce - respNonce = msg[resLen-keyLen-1 : msgLen-1] + respNonce = resp[pubLen : pubLen+keyLen] if _, err = rand.Read(respNonce); err != nil { return } @@ -238,7 +247,11 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyDER, sessionToken []b } // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - copy(resp[:keyLen], crypto.FromECDSAPub(&randomPrivKey.PublicKey)) + var randomPubKeyS []byte + if randomPubKeyS, err = ExportPublicKey(&randomPrivKey.PublicKey); err != nil { + return + } + copy(resp[:pubLen], randomPubKeyS) // nonce is already in the slice resp[resLen-1] = tokenFlag @@ -259,11 +272,9 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa return } - respNonce = msg[resLen-keyLen-1 : resLen-1] - var remoteRandomPubKeyDER = msg[:keyLen] - remoteRandomPubKey = crypto.ToECDSAPub(remoteRandomPubKeyDER) - if remoteRandomPubKey == nil { - err = fmt.Errorf("invalid ecdh random remote public key") + respNonce = msg[pubLen : pubLen+keyLen] + var remoteRandomPubKeyS = msg[:pubLen] + if remoteRandomPubKey, err = ImportPublicKey(remoteRandomPubKeyS); err != nil { return } if msg[resLen-1] == 0x01 { diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 89baca084..8000efaf1 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -2,16 +2,76 @@ package p2p import ( "bytes" + // "crypto/ecdsa" + // "crypto/elliptic" + // "crypto/rand" "fmt" "testing" "github.com/ethereum/go-ethereum/crypto" + "github.com/obscuren/ecies" ) +func TestPublicKeyEncoding(t *testing.T) { + prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) + pub0 := &prv0.PublicKey + pub0s := crypto.FromECDSAPub(pub0) + pub1, err := ImportPublicKey(pub0s) + if err != nil { + t.Errorf("%v", err) + } + eciesPub1 := ecies.ImportECDSAPublic(pub1) + if eciesPub1 == nil { + t.Errorf("invalid ecdsa public key") + } + pub1s, err := ExportPublicKey(pub1) + if err != nil { + t.Errorf("%v", err) + } + if len(pub1s) != 64 { + t.Errorf("wrong length expect 64, got", len(pub1s)) + } + pub2, err := ImportPublicKey(pub1s) + if err != nil { + t.Errorf("%v", err) + } + pub2s, err := ExportPublicKey(pub2) + if err != nil { + t.Errorf("%v", err) + } + if !bytes.Equal(pub1s, pub2s) { + t.Errorf("exports dont match") + } + pub2sEC := crypto.FromECDSAPub(pub2) + if !bytes.Equal(pub0s, pub2sEC) { + t.Errorf("exports dont match") + } +} + +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 TestCryptoHandshake(t *testing.T) { var err error var sessionToken []byte - prv0, _ := crypto.GenerateKey() + prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) pub0 := &prv0.PublicKey prv1, _ := crypto.GenerateKey() pub1 := &prv1.PublicKey @@ -26,17 +86,35 @@ func TestCryptoHandshake(t *testing.T) { // simulate handshake by feeding output to input // initiator sends handshake 'auth' - auth, initNonce, randomPrivKey, _, _ := initiator.startHandshake(receiver.pubKeyDER, sessionToken) + auth, initNonce, randomPrivKey, _, err := initiator.startHandshake(receiver.pubKeyS, sessionToken) + if err != nil { + t.Errorf("%v", err) + } + // receiver reads auth and responds with response - response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, _ := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, remoteInitRandomPubKey, err := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + if err != nil { + t.Errorf("%v", err) + } + // initiator reads receiver's response and the key exchange completes - recNonce, remoteRandomPubKey, _, _ := initiator.completeHandshake(response) + recNonce, remoteRandomPubKey, _, err := initiator.completeHandshake(response) + if err != nil { + t.Errorf("%v", err) + } // now both parties should have the same session parameters - initSessionToken, initSecretRW, _ := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) - recSessionToken, recSecretRW, _ := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, &randomPrivKey.PublicKey) + initSessionToken, initSecretRW, err := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + if err != nil { + t.Errorf("%v", err) + } + + recSessionToken, recSecretRW, err := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) + if err != nil { + t.Errorf("%v", err) + } - fmt.Printf("%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n%x\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, &randomPrivKey.PublicKey, initSessionToken, initSecretRW) + fmt.Printf("\nauth %x\ninitNonce %x\nresponse%x\nremoteRecNonce %x\nremoteInitNonce %x\nremoteRandomPubKey %x\nrecNonce %x\nremoteInitRandomPubKey %x\ninitSessionToken %x\n\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, remoteInitRandomPubKey, initSessionToken) if !bytes.Equal(initNonce, remoteInitNonce) { t.Errorf("nonces do not match") -- cgit v1.2.3 From 4afde4e738e3981e086d6e8710c3d711d0e8531d Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 10:22:07 +0000 Subject: add code documentation --- p2p/crypto.go | 62 ++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 15 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index dbef022cc..8551e317c 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -20,17 +20,27 @@ var ( resLen int = 97 // pubLen + keyLen + 1 ) +// secretRW implements a message read writer with encryption and authentication +// it is initialised by cryptoId.Run() after a successful crypto handshake // aesSecret, macSecret, egressMac, ingress type secretRW struct { aesSecret, macSecret, egressMac, ingressMac []byte } +/* +cryptoId implements the crypto layer for the p2p networking +It is initialised on the node's own identity (which has access to the node's private key) and run separately on a peer connection to set up a secure session after a crypto handshake +After it performs a crypto handshake it returns +*/ type cryptoId struct { prvKey *ecdsa.PrivateKey pubKey *ecdsa.PublicKey pubKeyS []byte } +/* +newCryptoId(id ClientIdentity) initialises a crypto layer manager. This object has a short lifecycle when the peer connection starts. It is survived by a secretRW (an message read writer with encryption and authentication) if the crypto handshake is successful. +*/ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // will be at server init var prvKeyS []byte = id.PrivKey() @@ -56,6 +66,20 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { return } +/* +Run(connection, remotePublicKey, sessionToken) is called when the peer connection starts to set up a secure session by performing a crypto handshake. + + connection is (a buffered) network connection. + + remotePublicKey is the remote peer's node Id. + + sessionToken is the token from the previous session with this same peer. Nil if no token is found. + + initiator is a boolean flag. True if the node represented by cryptoId is the initiator of the connection (ie., remote is an outbound peer reached by dialing out). False if the connection was established by accepting a call from the remote peer via a listener. + + It returns a secretRW which implements the MsgReadWriter interface. +*/ + func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { var auth, initNonce, recNonce []byte var randomPrivKey *ecdsa.PrivateKey @@ -86,21 +110,9 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } -/* startHandshake is called by peer if it initiated the connection. - By protocol spec, the party who initiates the connection (initiator) will send an 'auth' packet -New: authInitiator -> E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) - authRecipient -> E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - -Known: authInitiator = E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) - authRecipient = E(remote-pubk, ecdhe-random-pubk || nonce || 0x1) // token found - authRecipient = E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) // token not found - -The caller provides the public key of the peer as conjuctured from lookup based on IP:port, given as user input or proven by signatures. The caller must have access to persistant information about the peers, and pass the previous session token as an argument to cryptoId. - -The handshake is the process by which the peers establish their connection for a session. - +/* +ImportPublicKey creates a 512 bit *ecsda.PublicKey from a byte slice. It accepts the simple 64 byte uncompressed format or the 65 byte format given by calling elliptic.Marshal on the EC point represented by the key. Any other length will result in an invalid public key error. */ - func ImportPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err error) { var pubKey65 []byte switch len(pubKey) { @@ -114,6 +126,9 @@ func ImportPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err error) { return crypto.ToECDSAPub(pubKey65), nil } +/* +ExportPublicKey exports a *ecdsa.PublicKey into a byte slice using a simple 64-byte format. and is used for simple serialisation in network communication +*/ func ExportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { if pubKeyEC == nil { return nil, fmt.Errorf("no ECDSA public key given") @@ -121,6 +136,12 @@ func ExportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { return crypto.FromECDSAPub(pubKeyEC)[1:], nil } +/* startHandshake is called by if the node is the initiator of the connection. + +The caller provides the public key of the peer as conjuctured from lookup based on IP:port, given as user input or proven by signatures. The caller must have access to persistant information about the peers, and pass the previous session token as an argument to cryptoId. + +The first return value is the auth message that is to be sent out to the remote receiver. +*/ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { @@ -191,7 +212,11 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ return } -// verifyAuth is called by peer if it accepted (but not initiated) the connection +/* +respondToHandshake is called by peer if it accepted (but not initiated) the connection from the remote. It is passed the initiator handshake received, the public key and session token belonging to the remote initiator. + +The first return value is the authentication response (aka receiver handshake) that is to be sent to the remote initiator. +*/ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey, err error) { var msg []byte var remotePubKey *ecdsa.PublicKey @@ -264,6 +289,9 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt return } +/* +completeHandshake is called when the initiator receives an authentication response (aka receiver handshake). It completes the handshake by reading off parameters the remote peer provides needed to set up the secure session +*/ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { var msg []byte // they prove that msg is meant for me, @@ -283,6 +311,9 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa return } +/* +newSession is called after the handshake is completed. The arguments are values negotiated in the handshake and the return value is a new session : a new session Token to be remembered for the next time we connect with this peer. And a MsgReadWriter that implements an encrypted and authenticated connection with key material obtained from the crypto handshake key exchange +*/ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) @@ -316,6 +347,7 @@ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecd return } +// TODO: optimisation // should use cipher.xorBytes from crypto/cipher/xor.go for fast xor func Xor(one, other []byte) (xor []byte) { xor = make([]byte, len(one)) -- cgit v1.2.3 From 1f2adb05b57b0b657bfa62b47d46e3a9bf1d8496 Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 14:42:12 +0000 Subject: add initial peer level test (failing) --- p2p/crypto_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 8000efaf1..5fbdc61e3 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -6,6 +6,7 @@ import ( // "crypto/elliptic" // "crypto/rand" "fmt" + "net" "testing" "github.com/ethereum/go-ethereum/crypto" @@ -114,7 +115,9 @@ func TestCryptoHandshake(t *testing.T) { t.Errorf("%v", err) } - fmt.Printf("\nauth %x\ninitNonce %x\nresponse%x\nremoteRecNonce %x\nremoteInitNonce %x\nremoteRandomPubKey %x\nrecNonce %x\nremoteInitRandomPubKey %x\ninitSessionToken %x\n\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, remoteInitRandomPubKey, initSessionToken) + fmt.Printf("\nauth (%v) %x\n\nresp (%v) %x\n\n", len(auth), auth, len(response), response) + + // fmt.Printf("\nauth %x\ninitNonce %x\nresponse%x\nremoteRecNonce %x\nremoteInitNonce %x\nremoteRandomPubKey %x\nrecNonce %x\nremoteInitRandomPubKey %x\ninitSessionToken %x\n\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, remoteInitRandomPubKey, initSessionToken) if !bytes.Equal(initNonce, remoteInitNonce) { t.Errorf("nonces do not match") @@ -140,3 +143,51 @@ func TestCryptoHandshake(t *testing.T) { } } + +func TestPeersHandshake(t *testing.T) { + defer testlog(t).detach() + var err error + // var sessionToken []byte + prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) + pub0 := &prv0.PublicKey + prv1, _ := crypto.GenerateKey() + pub1 := &prv1.PublicKey + + prv0s := crypto.FromECDSA(prv0) + pub0s := crypto.FromECDSAPub(pub0) + prv1s := crypto.FromECDSA(prv1) + pub1s := crypto.FromECDSAPub(pub1) + + conn1, conn2 := net.Pipe() + initiator := newPeer(conn1, []Protocol{}, nil) + receiver := newPeer(conn2, []Protocol{}, nil) + initiator.dialAddr = &peerAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222, Pubkey: pub1s[1:]} + initiator.ourID = &peerId{prv0s, pub0s} + + // this is cheating. identity of initiator/dialler not available to listener/receiver + // its public key should be looked up based on IP address + receiver.identity = initiator.ourID + receiver.ourID = &peerId{prv1s, pub1s} + + initiator.pubkeyHook = func(*peerAddr) error { return nil } + receiver.pubkeyHook = func(*peerAddr) error { return nil } + + initiator.cryptoHandshake = true + receiver.cryptoHandshake = true + errc0 := make(chan error, 1) + errc1 := make(chan error, 1) + go func() { + _, err := initiator.loop() + errc0 <- err + }() + go func() { + _, err := receiver.loop() + errc1 <- err + }() + select { + case err = <-errc0: + t.Errorf("peer 0 quit with error: %v", err) + case err = <-errc1: + t.Errorf("peer 1 quit with error: %v", err) + } +} -- cgit v1.2.3 From 20aade56c3057a221d7fa7152a4969d5f8f980d5 Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 14:45:53 +0000 Subject: chop first byte when cryptoid.PubKeyS is set from identity.Pubkey() since this is directly copied in the auth message --- p2p/crypto.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 8551e317c..91d60aa7e 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -7,10 +7,13 @@ import ( "io" "github.com/ethereum/go-ethereum/crypto" + ethlogger "github.com/ethereum/go-ethereum/logger" "github.com/obscuren/ecies" "github.com/obscuren/secp256k1-go" ) +var clogger = ethlogger.NewLogger("CRYPTOID") + var ( sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 sigLen int = 65 // elliptic S256 @@ -62,10 +65,17 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // to be created at server init shared between peers and sessions // for reuse, call wth ReadAt, no reset seek needed } - self.pubKeyS = id.Pubkey() + self.pubKeyS = id.Pubkey()[1:] + clogger.Debugf("crytoid starting for %v", hexkey(self.pubKeyS)) return } +type hexkey []byte + +func (self hexkey) String() string { + return fmt.Sprintf("(%d) %x", len(self), []byte(self)) +} + /* Run(connection, remotePublicKey, sessionToken) is called when the peer connection starts to set up a secure session by performing a crypto handshake. -- cgit v1.2.3 From faa069a126da29a246193713568634e5be6edd2d Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 16:22:49 +0000 Subject: peer-level integration test for crypto handshake - add const length params for handshake messages - add length check to fail early - add debug logs to help interop testing (!ABSOLUTELY SHOULD BE DELETED LATER) - wrap connection read/writes in error check - add cryptoReady channel in peer to signal when secure session setup is finished - wait for cryptoReady or timeout in TestPeersHandshake --- p2p/crypto.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++------ p2p/crypto_test.go | 11 +++++++++++ p2p/peer.go | 22 +++++++++++++--------- 3 files changed, 71 insertions(+), 15 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 91d60aa7e..e8f4d551b 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -21,6 +21,8 @@ var ( keyLen int = 32 // ECDSA msgLen int = 194 // sigLen + keyLen + pubLen + keyLen + 1 = 194 resLen int = 97 // pubLen + keyLen + 1 + iHSLen int = 307 // size of the final ECIES payload sent as initiator's handshake + rHSLen int = 210 // size of the final ECIES payload sent as receiver's handshake ) // secretRW implements a message read writer with encryption and authentication @@ -66,7 +68,8 @@ func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { // for reuse, call wth ReadAt, no reset seek needed } self.pubKeyS = id.Pubkey()[1:] - clogger.Debugf("crytoid starting for %v", hexkey(self.pubKeyS)) + clogger.Debugf("initialise crypto for NodeId %v", hexkey(self.pubKeyS)) + clogger.Debugf("private-key %v\npublic key %v", hexkey(prvKeyS), hexkey(self.pubKeyS)) return } @@ -92,22 +95,51 @@ Run(connection, remotePublicKey, sessionToken) is called when the peer connectio func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { var auth, initNonce, recNonce []byte + var read int var randomPrivKey *ecdsa.PrivateKey var remoteRandomPubKey *ecdsa.PublicKey + clogger.Debugf("attempting session with %v", hexkey(remotePubKeyS)) if initiator { if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyS, sessionToken); err != nil { return } - conn.Write(auth) - var response []byte - conn.Read(response) + clogger.Debugf("initiator-nonce: %v", hexkey(initNonce)) + clogger.Debugf("initiator-random-private-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) + randomPublicKeyS, _ := ExportPublicKey(&randomPrivKey.PublicKey) + clogger.Debugf("initiator-random-public-key: %v", hexkey(randomPublicKeyS)) + + if _, err = conn.Write(auth); err != nil { + return + } + clogger.Debugf("initiator handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(auth)) + var response []byte = make([]byte, rHSLen) + if read, err = conn.Read(response); err != nil || read == 0 { + return + } + if read != rHSLen { + err = fmt.Errorf("remote receiver's handshake has invalid length. expect %v, got %v", rHSLen, read) + return + } // write out auth message // wait for response, then call complete if recNonce, remoteRandomPubKey, _, err = self.completeHandshake(response); err != nil { return } + clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) + remoteRandomPubKeyS, _ := ExportPublicKey(remoteRandomPubKey) + clogger.Debugf("receiver-random-public-key: %v", hexkey(remoteRandomPubKeyS)) + } else { - conn.Read(auth) + auth = make([]byte, iHSLen) + clogger.Debugf("waiting for initiator handshake (from %v)", hexkey(remotePubKeyS)) + if read, err = conn.Read(auth); err != nil { + return + } + if read != iHSLen { + err = fmt.Errorf("remote initiator's handshake has invalid length. expect %v, got %v", iHSLen, read) + return + } + clogger.Debugf("received initiator handshake (from %v):\n%v", hexkey(remotePubKeyS), hexkey(auth)) // we are listening connection. we are responders in the handshake. // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. // so we read auth message first, then respond @@ -115,7 +147,12 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken if response, recNonce, initNonce, randomPrivKey, remoteRandomPubKey, err = self.respondToHandshake(auth, remotePubKeyS, sessionToken); err != nil { return } - conn.Write(response) + clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) + clogger.Debugf("receiver-random-priv-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) + if _, err = conn.Write(response); err != nil { + return + } + clogger.Debugf("receiver handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(response)) } return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } @@ -354,6 +391,10 @@ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecd egressMac: egressMac, ingressMac: ingressMac, } + clogger.Debugf("aes-secret: %v", hexkey(aesSecret)) + clogger.Debugf("mac-secret: %v", hexkey(macSecret)) + clogger.Debugf("egress-mac: %v", hexkey(egressMac)) + clogger.Debugf("ingress-mac: %v", hexkey(ingressMac)) return } diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 5fbdc61e3..47b16040a 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -8,6 +8,7 @@ import ( "fmt" "net" "testing" + "time" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" @@ -184,7 +185,17 @@ func TestPeersHandshake(t *testing.T) { _, err := receiver.loop() errc1 <- err }() + ready := make(chan bool) + go func() { + <-initiator.cryptoReady + <-receiver.cryptoReady + close(ready) + }() + timeout := time.After(1 * time.Second) select { + case <-ready: + case <-timeout: + t.Errorf("crypto handshake hanging for too long") case err = <-errc0: t.Errorf("peer 0 quit with error: %v", err) case err = <-errc1: diff --git a/p2p/peer.go b/p2p/peer.go index e44eaab34..818f80580 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -71,6 +71,7 @@ type Peer struct { protocols []Protocol runBaseProtocol bool // for testing cryptoHandshake bool // for testing + cryptoReady chan struct{} runlock sync.RWMutex // protects running running map[string]*proto @@ -120,15 +121,16 @@ func newServerPeer(server *Server, conn net.Conn, dialAddr *peerAddr) *Peer { func newPeer(conn net.Conn, protocols []Protocol, dialAddr *peerAddr) *Peer { p := &Peer{ - Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), - conn: conn, - dialAddr: dialAddr, - bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), - protocols: protocols, - running: make(map[string]*proto), - disc: make(chan DiscReason), - protoErr: make(chan error), - closed: make(chan struct{}), + Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), + conn: conn, + dialAddr: dialAddr, + bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + protocols: protocols, + running: make(map[string]*proto), + disc: make(chan DiscReason), + protoErr: make(chan error), + closed: make(chan struct{}), + cryptoReady: make(chan struct{}), } return p } @@ -240,6 +242,7 @@ func (p *Peer) loop() (reason DiscReason, err error) { go readLoop(readMsg, readErr, readNext) readNext <- true + close(p.cryptoReady) if p.runBaseProtocol { p.startBaseProtocol() } @@ -353,6 +356,7 @@ func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // this bit handles the handshake and creates a secure communications channel with // var rw *secretRW if sessionToken, _, err = crypto.Run(p.conn, p.Pubkey(), sessionToken, initiator); err != nil { + p.Debugf("unable to setup secure session: %v", err) return } loop = func(msg chan<- Msg, err chan<- error, next <-chan bool) { -- cgit v1.2.3 From 54252ede3177cb169fbb9e4824a31ce58cb0316c Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 21 Jan 2015 16:53:13 +0000 Subject: add temporary forced session token generation --- p2p/crypto.go | 3 +++ p2p/peer.go | 5 +++++ 2 files changed, 8 insertions(+) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index e8f4d551b..f5307cd5a 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -103,6 +103,9 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyS, sessionToken); err != nil { return } + if sessionToken != nil { + clogger.Debugf("session-token: %v", hexkey(sessionToken)) + } clogger.Debugf("initiator-nonce: %v", hexkey(initNonce)) clogger.Debugf("initiator-random-private-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) randomPublicKeyS, _ := ExportPublicKey(&randomPrivKey.PublicKey) diff --git a/p2p/peer.go b/p2p/peer.go index 818f80580..99f1a61d3 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -3,6 +3,7 @@ package p2p import ( "bufio" "bytes" + "crypto/rand" "fmt" "io" "io/ioutil" @@ -342,6 +343,10 @@ func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // it is survived by an encrypted readwriter var initiator bool var sessionToken []byte + sessionToken = make([]byte, keyLen) + if _, err = rand.Read(sessionToken); err != nil { + return + } if p.dialAddr != nil { // this should have its own method Outgoing() bool initiator = true } -- cgit v1.2.3 From 4499743522d32990614c7d900d746e998a1b81ed Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 26 Jan 2015 14:50:12 +0000 Subject: apply handshake related improvements from p2p.crypto branch --- p2p/crypto.go | 44 +++++++++++++++++++++++--------------------- p2p/crypto_test.go | 14 +++++++------- p2p/peer.go | 2 +- 3 files changed, 31 insertions(+), 29 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index f5307cd5a..361012743 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -7,20 +7,20 @@ import ( "io" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" ethlogger "github.com/ethereum/go-ethereum/logger" "github.com/obscuren/ecies" - "github.com/obscuren/secp256k1-go" ) var clogger = ethlogger.NewLogger("CRYPTOID") -var ( +const ( sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 sigLen int = 65 // elliptic S256 pubLen int = 64 // 512 bit pubkey in uncompressed representation without format byte - keyLen int = 32 // ECDSA - msgLen int = 194 // sigLen + keyLen + pubLen + keyLen + 1 = 194 - resLen int = 97 // pubLen + keyLen + 1 + shaLen int = 32 // hash length (for nonce etc) + msgLen int = 194 // sigLen + shaLen + pubLen + shaLen + 1 = 194 + resLen int = 97 // pubLen + shaLen + 1 iHSLen int = 307 // size of the final ECIES payload sent as initiator's handshake rHSLen int = 210 // size of the final ECIES payload sent as receiver's handshake ) @@ -157,7 +157,7 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken } clogger.Debugf("receiver handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(response)) } - return self.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + return self.newSession(initiator, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } /* @@ -198,7 +198,7 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ return } - var tokenFlag byte + var tokenFlag byte // = 0x00 if sessionToken == nil { // no session token found means we need to generate shared secret. // ecies shared secret is used as initial session token for new peers @@ -216,7 +216,7 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) // allocate msgLen long message, var msg []byte = make([]byte, msgLen) - initNonce = msg[msgLen-keyLen-1 : msgLen-1] + initNonce = msg[msgLen-shaLen-1 : msgLen-1] if _, err = rand.Read(initNonce); err != nil { return } @@ -245,9 +245,9 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ if randomPubKey64, err = ExportPublicKey(&randomPrvKey.PublicKey); err != nil { return } - copy(msg[sigLen:sigLen+keyLen], crypto.Sha3(randomPubKey64)) + copy(msg[sigLen:sigLen+shaLen], crypto.Sha3(randomPubKey64)) // pubkey copied to the correct segment. - copy(msg[sigLen+keyLen:sigLen+keyLen+pubLen], self.pubKeyS) + copy(msg[sigLen+shaLen:sigLen+shaLen+pubLen], self.pubKeyS) // nonce is already in the slice // stick tokenFlag byte to the end msg[msgLen-1] = tokenFlag @@ -295,7 +295,7 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt } // the initiator nonce is read off the end of the message - initNonce = msg[msgLen-keyLen-1 : msgLen-1] + initNonce = msg[msgLen-shaLen-1 : msgLen-1] // I prove that i own prv key (to derive shared secret, and read nonce off encrypted msg) and that I own shared secret // they prove they own the private key belonging to ecdhe-random-pubk // we can now reconstruct the signed message and recover the peers pubkey @@ -311,8 +311,8 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt // now we find ourselves a long task too, fill it random var resp = make([]byte, resLen) - // generate keyLen long nonce - respNonce = resp[pubLen : pubLen+keyLen] + // generate shaLen long nonce + respNonce = resp[pubLen : pubLen+shaLen] if _, err = rand.Read(respNonce); err != nil { return } @@ -350,7 +350,7 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa return } - respNonce = msg[pubLen : pubLen+keyLen] + respNonce = msg[pubLen : pubLen+shaLen] var remoteRandomPubKeyS = msg[:pubLen] if remoteRandomPubKey, err = ImportPublicKey(remoteRandomPubKeyS); err != nil { return @@ -364,7 +364,7 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa /* newSession is called after the handshake is completed. The arguments are values negotiated in the handshake and the return value is a new session : a new session Token to be remembered for the next time we connect with this peer. And a MsgReadWriter that implements an encrypted and authenticated connection with key material obtained from the crypto handshake key exchange */ -func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { +func (self *cryptoId) newSession(initiator bool, initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte @@ -382,12 +382,14 @@ func (self *cryptoId) newSession(initNonce, respNonce, auth []byte, privKey *ecd // mac-secret = crypto.Sha3(ecdhe-shared-secret || aes-secret) var macSecret = crypto.Sha3(append(dhSharedSecret, aesSecret...)) // # destroy ecdhe-shared-secret - // egress-mac = crypto.Sha3(mac-secret^nonce || auth) - var egressMac = crypto.Sha3(append(Xor(macSecret, respNonce), auth...)) - // # destroy nonce - // ingress-mac = crypto.Sha3(mac-secret^initiator-nonce || auth), - var ingressMac = crypto.Sha3(append(Xor(macSecret, initNonce), auth...)) - // # destroy remote-nonce + var egressMac, ingressMac []byte + if initiator { + egressMac = Xor(macSecret, respNonce) + ingressMac = Xor(macSecret, initNonce) + } else { + egressMac = Xor(macSecret, initNonce) + ingressMac = Xor(macSecret, respNonce) + } rw = &secretRW{ aesSecret: aesSecret, macSecret: macSecret, diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 47b16040a..919d38df6 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -106,12 +106,12 @@ func TestCryptoHandshake(t *testing.T) { } // now both parties should have the same session parameters - initSessionToken, initSecretRW, err := initiator.newSession(initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + initSessionToken, initSecretRW, err := initiator.newSession(true, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) if err != nil { t.Errorf("%v", err) } - recSessionToken, recSecretRW, err := receiver.newSession(remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) + recSessionToken, recSecretRW, err := receiver.newSession(false, remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) if err != nil { t.Errorf("%v", err) } @@ -136,11 +136,11 @@ func TestCryptoHandshake(t *testing.T) { if !bytes.Equal(initSecretRW.macSecret, recSecretRW.macSecret) { t.Errorf("macSecrets do not match") } - if !bytes.Equal(initSecretRW.egressMac, recSecretRW.egressMac) { - t.Errorf("egressMacs do not match") + if !bytes.Equal(initSecretRW.egressMac, recSecretRW.ingressMac) { + t.Errorf("initiator's egressMac do not match receiver's ingressMac") } - if !bytes.Equal(initSecretRW.ingressMac, recSecretRW.ingressMac) { - t.Errorf("ingressMacs do not match") + if !bytes.Equal(initSecretRW.ingressMac, recSecretRW.egressMac) { + t.Errorf("initiator's inressMac do not match receiver's egressMac") } } @@ -191,7 +191,7 @@ func TestPeersHandshake(t *testing.T) { <-receiver.cryptoReady close(ready) }() - timeout := time.After(1 * time.Second) + timeout := time.After(10 * time.Second) select { case <-ready: case <-timeout: diff --git a/p2p/peer.go b/p2p/peer.go index 99f1a61d3..62df58f8d 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -343,7 +343,7 @@ func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // it is survived by an encrypted readwriter var initiator bool var sessionToken []byte - sessionToken = make([]byte, keyLen) + sessionToken = make([]byte, shaLen) if _, err = rand.Read(sessionToken); err != nil { return } -- cgit v1.2.3 From 68205dec9ff8ab7d16c61f5e32b104d7aa20b352 Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 26 Jan 2015 16:16:23 +0000 Subject: make crypto handshake calls package level, store privateKey on peer + tests ok --- p2p/crypto.go | 87 +++++++++++++++--------------------------------------- p2p/crypto_test.go | 25 +++++++--------- p2p/peer.go | 27 ++++++++++++----- 3 files changed, 52 insertions(+), 87 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 361012743..6a2b99e93 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -32,47 +32,6 @@ type secretRW struct { aesSecret, macSecret, egressMac, ingressMac []byte } -/* -cryptoId implements the crypto layer for the p2p networking -It is initialised on the node's own identity (which has access to the node's private key) and run separately on a peer connection to set up a secure session after a crypto handshake -After it performs a crypto handshake it returns -*/ -type cryptoId struct { - prvKey *ecdsa.PrivateKey - pubKey *ecdsa.PublicKey - pubKeyS []byte -} - -/* -newCryptoId(id ClientIdentity) initialises a crypto layer manager. This object has a short lifecycle when the peer connection starts. It is survived by a secretRW (an message read writer with encryption and authentication) if the crypto handshake is successful. -*/ -func newCryptoId(id ClientIdentity) (self *cryptoId, err error) { - // will be at server init - var prvKeyS []byte = id.PrivKey() - if prvKeyS == nil { - err = fmt.Errorf("no private key for client") - return - } - // initialise ecies private key via importing keys (known via our own clientIdentity) - // the key format is what elliptic package is using: elliptic.Marshal(Curve, X, Y) - var prvKey = crypto.ToECDSA(prvKeyS) - if prvKey == nil { - err = fmt.Errorf("invalid private key for client") - return - } - self = &cryptoId{ - prvKey: prvKey, - // initialise public key from the imported private key - pubKey: &prvKey.PublicKey, - // to be created at server init shared between peers and sessions - // for reuse, call wth ReadAt, no reset seek needed - } - self.pubKeyS = id.Pubkey()[1:] - clogger.Debugf("initialise crypto for NodeId %v", hexkey(self.pubKeyS)) - clogger.Debugf("private-key %v\npublic key %v", hexkey(prvKeyS), hexkey(self.pubKeyS)) - return -} - type hexkey []byte func (self hexkey) String() string { @@ -80,27 +39,29 @@ func (self hexkey) String() string { } /* -Run(connection, remotePublicKey, sessionToken) is called when the peer connection starts to set up a secure session by performing a crypto handshake. +NewSecureSession(connection, privateKey, remotePublicKey, sessionToken, initiator) is called when the peer connection starts to set up a secure session by performing a crypto handshake. connection is (a buffered) network connection. - remotePublicKey is the remote peer's node Id. + privateKey is the local client's private key (*ecdsa.PrivateKey) + + remotePublicKey is the remote peer's node Id ([]byte) sessionToken is the token from the previous session with this same peer. Nil if no token is found. - initiator is a boolean flag. True if the node represented by cryptoId is the initiator of the connection (ie., remote is an outbound peer reached by dialing out). False if the connection was established by accepting a call from the remote peer via a listener. + initiator is a boolean flag. True if the node is the initiator of the connection (ie., remote is an outbound peer reached by dialing out). False if the connection was established by accepting a call from the remote peer via a listener. It returns a secretRW which implements the MsgReadWriter interface. */ -func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { +func NewSecureSession(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { var auth, initNonce, recNonce []byte var read int var randomPrivKey *ecdsa.PrivateKey var remoteRandomPubKey *ecdsa.PublicKey clogger.Debugf("attempting session with %v", hexkey(remotePubKeyS)) if initiator { - if auth, initNonce, randomPrivKey, _, err = self.startHandshake(remotePubKeyS, sessionToken); err != nil { + if auth, initNonce, randomPrivKey, _, err = startHandshake(prvKey, remotePubKeyS, sessionToken); err != nil { return } if sessionToken != nil { @@ -125,7 +86,7 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken } // write out auth message // wait for response, then call complete - if recNonce, remoteRandomPubKey, _, err = self.completeHandshake(response); err != nil { + if recNonce, remoteRandomPubKey, _, err = completeHandshake(response, prvKey); err != nil { return } clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) @@ -147,7 +108,7 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. // so we read auth message first, then respond var response []byte - if response, recNonce, initNonce, randomPrivKey, remoteRandomPubKey, err = self.respondToHandshake(auth, remotePubKeyS, sessionToken); err != nil { + if response, recNonce, initNonce, randomPrivKey, remoteRandomPubKey, err = respondToHandshake(auth, prvKey, remotePubKeyS, sessionToken); err != nil { return } clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) @@ -157,7 +118,7 @@ func (self *cryptoId) Run(conn io.ReadWriter, remotePubKeyS []byte, sessionToken } clogger.Debugf("receiver handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(response)) } - return self.newSession(initiator, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + return newSession(initiator, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) } /* @@ -192,7 +153,7 @@ The caller provides the public key of the peer as conjuctured from lookup based The first return value is the auth message that is to be sent out to the remote receiver. */ -func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { +func startHandshake(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { // session init, common to both parties if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { return @@ -203,7 +164,7 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ // 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 - if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { + if sessionToken, err = ecies.ImportECDSA(prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } // tokenFlag = 0x00 // redundant @@ -245,9 +206,13 @@ func (self *cryptoId) startHandshake(remotePubKeyS, sessionToken []byte) (auth [ if randomPubKey64, err = ExportPublicKey(&randomPrvKey.PublicKey); err != nil { return } + var pubKey64 []byte + if pubKey64, err = ExportPublicKey(&prvKey.PublicKey); err != nil { + return + } copy(msg[sigLen:sigLen+shaLen], crypto.Sha3(randomPubKey64)) // pubkey copied to the correct segment. - copy(msg[sigLen+shaLen:sigLen+shaLen+pubLen], self.pubKeyS) + copy(msg[sigLen+shaLen:sigLen+shaLen+pubLen], pubKey64) // nonce is already in the slice // stick tokenFlag byte to the end msg[msgLen-1] = tokenFlag @@ -267,7 +232,7 @@ respondToHandshake is called by peer if it accepted (but not initiated) the conn The first return value is the authentication response (aka receiver handshake) that is to be sent to the remote initiator. */ -func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey, err error) { +func respondToHandshake(auth []byte, prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey, err error) { var msg []byte var remotePubKey *ecdsa.PublicKey if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { @@ -276,7 +241,7 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt // they prove that msg is meant for me, // I prove I possess private key if i can read it - if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { + if msg, err = crypto.Decrypt(prvKey, auth); err != nil { return } @@ -285,7 +250,7 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt // 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 - if sessionToken, err = ecies.ImportECDSA(self.prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { + if sessionToken, err = ecies.ImportECDSA(prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { return } // tokenFlag = 0x00 // redundant @@ -342,11 +307,11 @@ func (self *cryptoId) respondToHandshake(auth, remotePubKeyS, sessionToken []byt /* completeHandshake is called when the initiator receives an authentication response (aka receiver handshake). It completes the handshake by reading off parameters the remote peer provides needed to set up the secure session */ -func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { +func completeHandshake(auth []byte, prvKey *ecdsa.PrivateKey) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { var msg []byte // they prove that msg is meant for me, // I prove I possess private key if i can read it - if msg, err = crypto.Decrypt(self.prvKey, auth); err != nil { + if msg, err = crypto.Decrypt(prvKey, auth); err != nil { return } @@ -364,7 +329,7 @@ func (self *cryptoId) completeHandshake(auth []byte) (respNonce []byte, remoteRa /* newSession is called after the handshake is completed. The arguments are values negotiated in the handshake and the return value is a new session : a new session Token to be remembered for the next time we connect with this peer. And a MsgReadWriter that implements an encrypted and authenticated connection with key material obtained from the crypto handshake key exchange */ -func (self *cryptoId) newSession(initiator bool, initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { +func newSession(initiator bool, initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { // 3) Now we can trust ecdhe-random-pubk to derive new keys //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) var dhSharedSecret []byte @@ -372,16 +337,10 @@ func (self *cryptoId) newSession(initiator bool, initNonce, respNonce, auth []by if dhSharedSecret, err = ecies.ImportECDSA(privKey).GenerateShared(pubKey, sskLen, sskLen); err != nil { return } - // shared-secret = crypto.Sha3(ecdhe-shared-secret || crypto.Sha3(nonce || initiator-nonce)) var sharedSecret = crypto.Sha3(append(dhSharedSecret, crypto.Sha3(append(respNonce, initNonce...))...)) - // token = crypto.Sha3(shared-secret) sessionToken = crypto.Sha3(sharedSecret) - // aes-secret = crypto.Sha3(ecdhe-shared-secret || shared-secret) var aesSecret = crypto.Sha3(append(dhSharedSecret, sharedSecret...)) - // # destroy shared-secret - // mac-secret = crypto.Sha3(ecdhe-shared-secret || aes-secret) var macSecret = crypto.Sha3(append(dhSharedSecret, aesSecret...)) - // # destroy ecdhe-shared-secret var egressMac, ingressMac []byte if initiator { egressMac = Xor(macSecret, respNonce) diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 919d38df6..f55588ce2 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -78,40 +78,35 @@ func TestCryptoHandshake(t *testing.T) { prv1, _ := crypto.GenerateKey() pub1 := &prv1.PublicKey - var initiator, receiver *cryptoId - if initiator, err = newCryptoId(&peerId{crypto.FromECDSA(prv0), crypto.FromECDSAPub(pub0)}); err != nil { - return - } - if receiver, err = newCryptoId(&peerId{crypto.FromECDSA(prv1), crypto.FromECDSAPub(pub1)}); err != nil { - return - } + pub0s := crypto.FromECDSAPub(pub0) + pub1s := crypto.FromECDSAPub(pub1) // simulate handshake by feeding output to input // initiator sends handshake 'auth' - auth, initNonce, randomPrivKey, _, err := initiator.startHandshake(receiver.pubKeyS, sessionToken) + auth, initNonce, randomPrivKey, _, err := startHandshake(prv0, pub1s, sessionToken) if err != nil { t.Errorf("%v", err) } // receiver reads auth and responds with response - response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, remoteInitRandomPubKey, err := receiver.respondToHandshake(auth, crypto.FromECDSAPub(pub0), sessionToken) + response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, remoteInitRandomPubKey, err := respondToHandshake(auth, prv1, pub0s, sessionToken) if err != nil { t.Errorf("%v", err) } // initiator reads receiver's response and the key exchange completes - recNonce, remoteRandomPubKey, _, err := initiator.completeHandshake(response) + recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prv0) if err != nil { t.Errorf("%v", err) } // now both parties should have the same session parameters - initSessionToken, initSecretRW, err := initiator.newSession(true, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + initSessionToken, initSecretRW, err := newSession(true, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) if err != nil { t.Errorf("%v", err) } - recSessionToken, recSecretRW, err := receiver.newSession(false, remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) + recSessionToken, recSecretRW, err := newSession(false, remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) if err != nil { t.Errorf("%v", err) } @@ -163,12 +158,12 @@ func TestPeersHandshake(t *testing.T) { initiator := newPeer(conn1, []Protocol{}, nil) receiver := newPeer(conn2, []Protocol{}, nil) initiator.dialAddr = &peerAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222, Pubkey: pub1s[1:]} - initiator.ourID = &peerId{prv0s, pub0s} + initiator.privateKey = prv0s // this is cheating. identity of initiator/dialler not available to listener/receiver // its public key should be looked up based on IP address - receiver.identity = initiator.ourID - receiver.ourID = &peerId{prv1s, pub1s} + receiver.identity = &peerId{nil, pub0s} + receiver.privateKey = prv1s initiator.pubkeyHook = func(*peerAddr) error { return nil } receiver.pubkeyHook = func(*peerAddr) error { return nil } diff --git a/p2p/peer.go b/p2p/peer.go index 62df58f8d..e82bca222 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -3,6 +3,7 @@ package p2p import ( "bufio" "bytes" + "crypto/ecdsa" "crypto/rand" "fmt" "io" @@ -12,6 +13,8 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/logger" ) @@ -73,6 +76,7 @@ type Peer struct { runBaseProtocol bool // for testing cryptoHandshake bool // for testing cryptoReady chan struct{} + privateKey []byte runlock sync.RWMutex // protects running running map[string]*proto @@ -338,6 +342,13 @@ func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) type readLoop func(chan<- Msg, chan<- error, <-chan bool) +func (p *Peer) PrivateKey() (prv *ecdsa.PrivateKey, err error) { + if prv = crypto.ToECDSA(p.privateKey); prv == nil { + err = fmt.Errorf("invalid private key") + } + return +} + func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { // cryptoId is just created for the lifecycle of the handshake // it is survived by an encrypted readwriter @@ -350,17 +361,17 @@ func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { if p.dialAddr != nil { // this should have its own method Outgoing() bool initiator = true } - // create crypto layer - // this could in principle run only once but maybe we want to allow - // identity switching - var crypto *cryptoId - if crypto, err = newCryptoId(p.ourID); err != nil { - return - } + // run on peer // this bit handles the handshake and creates a secure communications channel with // var rw *secretRW - if sessionToken, _, err = crypto.Run(p.conn, p.Pubkey(), sessionToken, initiator); err != nil { + var prvKey *ecdsa.PrivateKey + if prvKey, err = p.PrivateKey(); err != nil { + err = fmt.Errorf("unable to access private key for client: %v", err) + return + } + // initialise a new secure session + if sessionToken, _, err = NewSecureSession(p.conn, prvKey, p.Pubkey(), sessionToken, initiator); err != nil { p.Debugf("unable to setup secure session: %v", err) return } -- cgit v1.2.3 From 71765957e4442105647045a14a0a551459daddfd Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 26 Jan 2015 16:58:58 +0000 Subject: get rid of Private Key in ClientIdentity --- p2p/client_identity.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'p2p') diff --git a/p2p/client_identity.go b/p2p/client_identity.go index fca2756bd..ef01f1ad7 100644 --- a/p2p/client_identity.go +++ b/p2p/client_identity.go @@ -7,9 +7,8 @@ import ( // ClientIdentity represents the identity of a peer. type ClientIdentity interface { - String() string // human readable identity - Pubkey() []byte // 512-bit public key - PrivKey() []byte // 512-bit private key + String() string // human readable identity + Pubkey() []byte // 512-bit public key } type SimpleClientIdentity struct { @@ -22,7 +21,7 @@ type SimpleClientIdentity struct { pubkey []byte } -func NewSimpleClientIdentity(clientIdentifier string, version string, customIdentifier string, privkey []byte, pubkey []byte) *SimpleClientIdentity { +func NewSimpleClientIdentity(clientIdentifier string, version string, customIdentifier string, pubkey []byte) *SimpleClientIdentity { clientIdentity := &SimpleClientIdentity{ clientIdentifier: clientIdentifier, version: version, @@ -30,7 +29,6 @@ func NewSimpleClientIdentity(clientIdentifier string, version string, customIden os: runtime.GOOS, implementation: runtime.Version(), pubkey: pubkey, - privkey: privkey, } return clientIdentity -- cgit v1.2.3 From 488a04273639694399929b28c50eadf1bb34405b Mon Sep 17 00:00:00 2001 From: zelig Date: Thu, 29 Jan 2015 01:49:50 +0000 Subject: fix clientidentity test after privkey removed --- p2p/client_identity_test.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'p2p') diff --git a/p2p/client_identity_test.go b/p2p/client_identity_test.go index 61c34fbf1..7b62f05ef 100644 --- a/p2p/client_identity_test.go +++ b/p2p/client_identity_test.go @@ -8,12 +8,8 @@ import ( ) func TestClientIdentity(t *testing.T) { - clientIdentity := NewSimpleClientIdentity("Ethereum(G)", "0.5.16", "test", []byte("privkey"), []byte("pubkey")) - key := clientIdentity.Privkey() - if !bytes.Equal(key, []byte("privkey")) { - t.Errorf("Expected Privkey to be %x, got %x", key, []byte("privkey")) - } - key = clientIdentity.Pubkey() + clientIdentity := NewSimpleClientIdentity("Ethereum(G)", "0.5.16", "test", []byte("pubkey")) + key := clientIdentity.Pubkey() if !bytes.Equal(key, []byte("pubkey")) { t.Errorf("Expected Pubkey to be %x, got %x", key, []byte("pubkey")) } -- cgit v1.2.3 From 2e48d39fc7fc9b8d65e9b6e0ce6863b9374f2233 Mon Sep 17 00:00:00 2001 From: zelig Date: Thu, 29 Jan 2015 03:16:10 +0000 Subject: key generation abstracted out, for testing with deterministic keys --- p2p/crypto.go | 41 ++++++++++++++++++++++++++++++----- p2p/crypto_test.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 92 insertions(+), 12 deletions(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 6a2b99e93..cb0534cba 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -1,6 +1,7 @@ package p2p import ( + // "binary" "crypto/ecdsa" "crypto/rand" "fmt" @@ -38,6 +39,33 @@ func (self hexkey) String() string { return fmt.Sprintf("(%d) %x", len(self), []byte(self)) } +var nonceF = func(b []byte) (n int, err error) { + return rand.Read(b) +} + +var step = 0 +var detnonceF = func(b []byte) (n int, err error) { + step++ + copy(b, crypto.Sha3([]byte("privacy"+string(step)))) + fmt.Printf("detkey %v: %v\n", step, hexkey(b)) + return +} + +var keyF = func() (priv *ecdsa.PrivateKey, err error) { + priv, err = ecdsa.GenerateKey(crypto.S256(), rand.Reader) + if err != nil { + return + } + return +} + +var detkeyF = func() (priv *ecdsa.PrivateKey, err error) { + s := make([]byte, 32) + detnonceF(s) + priv = crypto.ToECDSA(s) + return +} + /* NewSecureSession(connection, privateKey, remotePublicKey, sessionToken, initiator) is called when the peer connection starts to set up a secure session by performing a crypto handshake. @@ -53,7 +81,6 @@ NewSecureSession(connection, privateKey, remotePublicKey, sessionToken, initiato It returns a secretRW which implements the MsgReadWriter interface. */ - func NewSecureSession(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { var auth, initNonce, recNonce []byte var read int @@ -178,7 +205,8 @@ func startHandshake(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte // allocate msgLen long message, var msg []byte = make([]byte, msgLen) initNonce = msg[msgLen-shaLen-1 : msgLen-1] - if _, err = rand.Read(initNonce); err != nil { + fmt.Printf("init-nonce: ") + if _, err = nonceF(initNonce); err != nil { return } // create known message @@ -187,7 +215,8 @@ func startHandshake(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte var sharedSecret = Xor(sessionToken, initNonce) // generate random keypair to use for signing - if randomPrvKey, err = crypto.GenerateKey(); err != nil { + fmt.Printf("init-random-ecdhe-private-key: ") + if randomPrvKey, err = keyF(); err != nil { return } // sign shared secret (message known to both parties): shared-secret @@ -278,11 +307,13 @@ func respondToHandshake(auth []byte, prvKey *ecdsa.PrivateKey, remotePubKeyS, se var resp = make([]byte, resLen) // generate shaLen long nonce respNonce = resp[pubLen : pubLen+shaLen] - if _, err = rand.Read(respNonce); err != nil { + fmt.Printf("rec-nonce: ") + if _, err = nonceF(respNonce); err != nil { return } // generate random keypair for session - if randomPrivKey, err = crypto.GenerateKey(); err != nil { + fmt.Printf("rec-random-ecdhe-private-key: ") + if randomPrivKey, err = keyF(); err != nil { return } // responder auth message diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index f55588ce2..a4bf14ba6 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -2,9 +2,7 @@ package p2p import ( "bytes" - // "crypto/ecdsa" - // "crypto/elliptic" - // "crypto/rand" + "crypto/ecdsa" "fmt" "net" "testing" @@ -71,11 +69,60 @@ func TestSharedSecret(t *testing.T) { } func TestCryptoHandshake(t *testing.T) { + testCryptoHandshakeWithGen(false, t) +} + +func TestTokenCryptoHandshake(t *testing.T) { + testCryptoHandshakeWithGen(true, t) +} + +func TestDetCryptoHandshake(t *testing.T) { + defer testlog(t).detach() + tmpkeyF := keyF + keyF = detkeyF + tmpnonceF := nonceF + nonceF = detnonceF + testCryptoHandshakeWithGen(false, t) + keyF = tmpkeyF + nonceF = tmpnonceF +} + +func TestDetTokenCryptoHandshake(t *testing.T) { + defer testlog(t).detach() + tmpkeyF := keyF + keyF = detkeyF + tmpnonceF := nonceF + nonceF = detnonceF + testCryptoHandshakeWithGen(true, t) + keyF = tmpkeyF + nonceF = tmpnonceF +} + +func testCryptoHandshakeWithGen(token bool, t *testing.T) { + fmt.Printf("init-private-key: ") + prv0, err := keyF() + if err != nil { + t.Errorf("%v", err) + return + } + fmt.Printf("rec-private-key: ") + prv1, err := keyF() + if err != nil { + t.Errorf("%v", err) + return + } + var nonce []byte + if token { + fmt.Printf("session-token: ") + nonce = make([]byte, shaLen) + nonceF(nonce) + } + testCryptoHandshake(prv0, prv1, nonce, t) +} + +func testCryptoHandshake(prv0, prv1 *ecdsa.PrivateKey, sessionToken []byte, t *testing.T) { var err error - var sessionToken []byte - prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) pub0 := &prv0.PublicKey - prv1, _ := crypto.GenerateKey() pub1 := &prv1.PublicKey pub0s := crypto.FromECDSAPub(pub0) @@ -87,12 +134,14 @@ func TestCryptoHandshake(t *testing.T) { if err != nil { t.Errorf("%v", err) } + fmt.Printf("-> %v\n", hexkey(auth)) // receiver reads auth and responds with response response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, remoteInitRandomPubKey, err := respondToHandshake(auth, prv1, pub0s, sessionToken) if err != nil { t.Errorf("%v", err) } + fmt.Printf("<- %v\n", hexkey(response)) // initiator reads receiver's response and the key exchange completes recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prv0) @@ -111,7 +160,7 @@ func TestCryptoHandshake(t *testing.T) { t.Errorf("%v", err) } - fmt.Printf("\nauth (%v) %x\n\nresp (%v) %x\n\n", len(auth), auth, len(response), response) + // fmt.Printf("\nauth (%v) %x\n\nresp (%v) %x\n\n", len(auth), auth, len(response), response) // fmt.Printf("\nauth %x\ninitNonce %x\nresponse%x\nremoteRecNonce %x\nremoteInitNonce %x\nremoteRandomPubKey %x\nrecNonce %x\nremoteInitRandomPubKey %x\ninitSessionToken %x\n\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, remoteInitRandomPubKey, initSessionToken) -- cgit v1.2.3 From 12224c7f5924720767d73f06ed4571dc3ce2f092 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 27 Jan 2015 14:33:26 +0100 Subject: p2p/discover: new package implementing the Node Discovery Protocol --- p2p/discover/table.go | 447 +++++++++++++++++++++++++++++++++++++++++++++ p2p/discover/table_test.go | 403 ++++++++++++++++++++++++++++++++++++++++ p2p/discover/udp.go | 422 ++++++++++++++++++++++++++++++++++++++++++ p2p/discover/udp_test.go | 156 ++++++++++++++++ 4 files changed, 1428 insertions(+) create mode 100644 p2p/discover/table.go create mode 100644 p2p/discover/table_test.go create mode 100644 p2p/discover/udp.go create mode 100644 p2p/discover/udp_test.go (limited to 'p2p') diff --git a/p2p/discover/table.go b/p2p/discover/table.go new file mode 100644 index 000000000..26526330b --- /dev/null +++ b/p2p/discover/table.go @@ -0,0 +1,447 @@ +// Package discover implements the Node Discovery Protocol. +// +// The Node Discovery protocol provides a way to find RLPx nodes that +// can be connected to. It uses a Kademlia-like protocol to maintain a +// distributed database of the IDs and endpoints of all listening +// nodes. +package discover + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "encoding/hex" + "fmt" + "io" + "math/rand" + "net" + "sort" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + alpha = 3 // Kademlia concurrency factor + bucketSize = 16 // Kademlia bucket size + nBuckets = len(NodeID{})*8 + 1 // Number of buckets +) + +type Table struct { + mutex sync.Mutex // protects buckets, their content, and nursery + buckets [nBuckets]*bucket // index of known nodes by distance + nursery []*Node // bootstrap nodes + + net transport + self *Node // metadata of the local node +} + +// transport is implemented by the UDP transport. +// it is an interface so we can test without opening lots of UDP +// sockets and without generating a private key. +type transport interface { + ping(*Node) error + findnode(e *Node, target NodeID) ([]*Node, error) + close() +} + +// bucket contains nodes, ordered by their last activity. +type bucket struct { + lastLookup time.Time + entries []*Node +} + +// Node represents node metadata that is stored in the table. +type Node struct { + Addr *net.UDPAddr + ID NodeID + + active time.Time +} + +type rpcNode struct { + IP string + Port uint16 + ID NodeID +} + +func (n Node) EncodeRLP(w io.Writer) error { + return rlp.Encode(w, rpcNode{IP: n.Addr.IP.String(), Port: uint16(n.Addr.Port), ID: n.ID}) +} +func (n *Node) DecodeRLP(s *rlp.Stream) (err error) { + var ext rpcNode + if err = s.Decode(&ext); err == nil { + n.Addr = &net.UDPAddr{IP: net.ParseIP(ext.IP), Port: int(ext.Port)} + n.ID = ext.ID + } + return err +} + +func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr) *Table { + tab := &Table{net: t, self: &Node{ID: ourID, Addr: ourAddr}} + for i := range tab.buckets { + tab.buckets[i] = &bucket{} + } + return tab +} + +// Bootstrap sets the bootstrap nodes. These nodes are used to connect +// to the network if the table is empty. Bootstrap will also attempt to +// fill the table by performing random lookup operations on the +// network. +func (tab *Table) Bootstrap(nodes []Node) { + tab.mutex.Lock() + // TODO: maybe filter nodes with bad fields (nil, etc.) to avoid strange crashes + tab.nursery = make([]*Node, 0, len(nodes)) + for _, n := range nodes { + cpy := n + tab.nursery = append(tab.nursery, &cpy) + } + tab.mutex.Unlock() + tab.refresh() +} + +// Lookup performs a network search for nodes close +// to the given target. It approaches the target by querying +// nodes that are closer to it on each iteration. +func (tab *Table) Lookup(target NodeID) []*Node { + var ( + asked = make(map[NodeID]bool) + seen = make(map[NodeID]bool) + reply = make(chan []*Node, alpha) + pendingQueries = 0 + ) + // don't query further if we hit the target. + // unlikely to happen often in practice. + asked[target] = true + + tab.mutex.Lock() + // update last lookup stamp (for refresh logic) + tab.buckets[logdist(tab.self.ID, target)].lastLookup = time.Now() + // generate initial result set + result := tab.closest(target, bucketSize) + tab.mutex.Unlock() + + for { + // ask the closest nodes that we haven't asked yet + for i := 0; i < len(result.entries) && pendingQueries < alpha; i++ { + n := result.entries[i] + if !asked[n.ID] { + asked[n.ID] = true + pendingQueries++ + go func() { + result, _ := tab.net.findnode(n, target) + reply <- result + }() + } + } + if pendingQueries == 0 { + // we have asked all closest nodes, stop the search + break + } + + // wait for the next reply + for _, n := range <-reply { + cn := n + if !seen[n.ID] { + seen[n.ID] = true + result.push(cn, bucketSize) + } + } + pendingQueries-- + } + return result.entries +} + +// refresh performs a lookup for a random target to keep buckets full. +func (tab *Table) refresh() { + ld := -1 // logdist of chosen bucket + tab.mutex.Lock() + for i, b := range tab.buckets { + if i > 0 && b.lastLookup.Before(time.Now().Add(-1*time.Hour)) { + ld = i + break + } + } + tab.mutex.Unlock() + + result := tab.Lookup(randomID(tab.self.ID, ld)) + if len(result) == 0 { + // bootstrap the table with a self lookup + tab.mutex.Lock() + tab.add(tab.nursery) + tab.mutex.Unlock() + tab.Lookup(tab.self.ID) + // TODO: the Kademlia paper says that we're supposed to perform + // random lookups in all buckets further away than our closest neighbor. + } +} + +// closest returns the n nodes in the table that are closest to the +// given id. The caller must hold tab.mutex. +func (tab *Table) closest(target NodeID, nresults int) *nodesByDistance { + // This is a very wasteful way to find the closest nodes but + // obviously correct. I believe that tree-based buckets would make + // this easier to implement efficiently. + close := &nodesByDistance{target: target} + for _, b := range tab.buckets { + for _, n := range b.entries { + close.push(n, nresults) + } + } + return close +} + +func (tab *Table) len() (n int) { + for _, b := range tab.buckets { + n += len(b.entries) + } + return n +} + +// bumpOrAdd updates the activity timestamp for the given node and +// attempts to insert the node into a bucket. The returned Node might +// not be part of the table. The caller must hold tab.mutex. +func (tab *Table) bumpOrAdd(node NodeID, from *net.UDPAddr) (n *Node) { + b := tab.buckets[logdist(tab.self.ID, node)] + if n = b.bump(node); n == nil { + n = &Node{ID: node, Addr: from, active: time.Now()} + if len(b.entries) == bucketSize { + tab.pingReplace(n, b) + } else { + b.entries = append(b.entries, n) + } + } + return n +} + +func (tab *Table) pingReplace(n *Node, b *bucket) { + old := b.entries[bucketSize-1] + go func() { + if err := tab.net.ping(old); err == nil { + // it responded, we don't need to replace it. + return + } + // it didn't respond, replace the node if it is still the oldest node. + tab.mutex.Lock() + if len(b.entries) > 0 && b.entries[len(b.entries)-1] == old { + // slide down other entries and put the new one in front. + copy(b.entries[1:], b.entries) + b.entries[0] = n + } + tab.mutex.Unlock() + }() +} + +// bump updates the activity timestamp for the given node. +// The caller must hold tab.mutex. +func (tab *Table) bump(node NodeID) { + tab.buckets[logdist(tab.self.ID, node)].bump(node) +} + +// add puts the entries into the table if their corresponding +// bucket is not full. The caller must hold tab.mutex. +func (tab *Table) add(entries []*Node) { +outer: + for _, n := range entries { + if n == nil || n.ID == tab.self.ID { + // skip bad entries. The RLP decoder returns nil for empty + // input lists. + continue + } + bucket := tab.buckets[logdist(tab.self.ID, n.ID)] + for i := range bucket.entries { + if bucket.entries[i].ID == n.ID { + // already in bucket + continue outer + } + } + if len(bucket.entries) < bucketSize { + bucket.entries = append(bucket.entries, n) + } + } +} + +func (b *bucket) bump(id NodeID) *Node { + for i, n := range b.entries { + if n.ID == id { + n.active = time.Now() + // move it to the front + copy(b.entries[1:], b.entries[:i+1]) + b.entries[0] = n + return n + } + } + return nil +} + +// nodesByDistance is a list of nodes, ordered by +// distance to target. +type nodesByDistance struct { + entries []*Node + target NodeID +} + +// push adds the given node to the list, keeping the total size below maxElems. +func (h *nodesByDistance) push(n *Node, maxElems int) { + ix := sort.Search(len(h.entries), func(i int) bool { + return distcmp(h.target, h.entries[i].ID, n.ID) > 0 + }) + if len(h.entries) < maxElems { + h.entries = append(h.entries, n) + } + if ix == len(h.entries) { + // farther away than all nodes we already have. + // if there was room for it, the node is now the last element. + } else { + // slide existing entries down to make room + // this will overwrite the entry we just appended. + copy(h.entries[ix+1:], h.entries[ix:]) + h.entries[ix] = n + } +} + +// NodeID is a unique identifier for each node. +// The node identifier is a marshaled elliptic curve public key. +type NodeID [512 / 8]byte + +// NodeID prints as a long hexadecimal number. +func (n NodeID) String() string { + return fmt.Sprintf("%#x", n[:]) +} + +// The Go syntax representation of a NodeID is a call to HexID. +func (n NodeID) GoString() string { + return fmt.Sprintf("HexID(\"%#x\")", n[:]) +} + +// HexID converts a hex string to a NodeID. +// The string may be prefixed with 0x. +func HexID(in string) NodeID { + if strings.HasPrefix(in, "0x") { + in = in[2:] + } + var id NodeID + b, err := hex.DecodeString(in) + if err != nil { + panic(err) + } else if len(b) != len(id) { + panic("wrong length") + } + copy(id[:], b) + return id +} + +func newNodeID(priv *ecdsa.PrivateKey) (id NodeID) { + pubkey := elliptic.Marshal(priv.Curve, priv.X, priv.Y) + if len(pubkey)-1 != len(id) { + panic(fmt.Errorf("invalid key: need %d bit pubkey, got %d bits", (len(id)+1)*8, len(pubkey))) + } + copy(id[:], pubkey[1:]) + return id +} + +// recoverNodeID computes the public key used to sign the +// given hash from the signature. +func recoverNodeID(hash, sig []byte) (id NodeID, err error) { + pubkey, err := secp256k1.RecoverPubkey(hash, sig) + if err != nil { + return id, err + } + if len(pubkey)-1 != len(id) { + return id, fmt.Errorf("recovered pubkey has %d bits, want %d bits", len(pubkey)*8, (len(id)+1)*8) + } + for i := range id { + id[i] = pubkey[i+1] + } + return id, nil +} + +// distcmp compares the distances a->target and b->target. +// Returns -1 if a is closer to target, 1 if b is closer to target +// and 0 if they are equal. +func distcmp(target, a, b NodeID) int { + for i := range target { + da := a[i] ^ target[i] + db := b[i] ^ target[i] + if da > db { + return 1 + } else if da < db { + return -1 + } + } + return 0 +} + +// table of leading zero counts for bytes [0..255] +var lzcount = [256]int{ + 8, 7, 6, 6, 5, 5, 5, 5, + 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 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, + 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, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +} + +// logdist returns the logarithmic distance between a and b, log2(a ^ b). +func logdist(a, b NodeID) int { + lz := 0 + for i := range a { + x := a[i] ^ b[i] + if x == 0 { + lz += 8 + } else { + lz += lzcount[x] + break + } + } + return len(a)*8 - lz +} + +// randomID returns a random NodeID such that logdist(a, b) == n +func randomID(a NodeID, n int) (b NodeID) { + if n == 0 { + return a + } + // flip bit at position n, fill the rest with random bits + b = a + pos := len(a) - n/8 - 1 + bit := byte(0x01) << (byte(n%8) - 1) + if bit == 0 { + pos++ + bit = 0x80 + } + b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits + for i := pos + 1; i < len(a); i++ { + b[i] = byte(rand.Intn(255)) + } + return b +} diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go new file mode 100644 index 000000000..88563fe65 --- /dev/null +++ b/p2p/discover/table_test.go @@ -0,0 +1,403 @@ +package discover + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + "math/rand" + "net" + "reflect" + "testing" + "testing/quick" + "time" + + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + quickrand = rand.New(rand.NewSource(time.Now().Unix())) + quickcfg = &quick.Config{MaxCount: 5000, Rand: quickrand} +) + +func TestHexID(t *testing.T) { + ref := NodeID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} + id1 := HexID("0x000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + id2 := HexID("000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + + if id1 != ref { + t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:]) + } + if id2 != ref { + t.Errorf("wrong id2\ngot %v\nwant %v", id2[:], ref[:]) + } +} + +func TestNodeID_recover(t *testing.T) { + prv := newkey() + hash := make([]byte, 32) + sig, err := crypto.Sign(hash, prv) + if err != nil { + t.Fatalf("signing error: %v", err) + } + + pub := newNodeID(prv) + recpub, err := recoverNodeID(hash, sig) + if err != nil { + t.Fatalf("recovery error: %v", err) + } + if pub != recpub { + t.Errorf("recovered wrong pubkey:\ngot: %v\nwant: %v", recpub, pub) + } +} + +func TestNodeID_distcmp(t *testing.T) { + distcmpBig := func(target, a, b NodeID) int { + tbig := new(big.Int).SetBytes(target[:]) + abig := new(big.Int).SetBytes(a[:]) + bbig := new(big.Int).SetBytes(b[:]) + return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig)) + } + if err := quick.CheckEqual(distcmp, distcmpBig, quickcfg); err != nil { + t.Error(err) + } +} + +// the random tests is likely to miss the case where they're equal. +func TestNodeID_distcmpEqual(t *testing.T) { + base := NodeID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + x := NodeID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} + if distcmp(base, x, x) != 0 { + t.Errorf("distcmp(base, x, x) != 0") + } +} + +func TestNodeID_logdist(t *testing.T) { + logdistBig := func(a, b NodeID) int { + abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:]) + return new(big.Int).Xor(abig, bbig).BitLen() + } + if err := quick.CheckEqual(logdist, logdistBig, quickcfg); err != nil { + t.Error(err) + } +} + +// the random tests is likely to miss the case where they're equal. +func TestNodeID_logdistEqual(t *testing.T) { + x := NodeID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + if logdist(x, x) != 0 { + t.Errorf("logdist(x, x) != 0") + } +} + +func TestNodeID_randomID(t *testing.T) { + // we don't use quick.Check here because its output isn't + // very helpful when the test fails. + for i := 0; i < quickcfg.MaxCount; i++ { + a := gen(NodeID{}, quickrand).(NodeID) + dist := quickrand.Intn(len(NodeID{}) * 8) + result := randomID(a, dist) + actualdist := logdist(result, a) + + if dist != actualdist { + t.Log("a: ", a) + t.Log("result:", result) + t.Fatalf("#%d: distance of result is %d, want %d", i, actualdist, dist) + } + } +} + +func (NodeID) Generate(rand *rand.Rand, size int) reflect.Value { + var id NodeID + m := rand.Intn(len(id)) + for i := len(id) - 1; i > m; i-- { + id[i] = byte(rand.Uint32()) + } + return reflect.ValueOf(id) +} + +func TestTable_bumpOrAddPingReplace(t *testing.T) { + pingC := make(pingC) + tab := newTable(pingC, NodeID{}, &net.UDPAddr{}) + last := fillBucket(tab, 200) + + // this bumpOrAdd should not replace the last node + // because the node replies to ping. + new := tab.bumpOrAdd(randomID(tab.self.ID, 200), nil) + + pinged := <-pingC + if pinged != last.ID { + t.Fatalf("pinged wrong node: %v\nwant %v", pinged, last.ID) + } + + tab.mutex.Lock() + defer tab.mutex.Unlock() + if l := len(tab.buckets[200].entries); l != bucketSize { + t.Errorf("wrong bucket size after bumpOrAdd: got %d, want %d", bucketSize, l) + } + if !contains(tab.buckets[200].entries, last.ID) { + t.Error("last entry was removed") + } + if contains(tab.buckets[200].entries, new.ID) { + t.Error("new entry was added") + } +} + +func TestTable_bumpOrAddPingTimeout(t *testing.T) { + tab := newTable(pingC(nil), NodeID{}, &net.UDPAddr{}) + last := fillBucket(tab, 200) + + // this bumpOrAdd should replace the last node + // because the node does not reply to ping. + new := tab.bumpOrAdd(randomID(tab.self.ID, 200), nil) + + // wait for async bucket update. damn. this needs to go away. + time.Sleep(2 * time.Millisecond) + + tab.mutex.Lock() + defer tab.mutex.Unlock() + if l := len(tab.buckets[200].entries); l != bucketSize { + t.Errorf("wrong bucket size after bumpOrAdd: got %d, want %d", bucketSize, l) + } + if contains(tab.buckets[200].entries, last.ID) { + t.Error("last entry was not removed") + } + if !contains(tab.buckets[200].entries, new.ID) { + t.Error("new entry was not added") + } +} + +func fillBucket(tab *Table, ld int) (last *Node) { + b := tab.buckets[ld] + for len(b.entries) < bucketSize { + b.entries = append(b.entries, &Node{ID: randomID(tab.self.ID, ld)}) + } + return b.entries[bucketSize-1] +} + +type pingC chan NodeID + +func (t pingC) findnode(n *Node, target NodeID) ([]*Node, error) { + panic("findnode called on pingRecorder") +} +func (t pingC) close() { + panic("close called on pingRecorder") +} +func (t pingC) ping(n *Node) error { + if t == nil { + return errTimeout + } + t <- n.ID + return nil +} + +func TestTable_bump(t *testing.T) { + tab := newTable(nil, NodeID{}, &net.UDPAddr{}) + + // add an old entry and two recent ones + oldactive := time.Now().Add(-2 * time.Minute) + old := &Node{ID: randomID(tab.self.ID, 200), active: oldactive} + others := []*Node{ + &Node{ID: randomID(tab.self.ID, 200), active: time.Now()}, + &Node{ID: randomID(tab.self.ID, 200), active: time.Now()}, + } + tab.add(append(others, old)) + if tab.buckets[200].entries[0] == old { + t.Fatal("old entry is at front of bucket") + } + + // bumping the old entry should move it to the front + tab.bump(old.ID) + if old.active == oldactive { + t.Error("activity timestamp not updated") + } + if tab.buckets[200].entries[0] != old { + t.Errorf("bumped entry did not move to the front of bucket") + } +} + +func TestTable_closest(t *testing.T) { + t.Parallel() + + test := func(test *closeTest) bool { + // for any node table, Target and N + tab := newTable(nil, test.Self, &net.UDPAddr{}) + tab.add(test.All) + + // check that doClosest(Target, N) returns nodes + result := tab.closest(test.Target, test.N).entries + if hasDuplicates(result) { + t.Errorf("result contains duplicates") + return false + } + if !sortedByDistanceTo(test.Target, result) { + t.Errorf("result is not sorted by distance to target") + return false + } + + // check that the number of results is min(N, tablen) + wantN := test.N + if tlen := tab.len(); tlen < test.N { + wantN = tlen + } + if len(result) != wantN { + t.Errorf("wrong number of nodes: got %d, want %d", len(result), wantN) + return false + } else if len(result) == 0 { + return true // no need to check distance + } + + // check that the result nodes have minimum distance to target. + for _, b := range tab.buckets { + for _, n := range b.entries { + if contains(result, n.ID) { + continue // don't run the check below for nodes in result + } + farthestResult := result[len(result)-1].ID + if distcmp(test.Target, n.ID, farthestResult) < 0 { + t.Errorf("table contains node that is closer to target but it's not in result") + t.Logf(" Target: %v", test.Target) + t.Logf(" Farthest Result: %v", farthestResult) + t.Logf(" ID: %v", n.ID) + return false + } + } + } + return true + } + if err := quick.Check(test, quickcfg); err != nil { + t.Error(err) + } +} + +type closeTest struct { + Self NodeID + Target NodeID + All []*Node + N int +} + +func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value { + t := &closeTest{ + Self: gen(NodeID{}, rand).(NodeID), + Target: gen(NodeID{}, rand).(NodeID), + N: rand.Intn(bucketSize), + } + for _, id := range gen([]NodeID{}, rand).([]NodeID) { + t.All = append(t.All, &Node{ID: id}) + } + return reflect.ValueOf(t) +} + +func TestTable_Lookup(t *testing.T) { + self := gen(NodeID{}, quickrand).(NodeID) + target := randomID(self, 200) + transport := findnodeOracle{t, target} + tab := newTable(transport, self, &net.UDPAddr{}) + + // lookup on empty table returns no nodes + if results := tab.Lookup(target); len(results) > 0 { + t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results) + } + // seed table with initial node (otherwise lookup will terminate immediately) + tab.bumpOrAdd(randomID(target, 200), &net.UDPAddr{Port: 200}) + + results := tab.Lookup(target) + t.Logf("results:") + for _, e := range results { + t.Logf(" ld=%d, %v", logdist(target, e.ID), e.ID) + } + if len(results) != bucketSize { + t.Errorf("wrong number of results: got %d, want %d", len(results), bucketSize) + } + if hasDuplicates(results) { + t.Errorf("result set contains duplicate entries") + } + if !sortedByDistanceTo(target, results) { + t.Errorf("result set not sorted by distance to target") + } + if !contains(results, target) { + t.Errorf("result set does not contain target") + } +} + +// findnode on this transport always returns at least one node +// that is one bucket closer to the target. +type findnodeOracle struct { + t *testing.T + target NodeID +} + +func (t findnodeOracle) findnode(n *Node, target NodeID) ([]*Node, error) { + t.t.Logf("findnode query at dist %d", n.Addr.Port) + // current log distance is encoded in port number + var result []*Node + switch port := n.Addr.Port; port { + case 0: + panic("query to node at distance 0") + case 1: + result = append(result, &Node{ID: t.target, Addr: &net.UDPAddr{Port: 0}}) + default: + // TODO: add more randomness to distances + port-- + for i := 0; i < bucketSize; i++ { + result = append(result, &Node{ID: randomID(t.target, port), Addr: &net.UDPAddr{Port: port}}) + } + } + return result, nil +} + +func (t findnodeOracle) close() {} + +func (t findnodeOracle) ping(n *Node) error { + return errors.New("ping is not supported by this transport") +} + +func hasDuplicates(slice []*Node) bool { + seen := make(map[NodeID]bool) + for _, e := range slice { + if seen[e.ID] { + return true + } + seen[e.ID] = true + } + return false +} + +func sortedByDistanceTo(distbase NodeID, slice []*Node) bool { + var last NodeID + for i, e := range slice { + if i > 0 && distcmp(distbase, e.ID, last) < 0 { + return false + } + last = e.ID + } + return true +} + +func contains(ns []*Node, id NodeID) bool { + for _, n := range ns { + if n.ID == id { + return true + } + } + return false +} + +// gen wraps quick.Value so it's easier to use. +// it generates a random value of the given value's type. +func gen(typ interface{}, rand *rand.Rand) interface{} { + v, ok := quick.Value(reflect.TypeOf(typ), rand) + if !ok { + panic(fmt.Sprintf("couldn't generate random value of type %T", typ)) + } + return v.Interface() +} + +func newkey() *ecdsa.PrivateKey { + key, err := crypto.GenerateKey() + if err != nil { + panic("couldn't generate key: " + err.Error()) + } + return key +} diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go new file mode 100644 index 000000000..ec1f62dac --- /dev/null +++ b/p2p/discover/udp.go @@ -0,0 +1,422 @@ +package discover + +import ( + "bytes" + "crypto/ecdsa" + "errors" + "fmt" + "net" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/rlp" +) + +var log = logger.NewLogger("P2P Discovery") + +// Errors +var ( + errPacketTooSmall = errors.New("too small") + errBadHash = errors.New("bad hash") + errExpired = errors.New("expired") + errTimeout = errors.New("RPC timeout") + errClosed = errors.New("socket closed") +) + +// Timeouts +const ( + respTimeout = 300 * time.Millisecond + sendTimeout = 300 * time.Millisecond + expiration = 3 * time.Second + + refreshInterval = 1 * time.Hour +) + +// RPC packet types +const ( + pingPacket = iota + 1 // zero is 'reserved' + pongPacket + findnodePacket + neighborsPacket +) + +// RPC request structures +type ( + ping struct { + IP string // our IP + Port uint16 // our port + Expiration uint64 + } + + // reply to Ping + pong struct { + ReplyTok []byte + Expiration uint64 + } + + findnode struct { + // Id to look up. The responding node will send back nodes + // closest to the target. + Target NodeID + Expiration uint64 + } + + // reply to findnode + neighbors struct { + Nodes []*Node + Expiration uint64 + } +) + +// udp implements the RPC protocol. +type udp struct { + conn *net.UDPConn + priv *ecdsa.PrivateKey + addpending chan *pending + replies chan reply + closing chan struct{} + + *Table +} + +// pending represents a pending reply. +// +// some implementations of the protocol wish to send more than one +// reply packet to findnode. in general, any neighbors packet cannot +// be matched up with a specific findnode packet. +// +// our implementation handles this by storing a callback function for +// each pending reply. incoming packets from a node are dispatched +// to all the callback functions for that node. +type pending struct { + // these fields must match in the reply. + from NodeID + ptype byte + + // time when the request must complete + deadline time.Time + + // callback is called when a matching reply arrives. if it returns + // true, the callback is removed from the pending reply queue. + // if it returns false, the reply is considered incomplete and + // the callback will be invoked again for the next matching reply. + callback func(resp interface{}) (done bool) + + // errc receives nil when the callback indicates completion or an + // error if no further reply is received within the timeout. + errc chan<- error +} + +type reply struct { + from NodeID + ptype byte + data interface{} +} + +// ListenUDP returns a new table that listens for UDP packets on laddr. +func ListenUDP(priv *ecdsa.PrivateKey, laddr string) (*Table, error) { + net, realaddr, err := listen(priv, laddr) + if err != nil { + return nil, err + } + net.Table = newTable(net, newNodeID(priv), realaddr) + log.DebugDetailf("Listening on %v, my ID %x\n", realaddr, net.self.ID[:]) + return net.Table, nil +} + +func listen(priv *ecdsa.PrivateKey, laddr string) (*udp, *net.UDPAddr, error) { + addr, err := net.ResolveUDPAddr("udp", laddr) + if err != nil { + return nil, nil, err + } + conn, err := net.ListenUDP("udp", addr) + if err != nil { + return nil, nil, err + } + realaddr := conn.LocalAddr().(*net.UDPAddr) + + udp := &udp{ + conn: conn, + priv: priv, + closing: make(chan struct{}), + addpending: make(chan *pending), + replies: make(chan reply), + } + go udp.loop() + go udp.readLoop() + return udp, realaddr, nil +} + +func (t *udp) close() { + close(t.closing) + t.conn.Close() + // TODO: wait for the loops to end. +} + +// ping sends a ping message to the given node and waits for a reply. +func (t *udp) ping(e *Node) error { + // TODO: maybe check for ReplyTo field in callback to measure RTT + errc := t.pending(e.ID, pongPacket, func(interface{}) bool { return true }) + t.send(e, pingPacket, ping{ + IP: t.self.Addr.String(), + Port: uint16(t.self.Addr.Port), + Expiration: uint64(time.Now().Add(expiration).Unix()), + }) + return <-errc +} + +// findnode sends a findnode request to the given node and waits until +// the node has sent up to k neighbors. +func (t *udp) findnode(to *Node, target NodeID) ([]*Node, error) { + nodes := make([]*Node, 0, bucketSize) + nreceived := 0 + errc := t.pending(to.ID, neighborsPacket, func(r interface{}) bool { + reply := r.(*neighbors) + for i := 0; i < len(reply.Nodes); i++ { + nreceived++ + n := reply.Nodes[i] + if validAddr(n.Addr) && n.ID != t.self.ID { + nodes = append(nodes, n) + } + } + return nreceived == bucketSize + }) + + t.send(to, findnodePacket, findnode{ + Target: target, + Expiration: uint64(time.Now().Add(expiration).Unix()), + }) + err := <-errc + return nodes, err +} + +func validAddr(a *net.UDPAddr) bool { + return !a.IP.IsMulticast() && !a.IP.IsUnspecified() && a.Port != 0 +} + +// pending adds a reply callback to the pending reply queue. +// see the documentation of type pending for a detailed explanation. +func (t *udp) pending(id NodeID, ptype byte, callback func(interface{}) bool) <-chan error { + ch := make(chan error, 1) + p := &pending{from: id, ptype: ptype, callback: callback, errc: ch} + select { + case t.addpending <- p: + // loop will handle it + case <-t.closing: + ch <- errClosed + } + return ch +} + +// loop runs in its own goroutin. it keeps track of +// the refresh timer and the pending reply queue. +func (t *udp) loop() { + var ( + pending []*pending + nextDeadline time.Time + timeout = time.NewTimer(0) + refresh = time.NewTicker(refreshInterval) + ) + <-timeout.C // ignore first timeout + defer refresh.Stop() + defer timeout.Stop() + + rearmTimeout := func() { + if len(pending) == 0 || nextDeadline == pending[0].deadline { + return + } + nextDeadline = pending[0].deadline + timeout.Reset(nextDeadline.Sub(time.Now())) + } + + for { + select { + case <-refresh.C: + go t.refresh() + + case <-t.closing: + for _, p := range pending { + p.errc <- errClosed + } + return + + case p := <-t.addpending: + p.deadline = time.Now().Add(respTimeout) + pending = append(pending, p) + rearmTimeout() + + case reply := <-t.replies: + // run matching callbacks, remove if they return false. + for i, p := range pending { + if reply.from == p.from && reply.ptype == p.ptype && p.callback(reply.data) { + p.errc <- nil + copy(pending[i:], pending[i+1:]) + pending = pending[:len(pending)-1] + i-- + } + } + rearmTimeout() + + case now := <-timeout.C: + // notify and remove callbacks whose deadline is in the past. + i := 0 + for ; i < len(pending) && now.After(pending[i].deadline); i++ { + pending[i].errc <- errTimeout + } + if i > 0 { + copy(pending, pending[i:]) + pending = pending[:len(pending)-i] + } + rearmTimeout() + } + } +} + +const ( + macSize = 256 / 8 + sigSize = 520 / 8 + headSize = macSize + sigSize // space of packet frame data +) + +var headSpace = make([]byte, headSize) + +func (t *udp) send(to *Node, ptype byte, req interface{}) error { + b := new(bytes.Buffer) + b.Write(headSpace) + b.WriteByte(ptype) + if err := rlp.Encode(b, req); err != nil { + log.Errorln("error encoding packet:", err) + return err + } + + packet := b.Bytes() + sig, err := crypto.Sign(crypto.Sha3(packet[headSize:]), t.priv) + if err != nil { + log.Errorln("could not sign packet:", err) + return err + } + copy(packet[macSize:], sig) + // add the hash to the front. Note: this doesn't protect the + // packet in any way. Our public key will be part of this hash in + // the future. + copy(packet, crypto.Sha3(packet[macSize:])) + + log.DebugDetailf(">>> %v %T %v\n", to.Addr, req, req) + if _, err = t.conn.WriteToUDP(packet, to.Addr); err != nil { + log.DebugDetailln("UDP send failed:", err) + } + return err +} + +// readLoop runs in its own goroutine. it handles incoming UDP packets. +func (t *udp) readLoop() { + defer t.conn.Close() + buf := make([]byte, 4096) // TODO: good buffer size + for { + nbytes, from, err := t.conn.ReadFromUDP(buf) + if err != nil { + return + } + if err := t.packetIn(from, buf[:nbytes]); err != nil { + log.Debugf("Bad packet from %v: %v\n", from, err) + } + } +} + +func (t *udp) packetIn(from *net.UDPAddr, buf []byte) error { + if len(buf) < headSize+1 { + return errPacketTooSmall + } + hash, sig, sigdata := buf[:macSize], buf[macSize:headSize], buf[headSize:] + shouldhash := crypto.Sha3(buf[macSize:]) + if !bytes.Equal(hash, shouldhash) { + return errBadHash + } + fromID, err := recoverNodeID(crypto.Sha3(buf[headSize:]), sig) + if err != nil { + return err + } + + var req interface { + handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error + } + switch ptype := sigdata[0]; ptype { + case pingPacket: + req = new(ping) + case pongPacket: + req = new(pong) + case findnodePacket: + req = new(findnode) + case neighborsPacket: + req = new(neighbors) + default: + return fmt.Errorf("unknown type: %d", ptype) + } + if err := rlp.Decode(bytes.NewReader(sigdata[1:]), req); err != nil { + return err + } + log.DebugDetailf("<<< %v %T %v\n", from, req, req) + return req.handle(t, from, fromID, hash) +} + +func (req *ping) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { + if expired(req.Expiration) { + return errExpired + } + t.mutex.Lock() + // Note: we're ignoring the provided IP/Port right now. + e := t.bumpOrAdd(fromID, from) + t.mutex.Unlock() + + t.send(e, pongPacket, pong{ + ReplyTok: mac, + Expiration: uint64(time.Now().Add(expiration).Unix()), + }) + return nil +} + +func (req *pong) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { + if expired(req.Expiration) { + return errExpired + } + t.mutex.Lock() + t.bump(fromID) + t.mutex.Unlock() + + t.replies <- reply{fromID, pongPacket, req} + return nil +} + +func (req *findnode) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { + if expired(req.Expiration) { + return errExpired + } + t.mutex.Lock() + e := t.bumpOrAdd(fromID, from) + closest := t.closest(req.Target, bucketSize).entries + t.mutex.Unlock() + + t.send(e, neighborsPacket, neighbors{ + Nodes: closest, + Expiration: uint64(time.Now().Add(expiration).Unix()), + }) + return nil +} + +func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { + if expired(req.Expiration) { + return errExpired + } + t.mutex.Lock() + t.bump(fromID) + t.add(req.Nodes) + t.mutex.Unlock() + + t.replies <- reply{fromID, neighborsPacket, req} + return nil +} + +func expired(ts uint64) bool { + return time.Unix(int64(ts), 0).Before(time.Now()) +} diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go new file mode 100644 index 000000000..f2ab2b661 --- /dev/null +++ b/p2p/discover/udp_test.go @@ -0,0 +1,156 @@ +package discover + +import ( + logpkg "log" + "net" + "os" + "testing" + "time" + + "github.com/ethereum/go-ethereum/logger" +) + +func init() { + logger.AddLogSystem(logger.NewStdLogSystem(os.Stdout, logpkg.LstdFlags, logger.DebugLevel)) +} + +func TestUDP_ping(t *testing.T) { + t.Parallel() + + n1, _ := ListenUDP(newkey(), "127.0.0.1:0") + n2, _ := ListenUDP(newkey(), "127.0.0.1:0") + defer n1.net.close() + defer n2.net.close() + + if err := n1.net.ping(n2.self); err != nil { + t.Fatalf("ping error: %v", err) + } + if find(n2, n1.self.ID) == nil { + t.Errorf("node 2 does not contain id of node 1") + } + if e := find(n1, n2.self.ID); e != nil { + t.Errorf("node 1 does contains id of node 2: %v", e) + } +} + +func find(tab *Table, id NodeID) *Node { + for _, b := range tab.buckets { + for _, e := range b.entries { + if e.ID == id { + return e + } + } + } + return nil +} + +func TestUDP_findnode(t *testing.T) { + t.Parallel() + + n1, _ := ListenUDP(newkey(), "127.0.0.1:0") + n2, _ := ListenUDP(newkey(), "127.0.0.1:0") + defer n1.net.close() + defer n2.net.close() + + entry := &Node{ID: NodeID{1}, Addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 15}} + n2.add([]*Node{entry}) + + target := randomID(n1.self.ID, 100) + result, _ := n1.net.findnode(n2.self, target) + if len(result) != 1 { + t.Fatalf("wrong number of results: got %d, want 1", len(result)) + } + if result[0].ID != entry.ID { + t.Errorf("wrong result: got %v, want %v", result[0], entry) + } +} + +func TestUDP_replytimeout(t *testing.T) { + t.Parallel() + + // reserve a port so we don't talk to an existing service by accident + addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:0") + fd, err := net.ListenUDP("udp", addr) + if err != nil { + t.Fatal(err) + } + defer fd.Close() + + n1, _ := ListenUDP(newkey(), "127.0.0.1:0") + defer n1.net.close() + n2 := n1.bumpOrAdd(randomID(n1.self.ID, 10), fd.LocalAddr().(*net.UDPAddr)) + + if err := n1.net.ping(n2); err != errTimeout { + t.Error("expected timeout error, got", err) + } + + if result, err := n1.net.findnode(n2, n1.self.ID); err != errTimeout { + t.Error("expected timeout error, got", err) + } else if len(result) > 0 { + t.Error("expected empty result, got", result) + } +} + +func TestUDP_findnodeMultiReply(t *testing.T) { + t.Parallel() + + n1, _ := ListenUDP(newkey(), "127.0.0.1:0") + n2, _ := ListenUDP(newkey(), "127.0.0.1:0") + udp2 := n2.net.(*udp) + defer n1.net.close() + defer n2.net.close() + + nodes := make([]*Node, bucketSize) + for i := range nodes { + nodes[i] = &Node{ + Addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: i + 1}, + ID: randomID(n2.self.ID, i+1), + } + } + + // ask N2 for neighbors. it will send an empty reply back. + // the request will wait for up to bucketSize replies. + resultC := make(chan []*Node) + go func() { + ns, err := n1.net.findnode(n2.self, n1.self.ID) + if err != nil { + t.Error("findnode error:", err) + } + resultC <- ns + }() + + // send a few more neighbors packets to N1. + // it should collect those. + for end := 0; end < len(nodes); { + off := end + if end = end + 5; end > len(nodes) { + end = len(nodes) + } + udp2.send(n1.self, neighborsPacket, neighbors{ + Nodes: nodes[off:end], + Expiration: uint64(time.Now().Add(10 * time.Second).Unix()), + }) + } + + // check that they are all returned. we cannot just check for + // equality because they might not be returned in the order they + // were sent. + result := <-resultC + if hasDuplicates(result) { + t.Error("result slice contains duplicates") + } + if len(result) != len(nodes) { + t.Errorf("wrong number of nodes returned: got %d, want %d", len(result), len(nodes)) + } + matched := make(map[NodeID]bool) + for _, n := range result { + for _, expn := range nodes { + if n.ID == expn.ID { // && bytes.Equal(n.Addr.IP, expn.Addr.IP) && n.Addr.Port == expn.Addr.Port { + matched[n.ID] = true + } + } + } + if len(matched) != len(nodes) { + t.Errorf("wrong number of matching nodes: got %d, want %d", len(matched), len(nodes)) + } +} -- cgit v1.2.3 From 739066ec56393e63b93531787746fb8ba5f1df15 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 5 Feb 2015 03:07:18 +0100 Subject: p2p/discover: add some helper functions --- p2p/discover/table.go | 37 +++++++++++++++++++++++++++++-------- p2p/discover/table_test.go | 6 +++--- p2p/discover/udp.go | 4 ++-- p2p/discover/udp_test.go | 14 +++++++------- 4 files changed, 41 insertions(+), 20 deletions(-) (limited to 'p2p') diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 26526330b..ea9680404 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -87,6 +87,16 @@ func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr) *Table { return tab } +// Self returns the local node ID. +func (tab *Table) Self() NodeID { + return tab.self.ID +} + +// Close terminates the network listener. +func (tab *Table) Close() { + tab.net.close() +} + // Bootstrap sets the bootstrap nodes. These nodes are used to connect // to the network if the table is empty. Bootstrap will also attempt to // fill the table by performing random lookup operations on the @@ -319,27 +329,38 @@ func (n NodeID) GoString() string { // HexID converts a hex string to a NodeID. // The string may be prefixed with 0x. -func HexID(in string) NodeID { +func HexID(in string) (NodeID, error) { if strings.HasPrefix(in, "0x") { in = in[2:] } var id NodeID b, err := hex.DecodeString(in) if err != nil { - panic(err) + return id, err } else if len(b) != len(id) { - panic("wrong length") + return id, fmt.Errorf("wrong length, need %d hex bytes", len(id)) } copy(id[:], b) + return id, nil +} + +// MustHexID converts a hex string to a NodeID. +// It panics if the string is not a valid NodeID. +func MustHexID(in string) NodeID { + id, err := HexID(in) + if err != nil { + panic(err) + } return id } -func newNodeID(priv *ecdsa.PrivateKey) (id NodeID) { - pubkey := elliptic.Marshal(priv.Curve, priv.X, priv.Y) - if len(pubkey)-1 != len(id) { - panic(fmt.Errorf("invalid key: need %d bit pubkey, got %d bits", (len(id)+1)*8, len(pubkey))) +func PubkeyID(pub *ecdsa.PublicKey) NodeID { + var id NodeID + pbytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y) + if len(pbytes)-1 != len(id) { + panic(fmt.Errorf("invalid key: need %d bit pubkey, got %d bits", (len(id)+1)*8, len(pbytes))) } - copy(id[:], pubkey[1:]) + copy(id[:], pbytes[1:]) return id } diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index 88563fe65..90f89b147 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -22,8 +22,8 @@ var ( func TestHexID(t *testing.T) { ref := NodeID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} - id1 := HexID("0x000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") - id2 := HexID("000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + id1 := MustHexID("0x000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + id2 := MustHexID("000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") if id1 != ref { t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:]) @@ -41,7 +41,7 @@ func TestNodeID_recover(t *testing.T) { t.Fatalf("signing error: %v", err) } - pub := newNodeID(prv) + pub := PubkeyID(&prv.PublicKey) recpub, err := recoverNodeID(hash, sig) if err != nil { t.Fatalf("recovery error: %v", err) diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go index ec1f62dac..449139c1d 100644 --- a/p2p/discover/udp.go +++ b/p2p/discover/udp.go @@ -120,8 +120,8 @@ func ListenUDP(priv *ecdsa.PrivateKey, laddr string) (*Table, error) { if err != nil { return nil, err } - net.Table = newTable(net, newNodeID(priv), realaddr) - log.DebugDetailf("Listening on %v, my ID %x\n", realaddr, net.self.ID[:]) + net.Table = newTable(net, PubkeyID(&priv.PublicKey), realaddr) + log.Debugf("Listening on %v, my ID %x\n", realaddr, net.self.ID[:]) return net.Table, nil } diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go index f2ab2b661..8ed6ec9c0 100644 --- a/p2p/discover/udp_test.go +++ b/p2p/discover/udp_test.go @@ -19,8 +19,8 @@ func TestUDP_ping(t *testing.T) { n1, _ := ListenUDP(newkey(), "127.0.0.1:0") n2, _ := ListenUDP(newkey(), "127.0.0.1:0") - defer n1.net.close() - defer n2.net.close() + defer n1.Close() + defer n2.Close() if err := n1.net.ping(n2.self); err != nil { t.Fatalf("ping error: %v", err) @@ -49,8 +49,8 @@ func TestUDP_findnode(t *testing.T) { n1, _ := ListenUDP(newkey(), "127.0.0.1:0") n2, _ := ListenUDP(newkey(), "127.0.0.1:0") - defer n1.net.close() - defer n2.net.close() + defer n1.Close() + defer n2.Close() entry := &Node{ID: NodeID{1}, Addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 15}} n2.add([]*Node{entry}) @@ -77,7 +77,7 @@ func TestUDP_replytimeout(t *testing.T) { defer fd.Close() n1, _ := ListenUDP(newkey(), "127.0.0.1:0") - defer n1.net.close() + defer n1.Close() n2 := n1.bumpOrAdd(randomID(n1.self.ID, 10), fd.LocalAddr().(*net.UDPAddr)) if err := n1.net.ping(n2); err != errTimeout { @@ -97,8 +97,8 @@ func TestUDP_findnodeMultiReply(t *testing.T) { n1, _ := ListenUDP(newkey(), "127.0.0.1:0") n2, _ := ListenUDP(newkey(), "127.0.0.1:0") udp2 := n2.net.(*udp) - defer n1.net.close() - defer n2.net.close() + defer n1.Close() + defer n2.Close() nodes := make([]*Node, bucketSize) for i := range nodes { -- cgit v1.2.3 From 5bdc1159433138d92ed6fefb253e3c6ed3a43995 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 5 Feb 2015 03:07:58 +0100 Subject: p2p: integrate p2p/discover Overview of changes: - ClientIdentity has been removed, use discover.NodeID - Server now requires a private key to be set (instead of public key) - Server performs the encryption handshake before launching Peer - Dial logic takes peers from discover table - Encryption handshake code has been cleaned up a bit - baseProtocol is gone because we don't exchange peers anymore - Some parts of baseProtocol have moved into Peer instead --- p2p/client_identity.go | 68 ------ p2p/client_identity_test.go | 35 --- p2p/crypto.go | 429 +++++++++++++++++------------------- p2p/crypto_test.go | 171 ++++----------- p2p/message.go | 117 +++++++++- p2p/message_test.go | 140 ++++++++---- p2p/peer.go | 518 ++++++++++++++++---------------------------- p2p/peer_error.go | 56 +++-- p2p/peer_test.go | 296 +++++++++++++------------ p2p/protocol.go | 248 --------------------- p2p/protocol_test.go | 167 -------------- p2p/server.go | 343 +++++++++++++++-------------- p2p/server_test.go | 85 ++++---- p2p/testlog_test.go | 2 +- p2p/testpoc7.go | 40 ---- 15 files changed, 1056 insertions(+), 1659 deletions(-) delete mode 100644 p2p/client_identity.go delete mode 100644 p2p/client_identity_test.go delete mode 100644 p2p/protocol_test.go delete mode 100644 p2p/testpoc7.go (limited to 'p2p') diff --git a/p2p/client_identity.go b/p2p/client_identity.go deleted file mode 100644 index ef01f1ad7..000000000 --- a/p2p/client_identity.go +++ /dev/null @@ -1,68 +0,0 @@ -package p2p - -import ( - "fmt" - "runtime" -) - -// ClientIdentity represents the identity of a peer. -type ClientIdentity interface { - String() string // human readable identity - Pubkey() []byte // 512-bit public key -} - -type SimpleClientIdentity struct { - clientIdentifier string - version string - customIdentifier string - os string - implementation string - privkey []byte - pubkey []byte -} - -func NewSimpleClientIdentity(clientIdentifier string, version string, customIdentifier string, pubkey []byte) *SimpleClientIdentity { - clientIdentity := &SimpleClientIdentity{ - clientIdentifier: clientIdentifier, - version: version, - customIdentifier: customIdentifier, - os: runtime.GOOS, - implementation: runtime.Version(), - pubkey: pubkey, - } - - return clientIdentity -} - -func (c *SimpleClientIdentity) init() { -} - -func (c *SimpleClientIdentity) String() string { - var id string - if len(c.customIdentifier) > 0 { - id = "/" + c.customIdentifier - } - - return fmt.Sprintf("%s/v%s%s/%s/%s", - c.clientIdentifier, - c.version, - id, - c.os, - c.implementation) -} - -func (c *SimpleClientIdentity) Privkey() []byte { - return c.privkey -} - -func (c *SimpleClientIdentity) Pubkey() []byte { - return c.pubkey -} - -func (c *SimpleClientIdentity) SetCustomIdentifier(customIdentifier string) { - c.customIdentifier = customIdentifier -} - -func (c *SimpleClientIdentity) GetCustomIdentifier() string { - return c.customIdentifier -} diff --git a/p2p/client_identity_test.go b/p2p/client_identity_test.go deleted file mode 100644 index 7b62f05ef..000000000 --- a/p2p/client_identity_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package p2p - -import ( - "bytes" - "fmt" - "runtime" - "testing" -) - -func TestClientIdentity(t *testing.T) { - clientIdentity := NewSimpleClientIdentity("Ethereum(G)", "0.5.16", "test", []byte("pubkey")) - key := clientIdentity.Pubkey() - if !bytes.Equal(key, []byte("pubkey")) { - t.Errorf("Expected Pubkey to be %x, got %x", key, []byte("pubkey")) - } - clientString := clientIdentity.String() - expected := fmt.Sprintf("Ethereum(G)/v0.5.16/test/%s/%s", runtime.GOOS, runtime.Version()) - if clientString != expected { - t.Errorf("Expected clientIdentity to be %v, got %v", expected, clientString) - } - customIdentifier := clientIdentity.GetCustomIdentifier() - if customIdentifier != "test" { - t.Errorf("Expected clientIdentity.GetCustomIdentifier() to be 'test', got %v", customIdentifier) - } - clientIdentity.SetCustomIdentifier("test2") - customIdentifier = clientIdentity.GetCustomIdentifier() - if customIdentifier != "test2" { - t.Errorf("Expected clientIdentity.GetCustomIdentifier() to be 'test2', got %v", customIdentifier) - } - clientString = clientIdentity.String() - expected = fmt.Sprintf("Ethereum(G)/v0.5.16/test2/%s/%s", runtime.GOOS, runtime.Version()) - if clientString != expected { - t.Errorf("Expected clientIdentity to be %v, got %v", expected, clientString) - } -} diff --git a/p2p/crypto.go b/p2p/crypto.go index cb0534cba..2692d708c 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -10,28 +10,25 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/secp256k1" ethlogger "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/p2p/discover" "github.com/obscuren/ecies" ) var clogger = ethlogger.NewLogger("CRYPTOID") const ( - sskLen int = 16 // ecies.MaxSharedKeyLength(pubKey) / 2 - sigLen int = 65 // elliptic S256 - pubLen int = 64 // 512 bit pubkey in uncompressed representation without format byte - shaLen int = 32 // hash length (for nonce etc) - msgLen int = 194 // sigLen + shaLen + pubLen + shaLen + 1 = 194 - resLen int = 97 // pubLen + shaLen + 1 - iHSLen int = 307 // size of the final ECIES payload sent as initiator's handshake - rHSLen int = 210 // size of the final ECIES payload sent as receiver's handshake -) + 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) -// secretRW implements a message read writer with encryption and authentication -// it is initialised by cryptoId.Run() after a successful crypto handshake -// aesSecret, macSecret, egressMac, ingress -type secretRW struct { - aesSecret, macSecret, egressMac, ingressMac []byte -} + authMsgLen = sigLen + shaLen + pubLen + shaLen + 1 + authRespLen = pubLen + shaLen + 1 + + eciesBytes = 65 + 16 + 32 + iHSLen = authMsgLen + eciesBytes // size of the final ECIES payload sent as initiator's handshake + rHSLen = authRespLen + eciesBytes // size of the final ECIES payload sent as receiver's handshake +) type hexkey []byte @@ -39,150 +36,73 @@ func (self hexkey) String() string { return fmt.Sprintf("(%d) %x", len(self), []byte(self)) } -var nonceF = func(b []byte) (n int, err error) { - return rand.Read(b) -} - -var step = 0 -var detnonceF = func(b []byte) (n int, err error) { - step++ - copy(b, crypto.Sha3([]byte("privacy"+string(step)))) - fmt.Printf("detkey %v: %v\n", step, hexkey(b)) - return +func encHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, dial *discover.Node) ( + remoteID discover.NodeID, + sessionToken []byte, + err error, +) { + if dial == nil { + var remotePubkey []byte + sessionToken, remotePubkey, err = inboundEncHandshake(conn, prv, nil) + copy(remoteID[:], remotePubkey) + } else { + remoteID = dial.ID + sessionToken, err = outboundEncHandshake(conn, prv, remoteID[:], nil) + } + return remoteID, sessionToken, err } -var keyF = func() (priv *ecdsa.PrivateKey, err error) { - priv, err = ecdsa.GenerateKey(crypto.S256(), rand.Reader) +// outboundEncHandshake negotiates a session token on conn. +// it should be called on the dialing side of the connection. +// +// privateKey is the local client's private key +// remotePublicKey is the remote peer's node ID +// sessionToken is the token from a previous session with this node. +func outboundEncHandshake(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, remotePublicKey []byte, sessionToken []byte) ( + newSessionToken []byte, + err error, +) { + auth, initNonce, randomPrivKey, err := authMsg(prvKey, remotePublicKey, sessionToken) if err != nil { - return + return nil, err } - return -} - -var detkeyF = func() (priv *ecdsa.PrivateKey, err error) { - s := make([]byte, 32) - detnonceF(s) - priv = crypto.ToECDSA(s) - return -} - -/* -NewSecureSession(connection, privateKey, remotePublicKey, sessionToken, initiator) is called when the peer connection starts to set up a secure session by performing a crypto handshake. - - connection is (a buffered) network connection. - - privateKey is the local client's private key (*ecdsa.PrivateKey) - - remotePublicKey is the remote peer's node Id ([]byte) - - sessionToken is the token from the previous session with this same peer. Nil if no token is found. - - initiator is a boolean flag. True if the node is the initiator of the connection (ie., remote is an outbound peer reached by dialing out). False if the connection was established by accepting a call from the remote peer via a listener. - - It returns a secretRW which implements the MsgReadWriter interface. -*/ -func NewSecureSession(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, remotePubKeyS []byte, sessionToken []byte, initiator bool) (token []byte, rw *secretRW, err error) { - var auth, initNonce, recNonce []byte - var read int - var randomPrivKey *ecdsa.PrivateKey - var remoteRandomPubKey *ecdsa.PublicKey - clogger.Debugf("attempting session with %v", hexkey(remotePubKeyS)) - if initiator { - if auth, initNonce, randomPrivKey, _, err = startHandshake(prvKey, remotePubKeyS, sessionToken); err != nil { - return - } - if sessionToken != nil { - clogger.Debugf("session-token: %v", hexkey(sessionToken)) - } - clogger.Debugf("initiator-nonce: %v", hexkey(initNonce)) - clogger.Debugf("initiator-random-private-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) - randomPublicKeyS, _ := ExportPublicKey(&randomPrivKey.PublicKey) - clogger.Debugf("initiator-random-public-key: %v", hexkey(randomPublicKeyS)) - - if _, err = conn.Write(auth); err != nil { - return - } - clogger.Debugf("initiator handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(auth)) - var response []byte = make([]byte, rHSLen) - if read, err = conn.Read(response); err != nil || read == 0 { - return - } - if read != rHSLen { - err = fmt.Errorf("remote receiver's handshake has invalid length. expect %v, got %v", rHSLen, read) - return - } - // write out auth message - // wait for response, then call complete - if recNonce, remoteRandomPubKey, _, err = completeHandshake(response, prvKey); err != nil { - return - } - clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) - remoteRandomPubKeyS, _ := ExportPublicKey(remoteRandomPubKey) - clogger.Debugf("receiver-random-public-key: %v", hexkey(remoteRandomPubKeyS)) - - } else { - auth = make([]byte, iHSLen) - clogger.Debugf("waiting for initiator handshake (from %v)", hexkey(remotePubKeyS)) - if read, err = conn.Read(auth); err != nil { - return - } - if read != iHSLen { - err = fmt.Errorf("remote initiator's handshake has invalid length. expect %v, got %v", iHSLen, read) - return - } - clogger.Debugf("received initiator handshake (from %v):\n%v", hexkey(remotePubKeyS), hexkey(auth)) - // we are listening connection. we are responders in the handshake. - // Extract info from the authentication. The initiator starts by sending us a handshake that we need to respond to. - // so we read auth message first, then respond - var response []byte - if response, recNonce, initNonce, randomPrivKey, remoteRandomPubKey, err = respondToHandshake(auth, prvKey, remotePubKeyS, sessionToken); err != nil { - return - } - clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) - clogger.Debugf("receiver-random-priv-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) - if _, err = conn.Write(response); err != nil { - return - } - clogger.Debugf("receiver handshake (sent to %v):\n%v", hexkey(remotePubKeyS), hexkey(response)) + if sessionToken != nil { + clogger.Debugf("session-token: %v", hexkey(sessionToken)) } - return newSession(initiator, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) -} -/* -ImportPublicKey creates a 512 bit *ecsda.PublicKey from a byte slice. It accepts the simple 64 byte uncompressed format or the 65 byte format given by calling elliptic.Marshal on the EC point represented by the key. Any other length will result in an invalid public key error. -*/ -func ImportPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err error) { - var pubKey65 []byte - switch len(pubKey) { - case 64: - pubKey65 = append([]byte{0x04}, pubKey...) - case 65: - pubKey65 = pubKey - default: - return nil, fmt.Errorf("invalid public key length %v (expect 64/65)", len(pubKey)) + clogger.Debugf("initiator-nonce: %v", hexkey(initNonce)) + clogger.Debugf("initiator-random-private-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) + randomPublicKeyS, _ := exportPublicKey(&randomPrivKey.PublicKey) + clogger.Debugf("initiator-random-public-key: %v", hexkey(randomPublicKeyS)) + if _, err = conn.Write(auth); err != nil { + return nil, err } - return crypto.ToECDSAPub(pubKey65), nil -} + clogger.Debugf("initiator handshake: %v", hexkey(auth)) -/* -ExportPublicKey exports a *ecdsa.PublicKey into a byte slice using a simple 64-byte format. and is used for simple serialisation in network communication -*/ -func ExportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { - if pubKeyEC == nil { - return nil, fmt.Errorf("no ECDSA public key given") + response := make([]byte, rHSLen) + if _, err = io.ReadFull(conn, response); err != nil { + return nil, err + } + recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prvKey) + if err != nil { + return nil, err } - return crypto.FromECDSAPub(pubKeyEC)[1:], nil -} - -/* startHandshake is called by if the node is the initiator of the connection. -The caller provides the public key of the peer as conjuctured from lookup based on IP:port, given as user input or proven by signatures. The caller must have access to persistant information about the peers, and pass the previous session token as an argument to cryptoId. + clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) + remoteRandomPubKeyS, _ := exportPublicKey(remoteRandomPubKey) + clogger.Debugf("receiver-random-public-key: %v", hexkey(remoteRandomPubKeyS)) + return newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) +} -The first return value is the auth message that is to be sent out to the remote receiver. -*/ -func startHandshake(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte) (auth []byte, initNonce []byte, randomPrvKey *ecdsa.PrivateKey, remotePubKey *ecdsa.PublicKey, err error) { +// authMsg creates the initiator handshake. +func authMsg(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte) ( + auth, initNonce []byte, + randomPrvKey *ecdsa.PrivateKey, + err error, +) { // session init, common to both parties - if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { + remotePubKey, err := importPublicKey(remotePubKeyS) + if err != nil { return } @@ -203,20 +123,18 @@ func startHandshake(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte //E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) // allocate msgLen long message, - var msg []byte = make([]byte, msgLen) - initNonce = msg[msgLen-shaLen-1 : msgLen-1] - fmt.Printf("init-nonce: ") - if _, err = nonceF(initNonce); err != nil { + var msg []byte = make([]byte, authMsgLen) + initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1] + if _, err = rand.Read(initNonce); err != nil { return } // create known message // ecdh-shared-secret^nonce for new peers // token^nonce for old peers - var sharedSecret = Xor(sessionToken, initNonce) + var sharedSecret = xor(sessionToken, initNonce) // generate random keypair to use for signing - fmt.Printf("init-random-ecdhe-private-key: ") - if randomPrvKey, err = keyF(); err != nil { + if randomPrvKey, err = crypto.GenerateKey(); err != nil { return } // sign shared secret (message known to both parties): shared-secret @@ -232,11 +150,11 @@ func startHandshake(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte copy(msg, signature) // copy signed-shared-secret // H(ecdhe-random-pubk) var randomPubKey64 []byte - if randomPubKey64, err = ExportPublicKey(&randomPrvKey.PublicKey); err != nil { + if randomPubKey64, err = exportPublicKey(&randomPrvKey.PublicKey); err != nil { return } var pubKey64 []byte - if pubKey64, err = ExportPublicKey(&prvKey.PublicKey); err != nil { + if pubKey64, err = exportPublicKey(&prvKey.PublicKey); err != nil { return } copy(msg[sigLen:sigLen+shaLen], crypto.Sha3(randomPubKey64)) @@ -244,36 +162,98 @@ func startHandshake(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte copy(msg[sigLen+shaLen:sigLen+shaLen+pubLen], pubKey64) // nonce is already in the slice // stick tokenFlag byte to the end - msg[msgLen-1] = tokenFlag + msg[authMsgLen-1] = tokenFlag // encrypt using remote-pubk // auth = eciesEncrypt(remote-pubk, msg) - if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { return } - return } -/* -respondToHandshake is called by peer if it accepted (but not initiated) the connection from the remote. It is passed the initiator handshake received, the public key and session token belonging to the remote initiator. - -The first return value is the authentication response (aka receiver handshake) that is to be sent to the remote initiator. -*/ -func respondToHandshake(auth []byte, prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte) (authResp []byte, respNonce []byte, initNonce []byte, randomPrivKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey, err error) { +// completeHandshake is called when the initiator receives an +// authentication response (aka receiver handshake). It completes the +// handshake by reading off parameters the remote peer provides needed +// to set up the secure session. +func completeHandshake(auth []byte, prvKey *ecdsa.PrivateKey) ( + respNonce []byte, + remoteRandomPubKey *ecdsa.PublicKey, + tokenFlag bool, + err error, +) { var msg []byte - var remotePubKey *ecdsa.PublicKey - if remotePubKey, err = ImportPublicKey(remotePubKeyS); err != nil { + // they prove that msg is meant for me, + // I prove I possess private key if i can read it + if msg, err = crypto.Decrypt(prvKey, auth); err != nil { return } + respNonce = msg[pubLen : pubLen+shaLen] + var remoteRandomPubKeyS = msg[:pubLen] + if remoteRandomPubKey, err = importPublicKey(remoteRandomPubKeyS); err != nil { + return + } + if msg[authRespLen-1] == 0x01 { + tokenFlag = true + } + return +} + +// inboundEncHandshake negotiates a session token on conn. +// it should be called on the listening side of the connection. +// +// privateKey is the local client's private key +// sessionToken is the token from a previous session with this node. +func inboundEncHandshake(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, sessionToken []byte) ( + token, remotePubKey []byte, + err error, +) { + // we are listening connection. we are responders in the + // handshake. Extract info from the authentication. The initiator + // starts by sending us a handshake that we need to respond to. so + // we read auth message first, then respond. + auth := make([]byte, iHSLen) + if _, err := io.ReadFull(conn, auth); err != nil { + return nil, nil, err + } + response, recNonce, initNonce, remotePubKey, randomPrivKey, remoteRandomPubKey, err := authResp(auth, sessionToken, prvKey) + if err != nil { + return nil, nil, err + } + clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) + clogger.Debugf("receiver-random-priv-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) + if _, err = conn.Write(response); err != nil { + return nil, nil, err + } + clogger.Debugf("receiver handshake:\n%v", hexkey(response)) + token, err = newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) + return token, remotePubKey, err +} + +// authResp is called by peer if it accepted (but not +// initiated) the connection from the remote. It is passed the initiator +// handshake received and the session token belonging to the +// remote initiator. +// +// The first return value is the authentication response (aka receiver +// handshake) that is to be sent to the remote initiator. +func authResp(auth, sessionToken []byte, prvKey *ecdsa.PrivateKey) ( + authResp, respNonce, initNonce, remotePubKeyS []byte, + randomPrivKey *ecdsa.PrivateKey, + remoteRandomPubKey *ecdsa.PublicKey, + err error, +) { // they prove that msg is meant for me, // I prove I possess private key if i can read it - if msg, err = crypto.Decrypt(prvKey, auth); err != nil { + msg, err := crypto.Decrypt(prvKey, auth) + if err != nil { return } + remotePubKeyS = msg[sigLen+shaLen : sigLen+shaLen+pubLen] + remotePubKey, _ := importPublicKey(remotePubKeyS) + var tokenFlag byte if sessionToken == nil { // no session token found means we need to generate shared secret. @@ -289,42 +269,42 @@ func respondToHandshake(auth []byte, prvKey *ecdsa.PrivateKey, remotePubKeyS, se } // the initiator nonce is read off the end of the message - initNonce = msg[msgLen-shaLen-1 : msgLen-1] - // I prove that i own prv key (to derive shared secret, and read nonce off encrypted msg) and that I own shared secret - // they prove they own the private key belonging to ecdhe-random-pubk - // we can now reconstruct the signed message and recover the peers pubkey - var signedMsg = Xor(sessionToken, initNonce) + initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1] + // I prove that i own prv key (to derive shared secret, and read + // nonce off encrypted msg) and that I own shared secret they + // prove they own the private key belonging to ecdhe-random-pubk + // we can now reconstruct the signed message and recover the peers + // pubkey + var signedMsg = xor(sessionToken, initNonce) var remoteRandomPubKeyS []byte if remoteRandomPubKeyS, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { return } // convert to ECDSA standard - if remoteRandomPubKey, err = ImportPublicKey(remoteRandomPubKeyS); err != nil { + if remoteRandomPubKey, err = importPublicKey(remoteRandomPubKeyS); err != nil { return } // now we find ourselves a long task too, fill it random - var resp = make([]byte, resLen) + var resp = make([]byte, authRespLen) // generate shaLen long nonce respNonce = resp[pubLen : pubLen+shaLen] - fmt.Printf("rec-nonce: ") - if _, err = nonceF(respNonce); err != nil { + if _, err = rand.Read(respNonce); err != nil { return } // generate random keypair for session - fmt.Printf("rec-random-ecdhe-private-key: ") - if randomPrivKey, err = keyF(); err != nil { + if randomPrivKey, err = crypto.GenerateKey(); err != nil { return } // responder auth message // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) var randomPubKeyS []byte - if randomPubKeyS, err = ExportPublicKey(&randomPrivKey.PublicKey); err != nil { + if randomPubKeyS, err = exportPublicKey(&randomPrivKey.PublicKey); err != nil { return } copy(resp[:pubLen], randomPubKeyS) // nonce is already in the slice - resp[resLen-1] = tokenFlag + resp[authRespLen-1] = tokenFlag // encrypt using remote-pubk // auth = eciesEncrypt(remote-pubk, msg) @@ -335,70 +315,49 @@ func respondToHandshake(auth []byte, prvKey *ecdsa.PrivateKey, remotePubKeyS, se return } -/* -completeHandshake is called when the initiator receives an authentication response (aka receiver handshake). It completes the handshake by reading off parameters the remote peer provides needed to set up the secure session -*/ -func completeHandshake(auth []byte, prvKey *ecdsa.PrivateKey) (respNonce []byte, remoteRandomPubKey *ecdsa.PublicKey, tokenFlag bool, err error) { - var msg []byte - // they prove that msg is meant for me, - // I prove I possess private key if i can read it - if msg, err = crypto.Decrypt(prvKey, auth); err != nil { - return +// newSession is called after the handshake is completed. The +// arguments are values negotiated in the handshake. The return value +// is a new session Token to be remembered for the next time we +// connect with this peer. +func newSession(initNonce, respNonce []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) ([]byte, error) { + // 3) Now we can trust ecdhe-random-pubk to derive new keys + //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) + pubKey := ecies.ImportECDSAPublic(remoteRandomPubKey) + dhSharedSecret, err := ecies.ImportECDSA(privKey).GenerateShared(pubKey, sskLen, sskLen) + if err != nil { + return nil, err } + sharedSecret := crypto.Sha3(dhSharedSecret, crypto.Sha3(respNonce, initNonce)) + sessionToken := crypto.Sha3(sharedSecret) + return sessionToken, nil +} - respNonce = msg[pubLen : pubLen+shaLen] - var remoteRandomPubKeyS = msg[:pubLen] - if remoteRandomPubKey, err = ImportPublicKey(remoteRandomPubKeyS); err != nil { - return - } - if msg[resLen-1] == 0x01 { - tokenFlag = true +// importPublicKey unmarshals 512 bit public keys. +func importPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err 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)) } - return + return crypto.ToECDSAPub(pubKey65), nil } -/* -newSession is called after the handshake is completed. The arguments are values negotiated in the handshake and the return value is a new session : a new session Token to be remembered for the next time we connect with this peer. And a MsgReadWriter that implements an encrypted and authenticated connection with key material obtained from the crypto handshake key exchange -*/ -func newSession(initiator bool, initNonce, respNonce, auth []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) (sessionToken []byte, rw *secretRW, err error) { - // 3) Now we can trust ecdhe-random-pubk to derive new keys - //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) - var dhSharedSecret []byte - pubKey := ecies.ImportECDSAPublic(remoteRandomPubKey) - if dhSharedSecret, err = ecies.ImportECDSA(privKey).GenerateShared(pubKey, sskLen, sskLen); err != nil { - return +func exportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { + if pubKeyEC == nil { + return nil, fmt.Errorf("no ECDSA public key given") } - var sharedSecret = crypto.Sha3(append(dhSharedSecret, crypto.Sha3(append(respNonce, initNonce...))...)) - sessionToken = crypto.Sha3(sharedSecret) - var aesSecret = crypto.Sha3(append(dhSharedSecret, sharedSecret...)) - var macSecret = crypto.Sha3(append(dhSharedSecret, aesSecret...)) - var egressMac, ingressMac []byte - if initiator { - egressMac = Xor(macSecret, respNonce) - ingressMac = Xor(macSecret, initNonce) - } else { - egressMac = Xor(macSecret, initNonce) - ingressMac = Xor(macSecret, respNonce) - } - rw = &secretRW{ - aesSecret: aesSecret, - macSecret: macSecret, - egressMac: egressMac, - ingressMac: ingressMac, - } - clogger.Debugf("aes-secret: %v", hexkey(aesSecret)) - clogger.Debugf("mac-secret: %v", hexkey(macSecret)) - clogger.Debugf("egress-mac: %v", hexkey(egressMac)) - clogger.Debugf("ingress-mac: %v", hexkey(ingressMac)) - return + return crypto.FromECDSAPub(pubKeyEC)[1:], nil } -// TODO: optimisation -// should use cipher.xorBytes from crypto/cipher/xor.go for fast xor -func Xor(one, other []byte) (xor []byte) { +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 + return xor } diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index a4bf14ba6..0a9d49f96 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -3,10 +3,9 @@ package p2p import ( "bytes" "crypto/ecdsa" - "fmt" + "crypto/rand" "net" "testing" - "time" "github.com/ethereum/go-ethereum/crypto" "github.com/obscuren/ecies" @@ -16,7 +15,7 @@ func TestPublicKeyEncoding(t *testing.T) { prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) pub0 := &prv0.PublicKey pub0s := crypto.FromECDSAPub(pub0) - pub1, err := ImportPublicKey(pub0s) + pub1, err := importPublicKey(pub0s) if err != nil { t.Errorf("%v", err) } @@ -24,18 +23,18 @@ func TestPublicKeyEncoding(t *testing.T) { if eciesPub1 == nil { t.Errorf("invalid ecdsa public key") } - pub1s, err := ExportPublicKey(pub1) + pub1s, err := exportPublicKey(pub1) if err != nil { t.Errorf("%v", err) } if len(pub1s) != 64 { t.Errorf("wrong length expect 64, got", len(pub1s)) } - pub2, err := ImportPublicKey(pub1s) + pub2, err := importPublicKey(pub1s) if err != nil { t.Errorf("%v", err) } - pub2s, err := ExportPublicKey(pub2) + pub2s, err := exportPublicKey(pub2) if err != nil { t.Errorf("%v", err) } @@ -69,95 +68,53 @@ func TestSharedSecret(t *testing.T) { } func TestCryptoHandshake(t *testing.T) { - testCryptoHandshakeWithGen(false, t) + testCryptoHandshake(newkey(), newkey(), nil, t) } -func TestTokenCryptoHandshake(t *testing.T) { - testCryptoHandshakeWithGen(true, t) -} - -func TestDetCryptoHandshake(t *testing.T) { - defer testlog(t).detach() - tmpkeyF := keyF - keyF = detkeyF - tmpnonceF := nonceF - nonceF = detnonceF - testCryptoHandshakeWithGen(false, t) - keyF = tmpkeyF - nonceF = tmpnonceF -} - -func TestDetTokenCryptoHandshake(t *testing.T) { - defer testlog(t).detach() - tmpkeyF := keyF - keyF = detkeyF - tmpnonceF := nonceF - nonceF = detnonceF - testCryptoHandshakeWithGen(true, t) - keyF = tmpkeyF - nonceF = tmpnonceF -} - -func testCryptoHandshakeWithGen(token bool, t *testing.T) { - fmt.Printf("init-private-key: ") - prv0, err := keyF() - if err != nil { - t.Errorf("%v", err) - return - } - fmt.Printf("rec-private-key: ") - prv1, err := keyF() - if err != nil { - t.Errorf("%v", err) - return - } - var nonce []byte - if token { - fmt.Printf("session-token: ") - nonce = make([]byte, shaLen) - nonceF(nonce) - } - testCryptoHandshake(prv0, prv1, nonce, t) +func TestCryptoHandshakeWithToken(t *testing.T) { + sessionToken := make([]byte, shaLen) + rand.Read(sessionToken) + testCryptoHandshake(newkey(), newkey(), sessionToken, t) } func testCryptoHandshake(prv0, prv1 *ecdsa.PrivateKey, sessionToken []byte, t *testing.T) { var err error - pub0 := &prv0.PublicKey + // pub0 := &prv0.PublicKey pub1 := &prv1.PublicKey - pub0s := crypto.FromECDSAPub(pub0) + // pub0s := crypto.FromECDSAPub(pub0) pub1s := crypto.FromECDSAPub(pub1) // simulate handshake by feeding output to input // initiator sends handshake 'auth' - auth, initNonce, randomPrivKey, _, err := startHandshake(prv0, pub1s, sessionToken) + auth, initNonce, randomPrivKey, err := authMsg(prv0, pub1s, sessionToken) if err != nil { t.Errorf("%v", err) } - fmt.Printf("-> %v\n", hexkey(auth)) + t.Logf("-> %v", hexkey(auth)) // receiver reads auth and responds with response - response, remoteRecNonce, remoteInitNonce, remoteRandomPrivKey, remoteInitRandomPubKey, err := respondToHandshake(auth, prv1, pub0s, sessionToken) + response, remoteRecNonce, remoteInitNonce, _, remoteRandomPrivKey, remoteInitRandomPubKey, err := authResp(auth, sessionToken, prv1) if err != nil { t.Errorf("%v", err) } - fmt.Printf("<- %v\n", hexkey(response)) + t.Logf("<- %v\n", hexkey(response)) // initiator reads receiver's response and the key exchange completes recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prv0) if err != nil { - t.Errorf("%v", err) + t.Errorf("completeHandshake error: %v", err) } // now both parties should have the same session parameters - initSessionToken, initSecretRW, err := newSession(true, initNonce, recNonce, auth, randomPrivKey, remoteRandomPubKey) + initSessionToken, err := newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) if err != nil { - t.Errorf("%v", err) + t.Errorf("newSession error: %v", err) } - recSessionToken, recSecretRW, err := newSession(false, remoteInitNonce, remoteRecNonce, auth, remoteRandomPrivKey, remoteInitRandomPubKey) + recSessionToken, err := newSession(remoteInitNonce, remoteRecNonce, remoteRandomPrivKey, remoteInitRandomPubKey) if err != nil { - t.Errorf("%v", err) + t.Errorf("newSession error: %v", err) } // fmt.Printf("\nauth (%v) %x\n\nresp (%v) %x\n\n", len(auth), auth, len(response), response) @@ -173,76 +130,38 @@ func testCryptoHandshake(prv0, prv1 *ecdsa.PrivateKey, sessionToken []byte, t *t if !bytes.Equal(initSessionToken, recSessionToken) { t.Errorf("session tokens do not match") } - // aesSecret, macSecret, egressMac, ingressMac - if !bytes.Equal(initSecretRW.aesSecret, recSecretRW.aesSecret) { - t.Errorf("AES secrets do not match") - } - if !bytes.Equal(initSecretRW.macSecret, recSecretRW.macSecret) { - t.Errorf("macSecrets do not match") - } - if !bytes.Equal(initSecretRW.egressMac, recSecretRW.ingressMac) { - t.Errorf("initiator's egressMac do not match receiver's ingressMac") - } - if !bytes.Equal(initSecretRW.ingressMac, recSecretRW.egressMac) { - t.Errorf("initiator's inressMac do not match receiver's egressMac") - } - } -func TestPeersHandshake(t *testing.T) { +func TestHandshake(t *testing.T) { defer testlog(t).detach() - var err error - // var sessionToken []byte - prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) - pub0 := &prv0.PublicKey - prv1, _ := crypto.GenerateKey() - pub1 := &prv1.PublicKey - - prv0s := crypto.FromECDSA(prv0) - pub0s := crypto.FromECDSAPub(pub0) - prv1s := crypto.FromECDSA(prv1) - pub1s := crypto.FromECDSAPub(pub1) - - conn1, conn2 := net.Pipe() - initiator := newPeer(conn1, []Protocol{}, nil) - receiver := newPeer(conn2, []Protocol{}, nil) - initiator.dialAddr = &peerAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222, Pubkey: pub1s[1:]} - initiator.privateKey = prv0s - // this is cheating. identity of initiator/dialler not available to listener/receiver - // its public key should be looked up based on IP address - receiver.identity = &peerId{nil, pub0s} - receiver.privateKey = prv1s - - initiator.pubkeyHook = func(*peerAddr) error { return nil } - receiver.pubkeyHook = func(*peerAddr) error { return nil } + prv0, _ := crypto.GenerateKey() + prv1, _ := crypto.GenerateKey() + pub0s, _ := exportPublicKey(&prv0.PublicKey) + pub1s, _ := exportPublicKey(&prv1.PublicKey) + rw0, rw1 := net.Pipe() + tokens := make(chan []byte) - initiator.cryptoHandshake = true - receiver.cryptoHandshake = true - errc0 := make(chan error, 1) - errc1 := make(chan error, 1) go func() { - _, err := initiator.loop() - errc0 <- err + token, err := outboundEncHandshake(rw0, prv0, pub1s, nil) + if err != nil { + t.Errorf("outbound side error: %v", err) + } + tokens <- token }() go func() { - _, err := receiver.loop() - errc1 <- err + token, remotePubkey, err := inboundEncHandshake(rw1, prv1, nil) + if err != nil { + t.Errorf("inbound side error: %v", err) + } + if !bytes.Equal(remotePubkey, pub0s) { + t.Errorf("inbound side returned wrong remote pubkey\n got: %x\n want: %x", remotePubkey, pub0s) + } + tokens <- token }() - ready := make(chan bool) - go func() { - <-initiator.cryptoReady - <-receiver.cryptoReady - close(ready) - }() - timeout := time.After(10 * time.Second) - select { - case <-ready: - case <-timeout: - t.Errorf("crypto handshake hanging for too long") - case err = <-errc0: - t.Errorf("peer 0 quit with error: %v", err) - case err = <-errc1: - t.Errorf("peer 1 quit with error: %v", err) + + t1, t2 := <-tokens, <-tokens + if !bytes.Equal(t1, t2) { + t.Error("session token mismatch") } } diff --git a/p2p/message.go b/p2p/message.go index daf2bf05c..6521d09c2 100644 --- a/p2p/message.go +++ b/p2p/message.go @@ -1,6 +1,7 @@ package p2p import ( + "bufio" "bytes" "encoding/binary" "errors" @@ -8,7 +9,10 @@ import ( "io" "io/ioutil" "math/big" + "net" + "sync" "sync/atomic" + "time" "github.com/ethereum/go-ethereum/ethutil" "github.com/ethereum/go-ethereum/rlp" @@ -74,11 +78,14 @@ type MsgWriter interface { // WriteMsg sends a message. It will block until the message's // Payload has been consumed by the other end. // - // Note that messages can be sent only once. + // Note that messages can be sent only once because their + // payload reader is drained. WriteMsg(Msg) error } // MsgReadWriter provides reading and writing of encoded messages. +// Implementations should ensure that ReadMsg and WriteMsg can be +// called simultaneously from multiple goroutines. type MsgReadWriter interface { MsgReader MsgWriter @@ -90,8 +97,45 @@ func EncodeMsg(w MsgWriter, code uint64, data ...interface{}) error { return w.WriteMsg(NewMsg(code, data...)) } +// frameRW is a MsgReadWriter that reads and writes devp2p message frames. +// As required by the interface, ReadMsg and WriteMsg can be called from +// multiple goroutines. +type frameRW struct { + net.Conn // make Conn methods available. be careful. + bufconn *bufio.ReadWriter + + // this channel is used to 'lend' bufconn to a caller of ReadMsg + // until the message payload has been consumed. the channel + // receives a value when EOF is reached on the payload, unblocking + // a pending call to ReadMsg. + rsync chan struct{} + + // this mutex guards writes to bufconn. + writeMu sync.Mutex +} + +func newFrameRW(conn net.Conn, timeout time.Duration) *frameRW { + rsync := make(chan struct{}, 1) + rsync <- struct{}{} + return &frameRW{ + Conn: conn, + bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), + rsync: rsync, + } +} + var magicToken = []byte{34, 64, 8, 145} +func (rw *frameRW) WriteMsg(msg Msg) error { + rw.writeMu.Lock() + defer rw.writeMu.Unlock() + rw.SetWriteDeadline(time.Now().Add(msgWriteTimeout)) + if err := writeMsg(rw.bufconn, msg); err != nil { + return err + } + return rw.bufconn.Flush() +} + func writeMsg(w io.Writer, msg Msg) error { // TODO: handle case when Size + len(code) + len(listhdr) overflows uint32 code := ethutil.Encode(uint32(msg.Code)) @@ -120,12 +164,16 @@ func makeListHeader(length uint32) []byte { return append([]byte{lenb}, enc...) } -// readMsg reads a message header from r. -// It takes an rlp.ByteReader to ensure that the decoding doesn't buffer. -func readMsg(r rlp.ByteReader) (msg Msg, err error) { +func (rw *frameRW) ReadMsg() (msg Msg, err error) { + <-rw.rsync // wait until bufconn is ours + + // this read timeout applies also to the payload. + // TODO: proper read timeout + rw.SetReadDeadline(time.Now().Add(msgReadTimeout)) + // read magic and payload size start := make([]byte, 8) - if _, err = io.ReadFull(r, start); err != nil { + if _, err = io.ReadFull(rw.bufconn, start); err != nil { return msg, newPeerError(errRead, "%v", err) } if !bytes.HasPrefix(start, magicToken) { @@ -134,17 +182,33 @@ func readMsg(r rlp.ByteReader) (msg Msg, err error) { size := binary.BigEndian.Uint32(start[4:]) // decode start of RLP message to get the message code - posr := &postrack{r, 0} + posr := &postrack{rw.bufconn, 0} s := rlp.NewStream(posr) if _, err := s.List(); err != nil { return msg, err } - code, err := s.Uint() + msg.Code, err = s.Uint() if err != nil { return msg, err } - payloadsize := size - posr.p - return Msg{code, payloadsize, io.LimitReader(r, int64(payloadsize))}, nil + msg.Size = size - posr.p + + if msg.Size <= wholePayloadSize { + // msg is small, read all of it and move on to the next message. + pbuf := make([]byte, msg.Size) + if _, err := io.ReadFull(rw.bufconn, pbuf); err != nil { + return msg, err + } + rw.rsync <- struct{}{} // bufconn is available again + msg.Payload = bytes.NewReader(pbuf) + } else { + // lend bufconn to the caller until it has + // consumed the payload. eofSignal will send a value + // on rw.rsync when EOF is reached. + pr := &eofSignal{rw.bufconn, msg.Size, rw.rsync} + msg.Payload = pr + } + return msg, nil } // postrack wraps an rlp.ByteReader with a position counter. @@ -167,6 +231,39 @@ func (r *postrack) ReadByte() (byte, error) { return b, err } +// 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. +type eofSignal struct { + wrapped io.Reader + count uint32 // number of bytes left + eof chan<- struct{} +} + +// note: when using eofSignal to detect whether a message payload +// has been read, Read might not be called for zero sized messages. +func (r *eofSignal) Read(buf []byte) (int, error) { + if r.count == 0 { + if r.eof != nil { + r.eof <- struct{}{} + r.eof = nil + } + return 0, io.EOF + } + + max := len(buf) + if int(r.count) < len(buf) { + max = int(r.count) + } + n, err := r.wrapped.Read(buf[:max]) + r.count -= uint32(n) + if (err != nil || r.count == 0) && r.eof != nil { + r.eof <- struct{}{} // tell Peer that msg has been consumed + r.eof = nil + } + return n, err +} + // MsgPipe creates a message pipe. Reads on one end are matched // with writes on the other. The pipe is full-duplex, both ends // implement MsgReadWriter. @@ -198,7 +295,7 @@ type MsgPipeRW struct { func (p *MsgPipeRW) WriteMsg(msg Msg) error { if atomic.LoadInt32(p.closed) == 0 { consumed := make(chan struct{}, 1) - msg.Payload = &eofSignal{msg.Payload, int64(msg.Size), consumed} + msg.Payload = &eofSignal{msg.Payload, msg.Size, consumed} select { case p.w <- msg: if msg.Size > 0 { diff --git a/p2p/message_test.go b/p2p/message_test.go index 5cde9abf5..4b94ebb5f 100644 --- a/p2p/message_test.go +++ b/p2p/message_test.go @@ -3,12 +3,11 @@ package p2p import ( "bytes" "fmt" + "io" "io/ioutil" "runtime" "testing" "time" - - "github.com/ethereum/go-ethereum/ethutil" ) func TestNewMsg(t *testing.T) { @@ -26,51 +25,51 @@ func TestNewMsg(t *testing.T) { } } -func TestEncodeDecodeMsg(t *testing.T) { - msg := NewMsg(3, 1, "000") - buf := new(bytes.Buffer) - if err := writeMsg(buf, msg); err != nil { - t.Fatalf("encodeMsg error: %v", err) - } - // t.Logf("encoded: %x", buf.Bytes()) +// func TestEncodeDecodeMsg(t *testing.T) { +// msg := NewMsg(3, 1, "000") +// buf := new(bytes.Buffer) +// if err := writeMsg(buf, msg); err != nil { +// t.Fatalf("encodeMsg error: %v", err) +// } +// // t.Logf("encoded: %x", buf.Bytes()) - decmsg, err := readMsg(buf) - if err != nil { - t.Fatalf("readMsg error: %v", err) - } - if decmsg.Code != 3 { - t.Errorf("incorrect code %d, want %d", decmsg.Code, 3) - } - if decmsg.Size != 5 { - t.Errorf("incorrect size %d, want %d", decmsg.Size, 5) - } +// decmsg, err := readMsg(buf) +// if err != nil { +// t.Fatalf("readMsg error: %v", err) +// } +// if decmsg.Code != 3 { +// t.Errorf("incorrect code %d, want %d", decmsg.Code, 3) +// } +// if decmsg.Size != 5 { +// t.Errorf("incorrect size %d, want %d", decmsg.Size, 5) +// } - var data struct { - I uint - S string - } - if err := decmsg.Decode(&data); err != nil { - t.Fatalf("Decode error: %v", err) - } - if data.I != 1 { - t.Errorf("incorrect data.I: got %v, expected %d", data.I, 1) - } - if data.S != "000" { - t.Errorf("incorrect data.S: got %q, expected %q", data.S, "000") - } -} +// var data struct { +// I uint +// S string +// } +// if err := decmsg.Decode(&data); err != nil { +// t.Fatalf("Decode error: %v", err) +// } +// if data.I != 1 { +// t.Errorf("incorrect data.I: got %v, expected %d", data.I, 1) +// } +// if data.S != "000" { +// t.Errorf("incorrect data.S: got %q, expected %q", data.S, "000") +// } +// } -func TestDecodeRealMsg(t *testing.T) { - data := ethutil.Hex2Bytes("2240089100000080f87e8002b5457468657265756d282b2b292f5065657220536572766572204f6e652f76302e372e382f52656c656173652f4c696e75782f672b2bc082765fb84086dd80b7aefd6a6d2e3b93f4f300a86bfb6ef7bdc97cb03f793db6bb") - msg, err := readMsg(bytes.NewReader(data)) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } +// func TestDecodeRealMsg(t *testing.T) { +// data := ethutil.Hex2Bytes("2240089100000080f87e8002b5457468657265756d282b2b292f5065657220536572766572204f6e652f76302e372e382f52656c656173652f4c696e75782f672b2bc082765fb84086dd80b7aefd6a6d2e3b93f4f300a86bfb6ef7bdc97cb03f793db6bb") +// msg, err := readMsg(bytes.NewReader(data)) +// if err != nil { +// t.Fatalf("unexpected error: %v", err) +// } - if msg.Code != 0 { - t.Errorf("incorrect code %d, want %d", msg.Code, 0) - } -} +// if msg.Code != 0 { +// t.Errorf("incorrect code %d, want %d", msg.Code, 0) +// } +// } func ExampleMsgPipe() { rw1, rw2 := MsgPipe() @@ -131,3 +130,58 @@ func TestMsgPipeConcurrentClose(t *testing.T) { go rw1.Close() } } + +func TestEOFSignal(t *testing.T) { + rb := make([]byte, 10) + + // empty reader + eof := make(chan struct{}, 1) + sig := &eofSignal{new(bytes.Buffer), 0, eof} + if n, err := sig.Read(rb); n != 0 || err != io.EOF { + t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + } + select { + case <-eof: + default: + t.Error("EOF chan not signaled") + } + + // count before error + eof = make(chan struct{}, 1) + sig = &eofSignal{bytes.NewBufferString("aaaaaaaa"), 4, eof} + if n, err := sig.Read(rb); n != 4 || err != nil { + t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + } + select { + case <-eof: + default: + t.Error("EOF chan not signaled") + } + + // error before count + eof = make(chan struct{}, 1) + sig = &eofSignal{bytes.NewBufferString("aaaa"), 999, eof} + if n, err := sig.Read(rb); n != 4 || err != nil { + t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + } + if n, err := sig.Read(rb); n != 0 || err != io.EOF { + t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + } + select { + case <-eof: + default: + t.Error("EOF chan not signaled") + } + + // no signal if neither occurs + eof = make(chan struct{}, 1) + sig = &eofSignal{bytes.NewBufferString("aaaaaaaaaaaaaaaaaaaaa"), 999, eof} + if n, err := sig.Read(rb); n != 10 || err != nil { + t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + } + select { + case <-eof: + t.Error("unexpected EOF signal") + default: + } +} diff --git a/p2p/peer.go b/p2p/peer.go index e82bca222..1fa8264a3 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -1,10 +1,6 @@ package p2p import ( - "bufio" - "bytes" - "crypto/ecdsa" - "crypto/rand" "fmt" "io" "io/ioutil" @@ -13,179 +9,118 @@ import ( "sync" "time" - "github.com/ethereum/go-ethereum/crypto" - - "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/rlp" ) -// peerAddr is the structure of a peer list element. -// It is also a valid net.Addr. -type peerAddr struct { - IP net.IP - Port uint64 - Pubkey []byte // optional -} +const ( + // maximum amount of time allowed for reading a message + msgReadTimeout = 5 * time.Second + // maximum amount of time allowed for writing a message + msgWriteTimeout = 5 * time.Second + // messages smaller than this many bytes will be read at + // once before passing them to a protocol. + wholePayloadSize = 64 * 1024 -func newPeerAddr(addr net.Addr, pubkey []byte) *peerAddr { - n := addr.Network() - if n != "tcp" && n != "tcp4" && n != "tcp6" { - // for testing with non-TCP - return &peerAddr{net.ParseIP("127.0.0.1"), 30303, pubkey} - } - ta := addr.(*net.TCPAddr) - return &peerAddr{ta.IP, uint64(ta.Port), pubkey} -} + disconnectGracePeriod = 2 * time.Second +) -func (d peerAddr) Network() string { - if d.IP.To4() != nil { - return "tcp4" - } else { - return "tcp6" - } -} +const ( + baseProtocolVersion = 2 + baseProtocolLength = uint64(16) + baseProtocolMaxMsgSize = 10 * 1024 * 1024 +) -func (d peerAddr) String() string { - return fmt.Sprintf("%v:%d", d.IP, d.Port) -} +const ( + // devp2p message codes + handshakeMsg = 0x00 + discMsg = 0x01 + pingMsg = 0x02 + pongMsg = 0x03 + getPeersMsg = 0x04 + peersMsg = 0x05 +) -func (d *peerAddr) RlpData() interface{} { - return []interface{}{string(d.IP), d.Port, d.Pubkey} +// handshake is the RLP structure of the protocol handshake. +type handshake struct { + Version uint64 + Name string + Caps []Cap + ListenPort uint64 + NodeID discover.NodeID } -// Peer represents a remote peer. +// Peer represents a connected remote node. type Peer struct { // Peers have all the log methods. // Use them to display messages related to the peer. *logger.Logger - infolock sync.Mutex - identity ClientIdentity - caps []Cap - listenAddr *peerAddr // what remote peer is listening on - dialAddr *peerAddr // non-nil if dialing + infoMu sync.Mutex + name string + caps []Cap - // The mutex protects the connection - // so only one protocol can write at a time. - writeMu sync.Mutex - conn net.Conn - bufconn *bufio.ReadWriter + ourID, remoteID *discover.NodeID + ourName string + + rw *frameRW // These fields maintain the running protocols. - protocols []Protocol - runBaseProtocol bool // for testing - cryptoHandshake bool // for testing - cryptoReady chan struct{} - privateKey []byte + protocols []Protocol + runlock sync.RWMutex // protects running + running map[string]*proto - runlock sync.RWMutex // protects running - running map[string]*proto + protocolHandshakeEnabled bool protoWG sync.WaitGroup protoErr chan error closed chan struct{} disc chan DiscReason - - activity event.TypeMux // for activity events - - slot int // index into Server peer list - - // These fields are kept so base protocol can access them. - // TODO: this should be one or more interfaces - ourID ClientIdentity // client id of the Server - ourListenAddr *peerAddr // listen addr of Server, nil if not listening - newPeerAddr chan<- *peerAddr // tell server about received peers - otherPeers func() []*Peer // should return the list of all peers - pubkeyHook func(*peerAddr) error // called at end of handshake to validate pubkey } // NewPeer returns a peer for testing purposes. -func NewPeer(id ClientIdentity, caps []Cap) *Peer { +func NewPeer(id discover.NodeID, name string, caps []Cap) *Peer { conn, _ := net.Pipe() - peer := newPeer(conn, nil, nil) - peer.setHandshakeInfo(id, nil, caps) - close(peer.closed) + peer := newPeer(conn, nil, "", nil, &id) + peer.setHandshakeInfo(name, caps) + close(peer.closed) // ensures Disconnect doesn't block return peer } -func newServerPeer(server *Server, conn net.Conn, dialAddr *peerAddr) *Peer { - p := newPeer(conn, server.Protocols, dialAddr) - p.ourID = server.Identity - p.newPeerAddr = server.peerConnect - p.otherPeers = server.Peers - p.pubkeyHook = server.verifyPeer - p.runBaseProtocol = true - - // laddr can be updated concurrently by NAT traversal. - // newServerPeer must be called with the server lock held. - if server.laddr != nil { - p.ourListenAddr = newPeerAddr(server.laddr, server.Identity.Pubkey()) - } - return p -} - -func newPeer(conn net.Conn, protocols []Protocol, dialAddr *peerAddr) *Peer { - p := &Peer{ - Logger: logger.NewLogger("P2P " + conn.RemoteAddr().String()), - conn: conn, - dialAddr: dialAddr, - bufconn: bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), - protocols: protocols, - running: make(map[string]*proto), - disc: make(chan DiscReason), - protoErr: make(chan error), - closed: make(chan struct{}), - cryptoReady: make(chan struct{}), - } - return p +// ID returns the node's public key. +func (p *Peer) ID() discover.NodeID { + return *p.remoteID } -// Identity returns the client identity of the remote peer. The -// identity can be nil if the peer has not yet completed the -// handshake. -func (p *Peer) Identity() ClientIdentity { - p.infolock.Lock() - defer p.infolock.Unlock() - return p.identity -} - -func (self *Peer) Pubkey() (pubkey []byte) { - self.infolock.Lock() - defer self.infolock.Unlock() - switch { - case self.identity != nil: - pubkey = self.identity.Pubkey()[1:] - case self.dialAddr != nil: - pubkey = self.dialAddr.Pubkey - case self.listenAddr != nil: - pubkey = self.listenAddr.Pubkey - } - return +// Name returns the node name that the remote node advertised. +func (p *Peer) Name() string { + // this needs a lock because the information is part of the + // protocol handshake. + p.infoMu.Lock() + name := p.name + p.infoMu.Unlock() + return name } // Caps returns the capabilities (supported subprotocols) of the remote peer. func (p *Peer) Caps() []Cap { - p.infolock.Lock() - defer p.infolock.Unlock() - return p.caps -} - -func (p *Peer) setHandshakeInfo(id ClientIdentity, laddr *peerAddr, caps []Cap) { - p.infolock.Lock() - p.identity = id - p.listenAddr = laddr - p.caps = caps - p.infolock.Unlock() + // this needs a lock because the information is part of the + // protocol handshake. + p.infoMu.Lock() + caps := p.caps + p.infoMu.Unlock() + return caps } // RemoteAddr returns the remote address of the network connection. func (p *Peer) RemoteAddr() net.Addr { - return p.conn.RemoteAddr() + return p.rw.RemoteAddr() } // LocalAddr returns the local address of the network connection. func (p *Peer) LocalAddr() net.Addr { - return p.conn.LocalAddr() + return p.rw.LocalAddr() } // Disconnect terminates the peer connection with the given reason. @@ -199,201 +134,167 @@ func (p *Peer) Disconnect(reason DiscReason) { // String implements fmt.Stringer. func (p *Peer) String() string { - kind := "inbound" - p.infolock.Lock() - if p.dialAddr != nil { - kind = "outbound" + return fmt.Sprintf("Peer %.8x %v", p.remoteID, p.RemoteAddr()) +} + +func newPeer(conn net.Conn, protocols []Protocol, ourName string, ourID, remoteID *discover.NodeID) *Peer { + logtag := fmt.Sprintf("Peer %.8x %v", remoteID, conn.RemoteAddr()) + return &Peer{ + Logger: logger.NewLogger(logtag), + rw: newFrameRW(conn, msgWriteTimeout), + ourID: ourID, + ourName: ourName, + remoteID: remoteID, + protocols: protocols, + running: make(map[string]*proto), + disc: make(chan DiscReason), + protoErr: make(chan error), + closed: make(chan struct{}), } - p.infolock.Unlock() - return fmt.Sprintf("Peer(%p %v %s)", p, p.conn.RemoteAddr(), kind) } -const ( - // maximum amount of time allowed for reading a message - msgReadTimeout = 5 * time.Second - // maximum amount of time allowed for writing a message - msgWriteTimeout = 5 * time.Second - // messages smaller than this many bytes will be read at - // once before passing them to a protocol. - wholePayloadSize = 64 * 1024 -) - -var ( - inactivityTimeout = 2 * time.Second - disconnectGracePeriod = 2 * time.Second -) +func (p *Peer) setHandshakeInfo(name string, caps []Cap) { + p.infoMu.Lock() + p.name = name + p.caps = caps + p.infoMu.Unlock() +} -func (p *Peer) loop() (reason DiscReason, err error) { - defer p.activity.Stop() +func (p *Peer) run() DiscReason { + var readErr = make(chan error, 1) defer p.closeProtocols() defer close(p.closed) - defer p.conn.Close() + defer p.rw.Close() + + // start the read loop + go func() { readErr <- p.readLoop() }() - var readLoop func(chan<- Msg, chan<- error, <-chan bool) - if p.cryptoHandshake { - if readLoop, err = p.handleCryptoHandshake(); err != nil { - // from here on everything can be encrypted, authenticated - return DiscProtocolError, err // no graceful disconnect + if p.protocolHandshakeEnabled { + if err := writeProtocolHandshake(p.rw, p.ourName, *p.ourID, p.protocols); err != nil { + p.DebugDetailf("Protocol handshake error: %v\n", err) + return DiscProtocolError } - } else { - readLoop = p.readLoop } - // read loop - readMsg := make(chan Msg) - readErr := make(chan error) - readNext := make(chan bool, 1) - protoDone := make(chan struct{}, 1) - go readLoop(readMsg, readErr, readNext) - readNext <- true - - close(p.cryptoReady) - if p.runBaseProtocol { - p.startBaseProtocol() + // wait for an error or disconnect + var reason DiscReason + select { + case err := <-readErr: + // We rely on protocols to abort if there is a write error. It + // might be more robust to handle them here as well. + p.DebugDetailf("Read error: %v\n", err) + reason = DiscNetworkError + case err := <-p.protoErr: + reason = discReasonForError(err) + case reason = <-p.disc: } - -loop: - for { - select { - case msg := <-readMsg: - // a new message has arrived. - var wait bool - if wait, err = p.dispatch(msg, protoDone); err != nil { - p.Errorf("msg dispatch error: %v\n", err) - reason = discReasonForError(err) - break loop - } - if !wait { - // Msg has already been read completely, continue with next message. - readNext <- true - } - p.activity.Post(time.Now()) - case <-protoDone: - // protocol has consumed the message payload, - // we can continue reading from the socket. - readNext <- true - - case err := <-readErr: - // read failed. there is no need to run the - // polite disconnect sequence because the connection - // is probably dead anyway. - // TODO: handle write errors as well - return DiscNetworkError, err - case err = <-p.protoErr: - reason = discReasonForError(err) - break loop - case reason = <-p.disc: - break loop - } + if reason != DiscNetworkError { + p.politeDisconnect(reason) } + p.Debugf("Disconnected: %v\n", reason) + return reason +} - // wait for read loop to return. - close(readNext) - <-readErr - // tell the remote end to disconnect +func (p *Peer) politeDisconnect(reason DiscReason) { done := make(chan struct{}) go func() { - p.conn.SetDeadline(time.Now().Add(disconnectGracePeriod)) - p.writeMsg(NewMsg(discMsg, reason), disconnectGracePeriod) - io.Copy(ioutil.Discard, p.conn) + // send reason + EncodeMsg(p.rw, discMsg, uint(reason)) + // discard any data that might arrive + io.Copy(ioutil.Discard, p.rw) close(done) }() select { case <-done: case <-time.After(disconnectGracePeriod): } - return reason, err } -func (p *Peer) readLoop(msgc chan<- Msg, errc chan<- error, unblock <-chan bool) { - for _ = range unblock { - p.conn.SetReadDeadline(time.Now().Add(msgReadTimeout)) - if msg, err := readMsg(p.bufconn); err != nil { - errc <- err - } else { - msgc <- msg +func (p *Peer) readLoop() error { + if p.protocolHandshakeEnabled { + if err := readProtocolHandshake(p, p.rw); err != nil { + return err } } - close(errc) + for { + msg, err := p.rw.ReadMsg() + if err != nil { + return err + } + if err = p.handle(msg); err != nil { + return err + } + } + return nil } -func (p *Peer) dispatch(msg Msg, protoDone chan struct{}) (wait bool, err error) { - proto, err := p.getProto(msg.Code) - if err != nil { - return false, err - } - if msg.Size <= wholePayloadSize { - // optimization: msg is small enough, read all - // of it and move on to the next message - buf, err := ioutil.ReadAll(msg.Payload) +func (p *Peer) handle(msg Msg) error { + switch { + case msg.Code == pingMsg: + msg.Discard() + go EncodeMsg(p.rw, pongMsg) + case msg.Code == discMsg: + var reason DiscReason + // no need to discard or for error checking, we'll close the + // connection after this. + rlp.Decode(msg.Payload, &reason) + p.Disconnect(DiscRequested) + return discRequestedError(reason) + case msg.Code < baseProtocolLength: + // ignore other base protocol messages + return msg.Discard() + default: + // it's a subprotocol message + proto, err := p.getProto(msg.Code) if err != nil { - return false, err + return fmt.Errorf("msg code out of range: %v", msg.Code) } - msg.Payload = bytes.NewReader(buf) - proto.in <- msg - } else { - wait = true - pr := &eofSignal{msg.Payload, int64(msg.Size), protoDone} - msg.Payload = pr proto.in <- msg } - return wait, nil + return nil } -type readLoop func(chan<- Msg, chan<- error, <-chan bool) - -func (p *Peer) PrivateKey() (prv *ecdsa.PrivateKey, err error) { - if prv = crypto.ToECDSA(p.privateKey); prv == nil { - err = fmt.Errorf("invalid private key") +func readProtocolHandshake(p *Peer, rw MsgReadWriter) error { + // read and handle remote handshake + msg, err := rw.ReadMsg() + if err != nil { + return err } - return -} - -func (p *Peer) handleCryptoHandshake() (loop readLoop, err error) { - // cryptoId is just created for the lifecycle of the handshake - // it is survived by an encrypted readwriter - var initiator bool - var sessionToken []byte - sessionToken = make([]byte, shaLen) - if _, err = rand.Read(sessionToken); err != nil { - return + if msg.Code != handshakeMsg { + return newPeerError(errProtocolBreach, "expected handshake, got %x", msg.Code) } - if p.dialAddr != nil { // this should have its own method Outgoing() bool - initiator = true + if msg.Size > baseProtocolMaxMsgSize { + return newPeerError(errMisc, "message too big") } - - // run on peer - // this bit handles the handshake and creates a secure communications channel with - // var rw *secretRW - var prvKey *ecdsa.PrivateKey - if prvKey, err = p.PrivateKey(); err != nil { - err = fmt.Errorf("unable to access private key for client: %v", err) - return + var hs handshake + if err := msg.Decode(&hs); err != nil { + return err } - // initialise a new secure session - if sessionToken, _, err = NewSecureSession(p.conn, prvKey, p.Pubkey(), sessionToken, initiator); err != nil { - p.Debugf("unable to setup secure session: %v", err) - return + // validate handshake info + if hs.Version != baseProtocolVersion { + return newPeerError(errP2PVersionMismatch, "required version %d, received %d\n", + baseProtocolVersion, hs.Version) } - loop = func(msg chan<- Msg, err chan<- error, next <-chan bool) { - // this is the readloop :) + if hs.NodeID == *p.remoteID { + return newPeerError(errPubkeyForbidden, "node ID mismatch") } - return + // TODO: remove Caps with empty name + p.setHandshakeInfo(hs.Name, hs.Caps) + p.startSubprotocols(hs.Caps) + return nil } -func (p *Peer) startBaseProtocol() { - p.runlock.Lock() - defer p.runlock.Unlock() - p.running[""] = p.startProto(0, Protocol{ - Length: baseProtocolLength, - Run: runBaseProtocol, - }) +func writeProtocolHandshake(w MsgWriter, name string, id discover.NodeID, ps []Protocol) error { + var caps []interface{} + for _, proto := range ps { + caps = append(caps, proto.cap()) + } + return EncodeMsg(w, handshakeMsg, baseProtocolVersion, name, caps, 0, id) } // startProtocols starts matching named subprotocols. func (p *Peer) startSubprotocols(caps []Cap) { sort.Sort(capsByName(caps)) - p.runlock.Lock() defer p.runlock.Unlock() offset := baseProtocolLength @@ -412,20 +313,22 @@ outer: } func (p *Peer) startProto(offset uint64, impl Protocol) *proto { + p.DebugDetailf("Starting protocol %s/%d\n", impl.Name, impl.Version) rw := &proto{ + name: impl.Name, in: make(chan Msg), offset: offset, maxcode: impl.Length, - peer: p, + w: p.rw, } p.protoWG.Add(1) go func() { err := impl.Run(p, rw) if err == nil { - p.Infof("protocol %q returned", impl.Name) + p.DebugDetailf("Protocol %s/%d returned\n", impl.Name, impl.Version) err = newPeerError(errMisc, "protocol returned") } else { - p.Errorf("protocol %q error: %v\n", impl.Name, err) + p.DebugDetailf("Protocol %s/%d error: %v\n", impl.Name, impl.Version, err) } select { case p.protoErr <- err: @@ -459,6 +362,7 @@ func (p *Peer) closeProtocols() { } // writeProtoMsg sends the given message on behalf of the given named protocol. +// this exists because of Server.Broadcast. func (p *Peer) writeProtoMsg(protoName string, msg Msg) error { p.runlock.RLock() proto, ok := p.running[protoName] @@ -470,25 +374,14 @@ func (p *Peer) writeProtoMsg(protoName string, msg Msg) error { return newPeerError(errInvalidMsgCode, "code %x is out of range for protocol %q", msg.Code, protoName) } msg.Code += proto.offset - return p.writeMsg(msg, msgWriteTimeout) -} - -// writeMsg writes a message to the connection. -func (p *Peer) writeMsg(msg Msg, timeout time.Duration) error { - p.writeMu.Lock() - defer p.writeMu.Unlock() - p.conn.SetWriteDeadline(time.Now().Add(timeout)) - if err := writeMsg(p.bufconn, msg); err != nil { - return newPeerError(errWrite, "%v", err) - } - return p.bufconn.Flush() + return p.rw.WriteMsg(msg) } type proto struct { name string in chan Msg maxcode, offset uint64 - peer *Peer + w MsgWriter } func (rw *proto) WriteMsg(msg Msg) error { @@ -496,11 +389,7 @@ func (rw *proto) WriteMsg(msg Msg) error { return newPeerError(errInvalidMsgCode, "not handled") } msg.Code += rw.offset - return rw.peer.writeMsg(msg, msgWriteTimeout) -} - -func (rw *proto) EncodeMsg(code uint64, data ...interface{}) error { - return rw.WriteMsg(NewMsg(code, data...)) + return rw.w.WriteMsg(msg) } func (rw *proto) ReadMsg() (Msg, error) { @@ -511,26 +400,3 @@ func (rw *proto) ReadMsg() (Msg, error) { msg.Code -= rw.offset return msg, nil } - -// 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. -// -type eofSignal struct { - wrapped io.Reader - count int64 - eof chan<- struct{} -} - -// note: when using eofSignal to detect whether a message payload -// has been read, Read might not be called for zero sized messages. - -func (r *eofSignal) Read(buf []byte) (int, error) { - n, err := r.wrapped.Read(buf) - r.count -= int64(n) - if (err != nil || r.count <= 0) && r.eof != nil { - r.eof <- struct{}{} // tell Peer that msg has been consumed - r.eof = nil - } - return n, err -} diff --git a/p2p/peer_error.go b/p2p/peer_error.go index 0eb7ec838..9133768f9 100644 --- a/p2p/peer_error.go +++ b/p2p/peer_error.go @@ -12,7 +12,6 @@ const ( errInvalidMsgCode errInvalidMsg errP2PVersionMismatch - errPubkeyMissing errPubkeyInvalid errPubkeyForbidden errProtocolBreach @@ -22,20 +21,19 @@ const ( ) var errorToString = map[int]string{ - errMagicTokenMismatch: "Magic token mismatch", - errRead: "Read error", - errWrite: "Write error", - errMisc: "Misc error", - errInvalidMsgCode: "Invalid message code", - errInvalidMsg: "Invalid message", + errMagicTokenMismatch: "magic token mismatch", + errRead: "read error", + errWrite: "write error", + errMisc: "misc error", + errInvalidMsgCode: "invalid message code", + errInvalidMsg: "invalid message", errP2PVersionMismatch: "P2P Version Mismatch", - errPubkeyMissing: "Public key missing", - errPubkeyInvalid: "Public key invalid", - errPubkeyForbidden: "Public key forbidden", - errProtocolBreach: "Protocol Breach", - errPingTimeout: "Ping timeout", - errInvalidNetworkId: "Invalid network id", - errInvalidProtocolVersion: "Invalid protocol version", + errPubkeyInvalid: "public key invalid", + errPubkeyForbidden: "public key forbidden", + errProtocolBreach: "protocol Breach", + errPingTimeout: "ping timeout", + errInvalidNetworkId: "invalid network id", + errInvalidProtocolVersion: "invalid protocol version", } type peerError struct { @@ -62,22 +60,22 @@ func (self *peerError) Error() string { type DiscReason byte const ( - DiscRequested DiscReason = 0x00 - DiscNetworkError = 0x01 - DiscProtocolError = 0x02 - DiscUselessPeer = 0x03 - DiscTooManyPeers = 0x04 - DiscAlreadyConnected = 0x05 - DiscIncompatibleVersion = 0x06 - DiscInvalidIdentity = 0x07 - DiscQuitting = 0x08 - DiscUnexpectedIdentity = 0x09 - DiscSelf = 0x0a - DiscReadTimeout = 0x0b - DiscSubprotocolError = 0x10 + DiscRequested DiscReason = iota + DiscNetworkError + DiscProtocolError + DiscUselessPeer + DiscTooManyPeers + DiscAlreadyConnected + DiscIncompatibleVersion + DiscInvalidIdentity + DiscQuitting + DiscUnexpectedIdentity + DiscSelf + DiscReadTimeout + DiscSubprotocolError ) -var discReasonToString = [DiscSubprotocolError + 1]string{ +var discReasonToString = [...]string{ DiscRequested: "Disconnect requested", DiscNetworkError: "Network error", DiscProtocolError: "Breach of protocol", @@ -117,7 +115,7 @@ func discReasonForError(err error) DiscReason { switch peerError.Code { case errP2PVersionMismatch: return DiscIncompatibleVersion - case errPubkeyMissing, errPubkeyInvalid: + case errPubkeyInvalid: return DiscInvalidIdentity case errPubkeyForbidden: return DiscUselessPeer diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 4ee88f112..76d856d3e 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -1,15 +1,17 @@ package p2p import ( - "bufio" "bytes" - "encoding/hex" - "io" + "fmt" "io/ioutil" "net" "reflect" + "sort" "testing" "time" + + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/rlp" ) var discard = Protocol{ @@ -28,17 +30,13 @@ var discard = Protocol{ }, } -func testPeer(protos []Protocol) (net.Conn, *Peer, <-chan error) { +func testPeer(handshake bool, protos []Protocol) (*frameRW, *Peer, <-chan DiscReason) { conn1, conn2 := net.Pipe() - peer := newPeer(conn1, protos, nil) - peer.ourID = &peerId{} - peer.pubkeyHook = func(*peerAddr) error { return nil } - errc := make(chan error, 1) - go func() { - _, err := peer.loop() - errc <- err - }() - return conn2, peer, errc + peer := newPeer(conn1, protos, "name", &discover.NodeID{}, &discover.NodeID{}) + peer.protocolHandshakeEnabled = handshake + errc := make(chan DiscReason, 1) + go func() { errc <- peer.run() }() + return newFrameRW(conn2, msgWriteTimeout), peer, errc } func TestPeerProtoReadMsg(t *testing.T) { @@ -49,31 +47,28 @@ func TestPeerProtoReadMsg(t *testing.T) { Name: "a", Length: 5, Run: func(peer *Peer, rw MsgReadWriter) error { - msg, err := rw.ReadMsg() - if err != nil { - t.Errorf("read error: %v", err) + if err := expectMsg(rw, 2, []uint{1}); err != nil { + t.Error(err) } - if msg.Code != 2 { - t.Errorf("incorrect msg code %d relayed to protocol", msg.Code) - } - data, err := ioutil.ReadAll(msg.Payload) - if err != nil { - t.Errorf("payload read error: %v", err) + if err := expectMsg(rw, 3, []uint{2}); err != nil { + t.Error(err) } - expdata, _ := hex.DecodeString("0183303030") - if !bytes.Equal(expdata, data) { - t.Errorf("incorrect msg data %x", data) + if err := expectMsg(rw, 4, []uint{3}); err != nil { + t.Error(err) } close(done) return nil }, } - net, peer, errc := testPeer([]Protocol{proto}) - defer net.Close() + rw, peer, errc := testPeer(false, []Protocol{proto}) + defer rw.Close() peer.startSubprotocols([]Cap{proto.cap()}) - writeMsg(net, NewMsg(18, 1, "000")) + EncodeMsg(rw, baseProtocolLength+2, 1) + EncodeMsg(rw, baseProtocolLength+3, 2) + EncodeMsg(rw, baseProtocolLength+4, 3) + select { case <-done: case err := <-errc: @@ -105,11 +100,11 @@ func TestPeerProtoReadLargeMsg(t *testing.T) { }, } - net, peer, errc := testPeer([]Protocol{proto}) - defer net.Close() + rw, peer, errc := testPeer(false, []Protocol{proto}) + defer rw.Close() peer.startSubprotocols([]Cap{proto.cap()}) - writeMsg(net, NewMsg(18, make([]byte, msgsize))) + EncodeMsg(rw, 18, make([]byte, msgsize)) select { case <-done: case err := <-errc: @@ -135,32 +130,20 @@ func TestPeerProtoEncodeMsg(t *testing.T) { return nil }, } - net, peer, _ := testPeer([]Protocol{proto}) - defer net.Close() + rw, peer, _ := testPeer(false, []Protocol{proto}) + defer rw.Close() peer.startSubprotocols([]Cap{proto.cap()}) - bufr := bufio.NewReader(net) - msg, err := readMsg(bufr) - if err != nil { - t.Errorf("read error: %v", err) - } - if msg.Code != 17 { - t.Errorf("incorrect message code: got %d, expected %d", msg.Code, 17) - } - var data []string - if err := msg.Decode(&data); err != nil { - t.Errorf("payload decode error: %v", err) - } - if !reflect.DeepEqual(data, []string{"foo", "bar"}) { - t.Errorf("payload RLP mismatch, got %#v, want %#v", data, []string{"foo", "bar"}) + if err := expectMsg(rw, 17, []string{"foo", "bar"}); err != nil { + t.Error(err) } } -func TestPeerWrite(t *testing.T) { +func TestPeerWriteForBroadcast(t *testing.T) { defer testlog(t).detach() - net, peer, peerErr := testPeer([]Protocol{discard}) - defer net.Close() + rw, peer, peerErr := testPeer(false, []Protocol{discard}) + defer rw.Close() peer.startSubprotocols([]Cap{discard.cap()}) // test write errors @@ -176,18 +159,13 @@ func TestPeerWrite(t *testing.T) { // setup for reading the message on the other end read := make(chan struct{}) go func() { - bufr := bufio.NewReader(net) - msg, err := readMsg(bufr) - if err != nil { - t.Errorf("read error: %v", err) - } else if msg.Code != 16 { - t.Errorf("wrong code, got %d, expected %d", msg.Code, 16) + if err := expectMsg(rw, 16, nil); err != nil { + t.Error() } - msg.Discard() close(read) }() - // test succcessful write + // test successful write if err := peer.writeProtoMsg("discard", NewMsg(0)); err != nil { t.Errorf("expect no error for known protocol: %v", err) } @@ -198,104 +176,152 @@ func TestPeerWrite(t *testing.T) { } } -func TestPeerActivity(t *testing.T) { - // shorten inactivityTimeout while this test is running - oldT := inactivityTimeout - defer func() { inactivityTimeout = oldT }() - inactivityTimeout = 20 * time.Millisecond +func TestPeerPing(t *testing.T) { + defer testlog(t).detach() - net, peer, peerErr := testPeer([]Protocol{discard}) - defer net.Close() - peer.startSubprotocols([]Cap{discard.cap()}) + rw, _, _ := testPeer(false, nil) + defer rw.Close() + if err := EncodeMsg(rw, pingMsg); err != nil { + t.Fatal(err) + } + if err := expectMsg(rw, pongMsg, nil); err != nil { + t.Error(err) + } +} - sub := peer.activity.Subscribe(time.Time{}) - defer sub.Unsubscribe() +func TestPeerDisconnect(t *testing.T) { + defer testlog(t).detach() - for i := 0; i < 6; i++ { - writeMsg(net, NewMsg(16)) - select { - case <-sub.Chan(): - case <-time.After(inactivityTimeout / 2): - t.Fatal("no event within ", inactivityTimeout/2) - case err := <-peerErr: - t.Fatal("peer error", err) - } + rw, _, disc := testPeer(false, nil) + defer rw.Close() + if err := EncodeMsg(rw, discMsg, DiscQuitting); err != nil { + t.Fatal(err) } - - select { - case <-time.After(inactivityTimeout * 2): - case <-sub.Chan(): - t.Fatal("got activity event while connection was inactive") - case err := <-peerErr: - t.Fatal("peer error", err) + if err := expectMsg(rw, discMsg, []interface{}{DiscRequested}); err != nil { + t.Error(err) + } + rw.Close() // make test end faster + if reason := <-disc; reason != DiscRequested { + t.Errorf("run returned wrong reason: got %v, want %v", reason, DiscRequested) } } -func TestNewPeer(t *testing.T) { - caps := []Cap{{"foo", 2}, {"bar", 3}} - id := &peerId{} - p := NewPeer(id, caps) - if !reflect.DeepEqual(p.Caps(), caps) { - t.Errorf("Caps mismatch: got %v, expected %v", p.Caps(), caps) +func TestPeerHandshake(t *testing.T) { + defer testlog(t).detach() + + // remote has two matching protocols: a and c + remote := NewPeer(randomID(), "", []Cap{{"a", 1}, {"b", 999}, {"c", 3}}) + remoteID := randomID() + remote.ourID = &remoteID + remote.ourName = "remote peer" + + start := make(chan string) + stop := make(chan struct{}) + run := func(p *Peer, rw MsgReadWriter) error { + name := rw.(*proto).name + if name != "a" && name != "c" { + t.Errorf("protocol %q should not be started", name) + } else { + start <- name + } + <-stop + return nil } - if p.Identity() != id { - t.Errorf("Identity mismatch: got %v, expected %v", p.Identity(), id) + protocols := []Protocol{ + {Name: "a", Version: 1, Length: 1, Run: run}, + {Name: "b", Version: 2, Length: 1, Run: run}, + {Name: "c", Version: 3, Length: 1, Run: run}, + {Name: "d", Version: 4, Length: 1, Run: run}, } - // Should not hang. - p.Disconnect(DiscAlreadyConnected) -} + rw, p, disc := testPeer(true, protocols) + p.remoteID = remote.ourID + defer rw.Close() -func TestEOFSignal(t *testing.T) { - rb := make([]byte, 10) + // run the handshake + remoteProtocols := []Protocol{protocols[0], protocols[2]} + if err := writeProtocolHandshake(rw, "remote peer", remoteID, remoteProtocols); err != nil { + t.Fatalf("handshake write error: %v", err) + } + if err := readProtocolHandshake(remote, rw); err != nil { + t.Fatalf("handshake read error: %v", err) + } - // empty reader - eof := make(chan struct{}, 1) - sig := &eofSignal{new(bytes.Buffer), 0, eof} - if n, err := sig.Read(rb); n != 0 || err != io.EOF { - t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + // check that all protocols have been started + var started []string + for i := 0; i < 2; i++ { + select { + case name := <-start: + started = append(started, name) + case <-time.After(100 * time.Millisecond): + } } - select { - case <-eof: - default: - t.Error("EOF chan not signaled") + sort.Strings(started) + if !reflect.DeepEqual(started, []string{"a", "c"}) { + t.Errorf("wrong protocols started: %v", started) } - // count before error - eof = make(chan struct{}, 1) - sig = &eofSignal{bytes.NewBufferString("aaaaaaaa"), 4, eof} - if n, err := sig.Read(rb); n != 8 || err != nil { - t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + // check that metadata has been set + if p.ID() != remoteID { + t.Errorf("peer has wrong node ID: got %v, want %v", p.ID(), remoteID) } - select { - case <-eof: - default: - t.Error("EOF chan not signaled") + if p.Name() != remote.ourName { + t.Errorf("peer has wrong node name: got %q, want %q", p.Name(), remote.ourName) } - // error before count - eof = make(chan struct{}, 1) - sig = &eofSignal{bytes.NewBufferString("aaaa"), 999, eof} - if n, err := sig.Read(rb); n != 4 || err != nil { - t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + close(stop) + t.Logf("disc reason: %v", <-disc) +} + +func TestNewPeer(t *testing.T) { + name := "nodename" + caps := []Cap{{"foo", 2}, {"bar", 3}} + id := randomID() + p := NewPeer(id, name, caps) + if p.ID() != id { + t.Errorf("ID mismatch: got %v, expected %v", p.ID(), id) } - if n, err := sig.Read(rb); n != 0 || err != io.EOF { - t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + if p.Name() != name { + t.Errorf("Name mismatch: got %v, expected %v", p.Name(), name) } - select { - case <-eof: - default: - t.Error("EOF chan not signaled") + if !reflect.DeepEqual(p.Caps(), caps) { + t.Errorf("Caps mismatch: got %v, expected %v", p.Caps(), caps) } - // no signal if neither occurs - eof = make(chan struct{}, 1) - sig = &eofSignal{bytes.NewBufferString("aaaaaaaaaaaaaaaaaaaaa"), 999, eof} - if n, err := sig.Read(rb); n != 10 || err != nil { - t.Errorf("Read returned unexpected values: (%v, %v)", n, err) + p.Disconnect(DiscAlreadyConnected) // Should not hang +} + +// expectMsg reads a message from r and verifies that its +// code and encoded RLP content match the provided values. +// If content is nil, the payload is discarded and not verified. +func expectMsg(r MsgReader, code uint64, content interface{}) error { + msg, err := r.ReadMsg() + if err != nil { + return err } - select { - case <-eof: - t.Error("unexpected EOF signal") - default: + if msg.Code != code { + return fmt.Errorf("message code mismatch: got %d, expected %d", msg.Code, code) + } + if content == nil { + return msg.Discard() + } else { + contentEnc, err := rlp.EncodeToBytes(content) + if err != nil { + panic("content encode error: " + err.Error()) + } + // skip over list header in encoded value. this is temporary. + contentEncR := bytes.NewReader(contentEnc) + if k, _, err := rlp.NewStream(contentEncR).Kind(); k != rlp.List || err != nil { + panic("content must encode as RLP list") + } + contentEnc = contentEnc[len(contentEnc)-contentEncR.Len():] + + actualContent, err := ioutil.ReadAll(msg.Payload) + if err != nil { + return err + } + if !bytes.Equal(actualContent, contentEnc) { + return fmt.Errorf("message payload mismatch:\ngot: %x\nwant: %x", actualContent, contentEnc) + } } + return nil } diff --git a/p2p/protocol.go b/p2p/protocol.go index 62ada929d..fe359fc54 100644 --- a/p2p/protocol.go +++ b/p2p/protocol.go @@ -1,10 +1,5 @@ package p2p -import ( - "bytes" - "time" -) - // Protocol represents a P2P subprotocol implementation. type Protocol struct { // Name should contain the official protocol name, @@ -32,42 +27,6 @@ func (p Protocol) cap() Cap { return Cap{p.Name, p.Version} } -const ( - baseProtocolVersion = 2 - baseProtocolLength = uint64(16) - baseProtocolMaxMsgSize = 10 * 1024 * 1024 -) - -const ( - // devp2p message codes - handshakeMsg = 0x00 - discMsg = 0x01 - pingMsg = 0x02 - pongMsg = 0x03 - getPeersMsg = 0x04 - peersMsg = 0x05 -) - -// handshake is the structure of a handshake list. -type handshake struct { - Version uint64 - ID string - Caps []Cap - ListenPort uint64 - NodeID []byte -} - -func (h *handshake) String() string { - return h.ID -} -func (h *handshake) Pubkey() []byte { - return h.NodeID -} - -func (h *handshake) PrivKey() []byte { - return nil -} - // Cap is the structure of a peer capability. type Cap struct { Name string @@ -83,210 +42,3 @@ type capsByName []Cap func (cs capsByName) Len() int { return len(cs) } func (cs capsByName) Less(i, j int) bool { return cs[i].Name < cs[j].Name } func (cs capsByName) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] } - -type baseProtocol struct { - rw MsgReadWriter - peer *Peer -} - -func runBaseProtocol(peer *Peer, rw MsgReadWriter) error { - bp := &baseProtocol{rw, peer} - errc := make(chan error, 1) - go func() { errc <- rw.WriteMsg(bp.handshakeMsg()) }() - if err := bp.readHandshake(); err != nil { - return err - } - // handle write error - if err := <-errc; err != nil { - return err - } - // run main loop - go func() { - for { - if err := bp.handle(rw); err != nil { - errc <- err - break - } - } - }() - return bp.loop(errc) -} - -var pingTimeout = 2 * time.Second - -func (bp *baseProtocol) loop(quit <-chan error) error { - ping := time.NewTimer(pingTimeout) - activity := bp.peer.activity.Subscribe(time.Time{}) - lastActive := time.Time{} - defer ping.Stop() - defer activity.Unsubscribe() - - getPeersTick := time.NewTicker(10 * time.Second) - defer getPeersTick.Stop() - err := EncodeMsg(bp.rw, getPeersMsg) - - for err == nil { - select { - case err = <-quit: - return err - case <-getPeersTick.C: - err = EncodeMsg(bp.rw, getPeersMsg) - case event := <-activity.Chan(): - ping.Reset(pingTimeout) - lastActive = event.(time.Time) - case t := <-ping.C: - if lastActive.Add(pingTimeout * 2).Before(t) { - err = newPeerError(errPingTimeout, "") - } else if lastActive.Add(pingTimeout).Before(t) { - err = EncodeMsg(bp.rw, pingMsg) - } - } - } - return err -} - -func (bp *baseProtocol) handle(rw MsgReadWriter) error { - msg, err := rw.ReadMsg() - if err != nil { - return err - } - if msg.Size > baseProtocolMaxMsgSize { - return newPeerError(errMisc, "message too big") - } - // make sure that the payload has been fully consumed - defer msg.Discard() - - switch msg.Code { - case handshakeMsg: - return newPeerError(errProtocolBreach, "extra handshake received") - - case discMsg: - var reason [1]DiscReason - if err := msg.Decode(&reason); err != nil { - return err - } - return discRequestedError(reason[0]) - - case pingMsg: - return EncodeMsg(bp.rw, pongMsg) - - case pongMsg: - - case getPeersMsg: - peers := bp.peerList() - // this is dangerous. the spec says that we should _delay_ - // sending the response if no new information is available. - // this means that would need to send a response later when - // new peers become available. - // - // TODO: add event mechanism to notify baseProtocol for new peers - if len(peers) > 0 { - return EncodeMsg(bp.rw, peersMsg, peers...) - } - - case peersMsg: - var peers []*peerAddr - if err := msg.Decode(&peers); err != nil { - return err - } - for _, addr := range peers { - bp.peer.Debugf("received peer suggestion: %v", addr) - bp.peer.newPeerAddr <- addr - } - - default: - return newPeerError(errInvalidMsgCode, "unknown message code %v", msg.Code) - } - return nil -} - -func (bp *baseProtocol) readHandshake() error { - // read and handle remote handshake - msg, err := bp.rw.ReadMsg() - if err != nil { - return err - } - if msg.Code != handshakeMsg { - return newPeerError(errProtocolBreach, "first message must be handshake, got %x", msg.Code) - } - if msg.Size > baseProtocolMaxMsgSize { - return newPeerError(errMisc, "message too big") - } - var hs handshake - if err := msg.Decode(&hs); err != nil { - return err - } - // validate handshake info - if hs.Version != baseProtocolVersion { - return newPeerError(errP2PVersionMismatch, "Require protocol %d, received %d\n", - baseProtocolVersion, hs.Version) - } - if len(hs.NodeID) == 0 { - return newPeerError(errPubkeyMissing, "") - } - if len(hs.NodeID) != 64 { - return newPeerError(errPubkeyInvalid, "require 512 bit, got %v", len(hs.NodeID)*8) - } - if da := bp.peer.dialAddr; da != nil { - // verify that the peer we wanted to connect to - // actually holds the target public key. - if da.Pubkey != nil && !bytes.Equal(da.Pubkey, hs.NodeID) { - return newPeerError(errPubkeyForbidden, "dial address pubkey mismatch") - } - } - pa := newPeerAddr(bp.peer.conn.RemoteAddr(), hs.NodeID) - if err := bp.peer.pubkeyHook(pa); err != nil { - return newPeerError(errPubkeyForbidden, "%v", err) - } - // TODO: remove Caps with empty name - var addr *peerAddr - if hs.ListenPort != 0 { - addr = newPeerAddr(bp.peer.conn.RemoteAddr(), hs.NodeID) - addr.Port = hs.ListenPort - } - bp.peer.setHandshakeInfo(&hs, addr, hs.Caps) - bp.peer.startSubprotocols(hs.Caps) - return nil -} - -func (bp *baseProtocol) handshakeMsg() Msg { - var ( - port uint64 - caps []interface{} - ) - if bp.peer.ourListenAddr != nil { - port = bp.peer.ourListenAddr.Port - } - for _, proto := range bp.peer.protocols { - caps = append(caps, proto.cap()) - } - return NewMsg(handshakeMsg, - baseProtocolVersion, - bp.peer.ourID.String(), - caps, - port, - bp.peer.ourID.Pubkey()[1:], - ) -} - -func (bp *baseProtocol) peerList() []interface{} { - peers := bp.peer.otherPeers() - ds := make([]interface{}, 0, len(peers)) - for _, p := range peers { - p.infolock.Lock() - addr := p.listenAddr - p.infolock.Unlock() - // filter out this peer and peers that are not listening or - // have not completed the handshake. - // TODO: track previously sent peers and exclude them as well. - if p == bp.peer || addr == nil { - continue - } - ds = append(ds, addr) - } - ourAddr := bp.peer.ourListenAddr - if ourAddr != nil && !ourAddr.IP.IsLoopback() && !ourAddr.IP.IsUnspecified() { - ds = append(ds, ourAddr) - } - return ds -} diff --git a/p2p/protocol_test.go b/p2p/protocol_test.go deleted file mode 100644 index 6804a9d40..000000000 --- a/p2p/protocol_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package p2p - -import ( - "fmt" - "net" - "reflect" - "sync" - "testing" - - "github.com/ethereum/go-ethereum/crypto" -) - -type peerId struct { - privKey, pubkey []byte -} - -func (self *peerId) String() string { - return fmt.Sprintf("test peer %x", self.Pubkey()[:4]) -} - -func (self *peerId) Pubkey() (pubkey []byte) { - pubkey = self.pubkey - if len(pubkey) == 0 { - pubkey = crypto.GenerateNewKeyPair().PublicKey - self.pubkey = pubkey - } - return -} - -func (self *peerId) PrivKey() (privKey []byte) { - privKey = self.privKey - if len(privKey) == 0 { - privKey = crypto.GenerateNewKeyPair().PublicKey - self.privKey = privKey - } - return -} - -func newTestPeer() (peer *Peer) { - peer = NewPeer(&peerId{}, []Cap{}) - peer.pubkeyHook = func(*peerAddr) error { return nil } - peer.ourID = &peerId{} - peer.listenAddr = &peerAddr{} - peer.otherPeers = func() []*Peer { return nil } - return -} - -func TestBaseProtocolPeers(t *testing.T) { - peerList := []*peerAddr{ - {IP: net.ParseIP("1.2.3.4"), Port: 2222, Pubkey: []byte{}}, - {IP: net.ParseIP("5.6.7.8"), Port: 3333, Pubkey: []byte{}}, - } - listenAddr := &peerAddr{IP: net.ParseIP("1.3.5.7"), Port: 1111, Pubkey: []byte{}} - rw1, rw2 := MsgPipe() - defer rw1.Close() - wg := new(sync.WaitGroup) - - // run matcher, close pipe when addresses have arrived - numPeers := len(peerList) + 1 - addrChan := make(chan *peerAddr) - wg.Add(1) - go func() { - i := 0 - for got := range addrChan { - var want *peerAddr - switch { - case i < len(peerList): - want = peerList[i] - case i == len(peerList): - want = listenAddr // listenAddr should be the last thing sent - } - t.Logf("got peer %d/%d: %v", i+1, numPeers, got) - if !reflect.DeepEqual(want, got) { - t.Errorf("mismatch: got %+v, want %+v", got, want) - } - i++ - if i == numPeers { - break - } - } - if i != numPeers { - t.Errorf("wrong number of peers received: got %d, want %d", i, numPeers) - } - rw1.Close() - wg.Done() - }() - - // run first peer (in background) - peer1 := newTestPeer() - peer1.ourListenAddr = listenAddr - peer1.otherPeers = func() []*Peer { - pl := make([]*Peer, len(peerList)) - for i, addr := range peerList { - pl[i] = &Peer{listenAddr: addr} - } - return pl - } - wg.Add(1) - go func() { - runBaseProtocol(peer1, rw1) - wg.Done() - }() - - // run second peer - peer2 := newTestPeer() - peer2.newPeerAddr = addrChan // feed peer suggestions into matcher - if err := runBaseProtocol(peer2, rw2); err != ErrPipeClosed { - t.Errorf("peer2 terminated with unexpected error: %v", err) - } - - // terminate matcher - close(addrChan) - wg.Wait() -} - -func TestBaseProtocolDisconnect(t *testing.T) { - peer := NewPeer(&peerId{}, nil) - peer.ourID = &peerId{} - peer.pubkeyHook = func(*peerAddr) error { return nil } - - rw1, rw2 := MsgPipe() - done := make(chan struct{}) - go func() { - if err := expectMsg(rw2, handshakeMsg); err != nil { - t.Error(err) - } - err := EncodeMsg(rw2, handshakeMsg, - baseProtocolVersion, - "", - []interface{}{}, - 0, - make([]byte, 64), - ) - if err != nil { - t.Error(err) - } - if err := expectMsg(rw2, getPeersMsg); err != nil { - t.Error(err) - } - if err := EncodeMsg(rw2, discMsg, DiscQuitting); err != nil { - t.Error(err) - } - - close(done) - }() - - if err := runBaseProtocol(peer, rw1); err == nil { - t.Errorf("base protocol returned without error") - } else if reason, ok := err.(discRequestedError); !ok || reason != DiscQuitting { - t.Errorf("base protocol returned wrong error: %v", err) - } - <-done -} - -func expectMsg(r MsgReader, code uint64) error { - msg, err := r.ReadMsg() - if err != nil { - return err - } - if err := msg.Discard(); err != nil { - return err - } - if msg.Code != code { - return fmt.Errorf("wrong message code: got %d, expected %d", msg.Code, code) - } - return nil -} diff --git a/p2p/server.go b/p2p/server.go index 4fd1f7d03..9b8030541 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -2,37 +2,56 @@ package p2p import ( "bytes" + "crypto/ecdsa" "errors" "fmt" + "io" "net" + "runtime" "sync" "time" "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/p2p/discover" ) const ( - outboundAddressPoolSize = 500 defaultDialTimeout = 10 * time.Second + refreshPeersInterval = 30 * time.Second portMappingUpdateInterval = 15 * time.Minute portMappingTimeout = 20 * time.Minute ) var srvlog = logger.NewLogger("P2P Server") +// MakeName creates a node name that follows the ethereum convention +// for such names. It adds the operation system name and Go runtime version +// the name. +func MakeName(name, version string) string { + return fmt.Sprintf("%s/v%s/%s/%s", name, version, runtime.GOOS, runtime.Version()) +} + // Server manages all peer connections. // // The fields of Server are used as configuration parameters. // You should set them before starting the Server. Fields may not be // modified while the server is running. type Server struct { - // This field must be set to a valid client identity. - Identity ClientIdentity + // This field must be set to a valid secp256k1 private key. + PrivateKey *ecdsa.PrivateKey // MaxPeers is the maximum number of peers that can be // connected. It must be greater than zero. MaxPeers int + // Name sets the node name of this server. + // Use MakeName to create a name that follows existing conventions. + Name string + + // Bootstrap nodes are used to establish connectivity + // with the rest of the network. + BootstrapNodes []discover.Node + // Protocols should contain the protocols supported // by the server. Matching protocols are launched for // each peer. @@ -62,22 +81,23 @@ type Server struct { // If NoDial is true, the server will not dial any peers. NoDial bool - // Hook for testing. This is useful because we can inhibit + // Hooks for testing. These are useful because we can inhibit // the whole protocol stack. - newPeerFunc peerFunc + handshakeFunc + newPeerHook - lock sync.RWMutex - running bool - listener net.Listener - laddr *net.TCPAddr // real listen addr - peers []*Peer - peerSlots chan int - peerCount int - - quit chan struct{} - wg sync.WaitGroup - peerConnect chan *peerAddr - peerDisconnect chan *Peer + lock sync.RWMutex + running bool + listener net.Listener + laddr *net.TCPAddr // real listen addr + peers map[discover.NodeID]*Peer + + ntab *discover.Table + + quit chan struct{} + loopWG sync.WaitGroup // {dial,listen,nat}Loop + peerWG sync.WaitGroup // active peer goroutines + peerConnect chan *discover.Node } // NAT is implemented by NAT traversal methods. @@ -90,7 +110,8 @@ type NAT interface { String() string } -type peerFunc func(srv *Server, c net.Conn, dialAddr *peerAddr) *Peer +type handshakeFunc func(io.ReadWriter, *ecdsa.PrivateKey, *discover.Node) (discover.NodeID, []byte, error) +type newPeerHook func(*Peer) // Peers returns all connected peers. func (srv *Server) Peers() (peers []*Peer) { @@ -107,18 +128,15 @@ func (srv *Server) Peers() (peers []*Peer) { // PeerCount returns the number of connected peers. func (srv *Server) PeerCount() int { srv.lock.RLock() - defer srv.lock.RUnlock() - return srv.peerCount + n := len(srv.peers) + srv.lock.RUnlock() + return n } -// SuggestPeer injects an address into the outbound address pool. -func (srv *Server) SuggestPeer(ip net.IP, port int, nodeID []byte) { - addr := &peerAddr{ip, uint64(port), nodeID} - select { - case srv.peerConnect <- addr: - default: // don't block - srvlog.Warnf("peer suggestion %v ignored", addr) - } +// SuggestPeer creates a connection to the given Node if it +// is not already connected. +func (srv *Server) SuggestPeer(ip net.IP, port int, id discover.NodeID) { + srv.peerConnect <- &discover.Node{ID: id, Addr: &net.UDPAddr{IP: ip, Port: port}} } // Broadcast sends an RLP-encoded message to all connected peers. @@ -152,47 +170,47 @@ func (srv *Server) Start() (err error) { } srvlog.Infoln("Starting Server") - // initialize fields - if srv.Identity == nil { - return fmt.Errorf("Server.Identity must be set to a non-nil identity") + // initialize all the fields + if srv.PrivateKey == nil { + return fmt.Errorf("Server.PrivateKey must be set to a non-nil key") } if srv.MaxPeers <= 0 { return fmt.Errorf("Server.MaxPeers must be > 0") } srv.quit = make(chan struct{}) - srv.peers = make([]*Peer, srv.MaxPeers) - srv.peerSlots = make(chan int, srv.MaxPeers) - srv.peerConnect = make(chan *peerAddr, outboundAddressPoolSize) - srv.peerDisconnect = make(chan *Peer) - if srv.newPeerFunc == nil { - srv.newPeerFunc = newServerPeer + srv.peers = make(map[discover.NodeID]*Peer) + srv.peerConnect = make(chan *discover.Node) + + if srv.handshakeFunc == nil { + srv.handshakeFunc = encHandshake } if srv.Blacklist == nil { srv.Blacklist = NewBlacklist() } - if srv.Dialer == nil { - srv.Dialer = &net.Dialer{Timeout: defaultDialTimeout} - } - if srv.ListenAddr != "" { if err := srv.startListening(); err != nil { return err } } + + // dial stuff + dt, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr) + if err != nil { + return err + } + srv.ntab = dt + if srv.Dialer == nil { + srv.Dialer = &net.Dialer{Timeout: defaultDialTimeout} + } if !srv.NoDial { - srv.wg.Add(1) + srv.loopWG.Add(1) go srv.dialLoop() } + if srv.NoDial && srv.ListenAddr == "" { srvlog.Warnln("I will be kind-of useless, neither dialing nor listening.") } - // make all slots available - for i := range srv.peers { - srv.peerSlots <- i - } - // note: discLoop is not part of WaitGroup - go srv.discLoop() srv.running = true return nil } @@ -205,10 +223,10 @@ func (srv *Server) startListening() error { srv.ListenAddr = listener.Addr().String() srv.laddr = listener.Addr().(*net.TCPAddr) srv.listener = listener - srv.wg.Add(1) + srv.loopWG.Add(1) go srv.listenLoop() if !srv.laddr.IP.IsLoopback() && srv.NAT != nil { - srv.wg.Add(1) + srv.loopWG.Add(1) go srv.natLoop(srv.laddr.Port) } return nil @@ -225,57 +243,41 @@ func (srv *Server) Stop() { srv.running = false srv.lock.Unlock() - srvlog.Infoln("Stopping server") + srvlog.Infoln("Stopping Server") + srv.ntab.Close() if srv.listener != nil { // this unblocks listener Accept srv.listener.Close() } close(srv.quit) - for _, peer := range srv.Peers() { - peer.Disconnect(DiscQuitting) - } - srv.wg.Wait() + srv.loopWG.Wait() - // wait till they actually disconnect - // this is checked by claiming all peerSlots. - // slots become available as the peers disconnect. - for i := 0; i < cap(srv.peerSlots); i++ { - <-srv.peerSlots - } - // terminate discLoop - close(srv.peerDisconnect) -} - -func (srv *Server) discLoop() { - for peer := range srv.peerDisconnect { - srv.removePeer(peer) + // No new peers can be added at this point because dialLoop and + // listenLoop are down. It is safe to call peerWG.Wait because + // peerWG.Add is not called outside of those loops. + for _, peer := range srv.peers { + peer.Disconnect(DiscQuitting) } + srv.peerWG.Wait() } // main loop for adding connections via listening func (srv *Server) listenLoop() { - defer srv.wg.Done() - + defer srv.loopWG.Done() srvlog.Infoln("Listening on", srv.listener.Addr()) for { - select { - case slot := <-srv.peerSlots: - srvlog.Debugf("grabbed slot %v for listening", slot) - conn, err := srv.listener.Accept() - if err != nil { - srv.peerSlots <- slot - return - } - srvlog.Debugf("Accepted conn %v (slot %d)\n", conn.RemoteAddr(), slot) - srv.addPeer(conn, nil, slot) - case <-srv.quit: + conn, err := srv.listener.Accept() + if err != nil { return } + srvlog.Debugf("Accepted conn %v\n", conn.RemoteAddr()) + srv.peerWG.Add(1) + go srv.startPeer(conn, nil) } } func (srv *Server) natLoop(port int) { - defer srv.wg.Done() + defer srv.loopWG.Done() for { srv.updatePortMapping(port) select { @@ -314,108 +316,131 @@ func (srv *Server) removePortMapping(port int) { } func (srv *Server) dialLoop() { - defer srv.wg.Done() - var ( - suggest chan *peerAddr - slot *int - slots = srv.peerSlots - ) + defer srv.loopWG.Done() + refresh := time.NewTicker(refreshPeersInterval) + defer refresh.Stop() + + srv.ntab.Bootstrap(srv.BootstrapNodes) + go srv.findPeers() + + dialed := make(chan *discover.Node) + dialing := make(map[discover.NodeID]bool) + + // TODO: limit number of active dials + // TODO: ensure only one findPeers goroutine is running + // TODO: pause findPeers when we're at capacity + for { select { - case i := <-slots: - // we need a peer in slot i, slot reserved - slot = &i - // now we can watch for candidate peers in the next loop - suggest = srv.peerConnect - // do not consume more until candidate peer is found - slots = nil - - case desc := <-suggest: - // candidate peer found, will dial out asyncronously - // if connection fails slot will be released - srvlog.DebugDetailf("dial %v (%v)", desc, *slot) - go srv.dialPeer(desc, *slot) - // we can watch if more peers needed in the next loop - slots = srv.peerSlots - // until then we dont care about candidate peers - suggest = nil + case <-refresh.C: - case <-srv.quit: - // give back the currently reserved slot - if slot != nil { - srv.peerSlots <- *slot + go srv.findPeers() + + case dest := <-srv.peerConnect: + srv.lock.Lock() + _, isconnected := srv.peers[dest.ID] + srv.lock.Unlock() + if isconnected || dialing[dest.ID] { + continue } + + dialing[dest.ID] = true + srv.peerWG.Add(1) + go func() { + srv.dialNode(dest) + // at this point, the peer has been added + // or discarded. either way, we're not dialing it anymore. + dialed <- dest + }() + + case dest := <-dialed: + delete(dialing, dest.ID) + + case <-srv.quit: + // TODO: maybe wait for active dials return } } } -// connect to peer via dial out -func (srv *Server) dialPeer(desc *peerAddr, slot int) { - srvlog.Debugf("Dialing %v (slot %d)\n", desc, slot) - conn, err := srv.Dialer.Dial(desc.Network(), desc.String()) +func (srv *Server) dialNode(dest *discover.Node) { + srvlog.Debugf("Dialing %v\n", dest.Addr) + conn, err := srv.Dialer.Dial("tcp", dest.Addr.String()) if err != nil { srvlog.DebugDetailf("dial error: %v", err) - srv.peerSlots <- slot return } - go srv.addPeer(conn, desc, slot) + srv.startPeer(conn, dest) } -// creates the new peer object and inserts it into its slot -func (srv *Server) addPeer(conn net.Conn, desc *peerAddr, slot int) *Peer { - srv.lock.Lock() - defer srv.lock.Unlock() - if !srv.running { +func (srv *Server) findPeers() { + far := srv.ntab.Self() + for i := range far { + far[i] = ^far[i] + } + closeToSelf := srv.ntab.Lookup(srv.ntab.Self()) + farFromSelf := srv.ntab.Lookup(far) + + for i := 0; i < len(closeToSelf) || i < len(farFromSelf); i++ { + if i < len(closeToSelf) { + srv.peerConnect <- closeToSelf[i] + } + if i < len(farFromSelf) { + srv.peerConnect <- farFromSelf[i] + } + } +} + +func (srv *Server) startPeer(conn net.Conn, dest *discover.Node) { + // TODO: I/O timeout, handle/store session token + remoteID, _, err := srv.handshakeFunc(conn, srv.PrivateKey, dest) + if err != nil { conn.Close() - srv.peerSlots <- slot // release slot - return nil + srvlog.Debugf("Encryption Handshake with %v failed: %v", conn.RemoteAddr(), err) + return + } + + ourID := srv.ntab.Self() + p := newPeer(conn, srv.Protocols, srv.Name, &ourID, &remoteID) + if ok, reason := srv.addPeer(remoteID, p); !ok { + p.Disconnect(reason) + return } - peer := srv.newPeerFunc(srv, conn, desc) - peer.slot = slot - srv.peers[slot] = peer - srv.peerCount++ - go func() { peer.loop(); srv.peerDisconnect <- peer }() - return peer + + srv.newPeerHook(p) + p.run() + srv.removePeer(p) } -// removes peer: sending disconnect msg, stop peer, remove rom list/table, release slot -func (srv *Server) removePeer(peer *Peer) { +func (srv *Server) addPeer(id discover.NodeID, p *Peer) (bool, DiscReason) { srv.lock.Lock() defer srv.lock.Unlock() - srvlog.Debugf("Removing %v (slot %v)\n", peer, peer.slot) - if srv.peers[peer.slot] != peer { - srvlog.Warnln("Invalid peer to remove:", peer) - return - } - // remove from list and index - srv.peerCount-- - srv.peers[peer.slot] = nil - // release slot to signal need for a new peer, last! - srv.peerSlots <- peer.slot + switch { + case !srv.running: + return false, DiscQuitting + case len(srv.peers) >= srv.MaxPeers: + return false, DiscTooManyPeers + case srv.peers[id] != nil: + return false, DiscAlreadyConnected + case srv.Blacklist.Exists(id[:]): + return false, DiscUselessPeer + case id == srv.ntab.Self(): + return false, DiscSelf + } + srvlog.Debugf("Adding %v\n", p) + srv.peers[id] = p + return true, 0 } -func (srv *Server) verifyPeer(addr *peerAddr) error { - if srv.Blacklist.Exists(addr.Pubkey) { - return errors.New("blacklisted") - } - if bytes.Equal(srv.Identity.Pubkey()[1:], addr.Pubkey) { - return newPeerError(errPubkeyForbidden, "not allowed to connect to srv") - } - srv.lock.RLock() - defer srv.lock.RUnlock() - for _, peer := range srv.peers { - if peer != nil { - id := peer.Identity() - if id != nil && bytes.Equal(id.Pubkey(), addr.Pubkey) { - return errors.New("already connected") - } - } - } - return nil +// removes peer: sending disconnect msg, stop peer, remove rom list/table, release slot +func (srv *Server) removePeer(p *Peer) { + srvlog.Debugf("Removing %v\n", p) + srv.lock.Lock() + delete(srv.peers, *p.remoteID) + srv.lock.Unlock() + srv.peerWG.Done() } -// TODO replace with "Set" type Blacklist interface { Get([]byte) (bool, error) Put([]byte) error diff --git a/p2p/server_test.go b/p2p/server_test.go index ceb89e3f7..a05e01014 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -2,19 +2,28 @@ package p2p import ( "bytes" + "crypto/ecdsa" "io" + "math/rand" "net" "sync" "testing" "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/discover" ) -func startTestServer(t *testing.T, pf peerFunc) *Server { +func startTestServer(t *testing.T, pf newPeerHook) *Server { server := &Server{ - Identity: &peerId{}, + Name: "test", MaxPeers: 10, ListenAddr: "127.0.0.1:0", - newPeerFunc: pf, + PrivateKey: newkey(), + newPeerHook: pf, + handshakeFunc: func(io.ReadWriter, *ecdsa.PrivateKey, *discover.Node) (id discover.NodeID, st []byte, err error) { + return randomID(), nil, err + }, } if err := server.Start(); err != nil { t.Fatalf("Could not start server: %v", err) @@ -27,16 +36,11 @@ func TestServerListen(t *testing.T) { // start the test server connected := make(chan *Peer) - srv := startTestServer(t, func(srv *Server, conn net.Conn, dialAddr *peerAddr) *Peer { - if conn == nil { + srv := startTestServer(t, func(p *Peer) { + if p == nil { t.Error("peer func called with nil conn") } - if dialAddr != nil { - t.Error("peer func called with non-nil dialAddr") - } - peer := newPeer(conn, nil, dialAddr) - connected <- peer - return peer + connected <- p }) defer close(connected) defer srv.Stop() @@ -50,9 +54,9 @@ func TestServerListen(t *testing.T) { select { case peer := <-connected: - if peer.conn.LocalAddr().String() != conn.RemoteAddr().String() { + if peer.LocalAddr().String() != conn.RemoteAddr().String() { t.Errorf("peer started with wrong conn: got %v, want %v", - peer.conn.LocalAddr(), conn.RemoteAddr()) + peer.LocalAddr(), conn.RemoteAddr()) } case <-time.After(1 * time.Second): t.Error("server did not accept within one second") @@ -62,7 +66,7 @@ func TestServerListen(t *testing.T) { func TestServerDial(t *testing.T) { defer testlog(t).detach() - // run a fake TCP server to handle the connection. + // run a one-shot TCP server to handle the connection. listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("could not setup listener: %v") @@ -72,41 +76,33 @@ func TestServerDial(t *testing.T) { go func() { conn, err := listener.Accept() if err != nil { - t.Error("acccept error:", err) + t.Error("accept error:", err) + return } conn.Close() accepted <- conn }() - // start the test server + // start the server connected := make(chan *Peer) - srv := startTestServer(t, func(srv *Server, conn net.Conn, dialAddr *peerAddr) *Peer { - if conn == nil { - t.Error("peer func called with nil conn") - } - peer := newPeer(conn, nil, dialAddr) - connected <- peer - return peer - }) + srv := startTestServer(t, func(p *Peer) { connected <- p }) defer close(connected) defer srv.Stop() - // tell the server to connect. - connAddr := newPeerAddr(listener.Addr(), nil) + // tell the server to connect + tcpAddr := listener.Addr().(*net.TCPAddr) + connAddr := &discover.Node{Addr: &net.UDPAddr{IP: tcpAddr.IP, Port: tcpAddr.Port}} srv.peerConnect <- connAddr select { case conn := <-accepted: select { case peer := <-connected: - if peer.conn.RemoteAddr().String() != conn.LocalAddr().String() { + if peer.RemoteAddr().String() != conn.LocalAddr().String() { t.Errorf("peer started with wrong conn: got %v, want %v", - peer.conn.RemoteAddr(), conn.LocalAddr()) - } - if peer.dialAddr != connAddr { - t.Errorf("peer started with wrong dialAddr: got %v, want %v", - peer.dialAddr, connAddr) + peer.RemoteAddr(), conn.LocalAddr()) } + // TODO: validate more fields case <-time.After(1 * time.Second): t.Error("server did not launch peer within one second") } @@ -118,16 +114,16 @@ func TestServerDial(t *testing.T) { func TestServerBroadcast(t *testing.T) { defer testlog(t).detach() + var connected sync.WaitGroup - srv := startTestServer(t, func(srv *Server, c net.Conn, dialAddr *peerAddr) *Peer { - peer := newPeer(c, []Protocol{discard}, dialAddr) - peer.startSubprotocols([]Cap{discard.cap()}) + srv := startTestServer(t, func(p *Peer) { + p.protocols = []Protocol{discard} + p.startSubprotocols([]Cap{discard.cap()}) connected.Done() - return peer }) defer srv.Stop() - // dial a bunch of conns + // create a few peers var conns = make([]net.Conn, 8) connected.Add(len(conns)) deadline := time.Now().Add(3 * time.Second) @@ -159,3 +155,18 @@ func TestServerBroadcast(t *testing.T) { } } } + +func newkey() *ecdsa.PrivateKey { + key, err := crypto.GenerateKey() + if err != nil { + panic("couldn't generate key: " + err.Error()) + } + return key +} + +func randomID() (id discover.NodeID) { + for i := range id { + id[i] = byte(rand.Intn(255)) + } + return id +} diff --git a/p2p/testlog_test.go b/p2p/testlog_test.go index 951d43243..c524c154c 100644 --- a/p2p/testlog_test.go +++ b/p2p/testlog_test.go @@ -15,7 +15,7 @@ func testlog(t *testing.T) testLogger { return l } -func (testLogger) GetLogLevel() logger.LogLevel { return logger.DebugLevel } +func (testLogger) GetLogLevel() logger.LogLevel { return logger.DebugDetailLevel } func (testLogger) SetLogLevel(logger.LogLevel) {} func (l testLogger) LogPrint(level logger.LogLevel, msg string) { diff --git a/p2p/testpoc7.go b/p2p/testpoc7.go deleted file mode 100644 index 63b996f79..000000000 --- a/p2p/testpoc7.go +++ /dev/null @@ -1,40 +0,0 @@ -// +build none - -package main - -import ( - "fmt" - "log" - "net" - "os" - - "github.com/ethereum/go-ethereum/crypto/secp256k1" - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/p2p" -) - -func main() { - logger.AddLogSystem(logger.NewStdLogSystem(os.Stdout, log.LstdFlags, logger.DebugLevel)) - - pub, _ := secp256k1.GenerateKeyPair() - srv := p2p.Server{ - MaxPeers: 10, - Identity: p2p.NewSimpleClientIdentity("test", "1.0", "", string(pub)), - ListenAddr: ":30303", - NAT: p2p.PMP(net.ParseIP("10.0.0.1")), - } - if err := srv.Start(); err != nil { - fmt.Println("could not start server:", err) - os.Exit(1) - } - - // add seed peers - seed, err := net.ResolveTCPAddr("tcp", "poc-7.ethdev.com:30303") - if err != nil { - fmt.Println("couldn't resolve:", err) - os.Exit(1) - } - srv.SuggestPeer(seed.IP, seed.Port, nil) - - select {} -} -- cgit v1.2.3 From 8564eb9f7efcc7a0e639f6a45d7e67037fedac5f Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 6 Feb 2015 14:40:53 +0100 Subject: p2p/discover: add node URL functions, distinguish TCP/UDP ports The discovery RPC protocol does not yet distinguish TCP and UDP ports. But it can't hurt to do so in our internal model. --- p2p/discover/node.go | 289 +++++++++++++++++++++++++++++++++++++++++++++ p2p/discover/node_test.go | 201 +++++++++++++++++++++++++++++++ p2p/discover/table.go | 197 +----------------------------- p2p/discover/table_test.go | 116 +----------------- p2p/discover/udp.go | 32 +++-- p2p/discover/udp_test.go | 13 +- p2p/server.go | 7 +- p2p/server_test.go | 3 +- 8 files changed, 532 insertions(+), 326 deletions(-) create mode 100644 p2p/discover/node.go create mode 100644 p2p/discover/node_test.go (limited to 'p2p') diff --git a/p2p/discover/node.go b/p2p/discover/node.go new file mode 100644 index 000000000..a49a2f547 --- /dev/null +++ b/p2p/discover/node.go @@ -0,0 +1,289 @@ +package discover + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "encoding/hex" + "errors" + "fmt" + "io" + "math/rand" + "net" + "net/url" + "strconv" + "strings" + "time" + + "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/ethereum/go-ethereum/rlp" +) + +// Node represents a host on the network. +type Node struct { + ID NodeID + IP net.IP + + DiscPort int // UDP listening port for discovery protocol + TCPPort int // TCP listening port for RLPx + + active time.Time +} + +func newNode(id NodeID, addr *net.UDPAddr) *Node { + return &Node{ + ID: id, + IP: addr.IP, + DiscPort: addr.Port, + TCPPort: addr.Port, + active: time.Now(), + } +} + +func (n *Node) isValid() bool { + // TODO: don't accept localhost, LAN addresses from internet hosts + return !n.IP.IsMulticast() && !n.IP.IsUnspecified() && n.TCPPort != 0 && n.DiscPort != 0 +} + +// The string representation of a Node is a URL. +// Please see ParseNode for a description of the format. +func (n *Node) String() string { + addr := net.TCPAddr{IP: n.IP, Port: n.TCPPort} + u := url.URL{ + Scheme: "enode", + User: url.User(fmt.Sprintf("%x", n.ID[:])), + Host: addr.String(), + } + if n.DiscPort != n.TCPPort { + u.RawQuery = "discport=" + strconv.Itoa(n.DiscPort) + } + return u.String() +} + +// ParseNode parses a node URL. +// +// A node URL has scheme "enode". +// +// The hexadecimal node ID is encoded in the username portion of the +// URL, separated from the host by an @ sign. The hostname can only be +// given as an IP address, DNS domain names are not allowed. The port +// in the host name section is the TCP listening port. If the TCP and +// UDP (discovery) ports differ, the UDP port is specified as query +// parameter "discport". +// +// In the following example, the node URL describes +// a node with IP address 10.3.58.6, TCP listening port 30303 +// and UDP discovery port 30301. +// +// enode://@10.3.58.6:30303?discport=30301 +func ParseNode(rawurl string) (*Node, error) { + var n Node + u, err := url.Parse(rawurl) + if u.Scheme != "enode" { + return nil, errors.New("invalid URL scheme, want \"enode\"") + } + if u.User == nil { + return nil, errors.New("does not contain node ID") + } + if n.ID, err = HexID(u.User.String()); err != nil { + return nil, fmt.Errorf("invalid node ID (%v)", err) + } + ip, port, err := net.SplitHostPort(u.Host) + if err != nil { + return nil, fmt.Errorf("invalid host: %v", err) + } + if n.IP = net.ParseIP(ip); n.IP == nil { + return nil, errors.New("invalid IP address") + } + if n.TCPPort, err = strconv.Atoi(port); err != nil { + return nil, errors.New("invalid port") + } + qv := u.Query() + if qv.Get("discport") == "" { + n.DiscPort = n.TCPPort + } else { + if n.DiscPort, err = strconv.Atoi(qv.Get("discport")); err != nil { + return nil, errors.New("invalid discport in query") + } + } + return &n, nil +} + +// MustParseNode parses a node URL. It panics if the URL is not valid. +func MustParseNode(rawurl string) *Node { + n, err := ParseNode(rawurl) + if err != nil { + panic("invalid node URL: " + err.Error()) + } + return n +} + +func (n Node) EncodeRLP(w io.Writer) error { + return rlp.Encode(w, rpcNode{IP: n.IP.String(), Port: uint16(n.TCPPort), ID: n.ID}) +} +func (n *Node) DecodeRLP(s *rlp.Stream) (err error) { + var ext rpcNode + if err = s.Decode(&ext); err == nil { + n.TCPPort = int(ext.Port) + n.DiscPort = int(ext.Port) + n.ID = ext.ID + if n.IP = net.ParseIP(ext.IP); n.IP == nil { + return errors.New("invalid IP string") + } + } + return err +} + +// NodeID is a unique identifier for each node. +// The node identifier is a marshaled elliptic curve public key. +type NodeID [512 / 8]byte + +// NodeID prints as a long hexadecimal number. +func (n NodeID) String() string { + return fmt.Sprintf("%#x", n[:]) +} + +// The Go syntax representation of a NodeID is a call to HexID. +func (n NodeID) GoString() string { + return fmt.Sprintf("discover.HexID(\"%#x\")", n[:]) +} + +// HexID converts a hex string to a NodeID. +// The string may be prefixed with 0x. +func HexID(in string) (NodeID, error) { + if strings.HasPrefix(in, "0x") { + in = in[2:] + } + var id NodeID + b, err := hex.DecodeString(in) + if err != nil { + return id, err + } else if len(b) != len(id) { + return id, fmt.Errorf("wrong length, need %d hex bytes", len(id)) + } + copy(id[:], b) + return id, nil +} + +// MustHexID converts a hex string to a NodeID. +// It panics if the string is not a valid NodeID. +func MustHexID(in string) NodeID { + id, err := HexID(in) + if err != nil { + panic(err) + } + return id +} + +// PubkeyID returns a marshaled representation of the given public key. +func PubkeyID(pub *ecdsa.PublicKey) NodeID { + var id NodeID + pbytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y) + if len(pbytes)-1 != len(id) { + panic(fmt.Errorf("need %d bit pubkey, got %d bits", (len(id)+1)*8, len(pbytes))) + } + copy(id[:], pbytes[1:]) + return id +} + +// recoverNodeID computes the public key used to sign the +// given hash from the signature. +func recoverNodeID(hash, sig []byte) (id NodeID, err error) { + pubkey, err := secp256k1.RecoverPubkey(hash, sig) + if err != nil { + return id, err + } + if len(pubkey)-1 != len(id) { + return id, fmt.Errorf("recovered pubkey has %d bits, want %d bits", len(pubkey)*8, (len(id)+1)*8) + } + for i := range id { + id[i] = pubkey[i+1] + } + return id, nil +} + +// distcmp compares the distances a->target and b->target. +// Returns -1 if a is closer to target, 1 if b is closer to target +// and 0 if they are equal. +func distcmp(target, a, b NodeID) int { + for i := range target { + da := a[i] ^ target[i] + db := b[i] ^ target[i] + if da > db { + return 1 + } else if da < db { + return -1 + } + } + return 0 +} + +// table of leading zero counts for bytes [0..255] +var lzcount = [256]int{ + 8, 7, 6, 6, 5, 5, 5, 5, + 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, + 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, + 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, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +} + +// logdist returns the logarithmic distance between a and b, log2(a ^ b). +func logdist(a, b NodeID) int { + lz := 0 + for i := range a { + x := a[i] ^ b[i] + if x == 0 { + lz += 8 + } else { + lz += lzcount[x] + break + } + } + return len(a)*8 - lz +} + +// randomID returns a random NodeID such that logdist(a, b) == n +func randomID(a NodeID, n int) (b NodeID) { + if n == 0 { + return a + } + // flip bit at position n, fill the rest with random bits + b = a + pos := len(a) - n/8 - 1 + bit := byte(0x01) << (byte(n%8) - 1) + if bit == 0 { + pos++ + bit = 0x80 + } + b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits + for i := pos + 1; i < len(a); i++ { + b[i] = byte(rand.Intn(255)) + } + return b +} diff --git a/p2p/discover/node_test.go b/p2p/discover/node_test.go new file mode 100644 index 000000000..ae82ae4f1 --- /dev/null +++ b/p2p/discover/node_test.go @@ -0,0 +1,201 @@ +package discover + +import ( + "math/big" + "math/rand" + "net" + "reflect" + "testing" + "testing/quick" + "time" + + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + quickrand = rand.New(rand.NewSource(time.Now().Unix())) + quickcfg = &quick.Config{MaxCount: 5000, Rand: quickrand} +) + +var parseNodeTests = []struct { + rawurl string + wantError string + wantResult *Node +}{ + { + rawurl: "http://foobar", + wantError: `invalid URL scheme, want "enode"`, + }, + { + rawurl: "enode://foobar", + wantError: `does not contain node ID`, + }, + { + rawurl: "enode://01010101@123.124.125.126:3", + wantError: `invalid node ID (wrong length, need 64 hex bytes)`, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3", + wantError: `invalid IP address`, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo", + wantError: `invalid port`, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo", + wantError: `invalid discport in query`, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150", + wantResult: &Node{ + ID: MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + IP: net.ParseIP("127.0.0.1"), + DiscPort: 52150, + TCPPort: 52150, + }, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150", + wantResult: &Node{ + ID: MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + IP: net.ParseIP("::"), + DiscPort: 52150, + TCPPort: 52150, + }, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=223344", + wantResult: &Node{ + ID: MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + IP: net.ParseIP("127.0.0.1"), + DiscPort: 223344, + TCPPort: 52150, + }, + }, +} + +func TestParseNode(t *testing.T) { + for i, test := range parseNodeTests { + n, err := ParseNode(test.rawurl) + if err == nil && test.wantError != "" { + t.Errorf("test %d: got nil error, expected %#q", i, test.wantError) + continue + } + if err != nil && err.Error() != test.wantError { + t.Errorf("test %d: got error %#q, expected %#q", i, err.Error(), test.wantError) + continue + } + if !reflect.DeepEqual(n, test.wantResult) { + t.Errorf("test %d: result mismatch:\ngot: %#v, want: %#v", i, n, test.wantResult) + } + } +} + +func TestNodeString(t *testing.T) { + for i, test := range parseNodeTests { + if test.wantError != "" { + continue + } + str := test.wantResult.String() + if str != test.rawurl { + t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl) + } + } +} + +func TestHexID(t *testing.T) { + ref := NodeID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} + id1 := MustHexID("0x000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + id2 := MustHexID("000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + + if id1 != ref { + t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:]) + } + if id2 != ref { + t.Errorf("wrong id2\ngot %v\nwant %v", id2[:], ref[:]) + } +} + +func TestNodeID_recover(t *testing.T) { + prv := newkey() + hash := make([]byte, 32) + sig, err := crypto.Sign(hash, prv) + if err != nil { + t.Fatalf("signing error: %v", err) + } + + pub := PubkeyID(&prv.PublicKey) + recpub, err := recoverNodeID(hash, sig) + if err != nil { + t.Fatalf("recovery error: %v", err) + } + if pub != recpub { + t.Errorf("recovered wrong pubkey:\ngot: %v\nwant: %v", recpub, pub) + } +} + +func TestNodeID_distcmp(t *testing.T) { + distcmpBig := func(target, a, b NodeID) int { + tbig := new(big.Int).SetBytes(target[:]) + abig := new(big.Int).SetBytes(a[:]) + bbig := new(big.Int).SetBytes(b[:]) + return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig)) + } + if err := quick.CheckEqual(distcmp, distcmpBig, quickcfg); err != nil { + t.Error(err) + } +} + +// the random tests is likely to miss the case where they're equal. +func TestNodeID_distcmpEqual(t *testing.T) { + base := NodeID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + x := NodeID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} + if distcmp(base, x, x) != 0 { + t.Errorf("distcmp(base, x, x) != 0") + } +} + +func TestNodeID_logdist(t *testing.T) { + logdistBig := func(a, b NodeID) int { + abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:]) + return new(big.Int).Xor(abig, bbig).BitLen() + } + if err := quick.CheckEqual(logdist, logdistBig, quickcfg); err != nil { + t.Error(err) + } +} + +// the random tests is likely to miss the case where they're equal. +func TestNodeID_logdistEqual(t *testing.T) { + x := NodeID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + if logdist(x, x) != 0 { + t.Errorf("logdist(x, x) != 0") + } +} + +func TestNodeID_randomID(t *testing.T) { + // we don't use quick.Check here because its output isn't + // very helpful when the test fails. + for i := 0; i < quickcfg.MaxCount; i++ { + a := gen(NodeID{}, quickrand).(NodeID) + dist := quickrand.Intn(len(NodeID{}) * 8) + result := randomID(a, dist) + actualdist := logdist(result, a) + + if dist != actualdist { + t.Log("a: ", a) + t.Log("result:", result) + t.Fatalf("#%d: distance of result is %d, want %d", i, actualdist, dist) + } + } +} + +func (NodeID) Generate(rand *rand.Rand, size int) reflect.Value { + var id NodeID + m := rand.Intn(len(id)) + for i := len(id) - 1; i > m; i-- { + id[i] = byte(rand.Uint32()) + } + return reflect.ValueOf(id) +} diff --git a/p2p/discover/table.go b/p2p/discover/table.go index ea9680404..6025507eb 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -7,20 +7,10 @@ package discover import ( - "crypto/ecdsa" - "crypto/elliptic" - "encoding/hex" - "fmt" - "io" - "math/rand" "net" "sort" - "strings" "sync" "time" - - "github.com/ethereum/go-ethereum/crypto/secp256k1" - "github.com/ethereum/go-ethereum/rlp" ) const ( @@ -53,36 +43,10 @@ type bucket struct { entries []*Node } -// Node represents node metadata that is stored in the table. -type Node struct { - Addr *net.UDPAddr - ID NodeID - - active time.Time -} - -type rpcNode struct { - IP string - Port uint16 - ID NodeID -} - -func (n Node) EncodeRLP(w io.Writer) error { - return rlp.Encode(w, rpcNode{IP: n.Addr.IP.String(), Port: uint16(n.Addr.Port), ID: n.ID}) -} -func (n *Node) DecodeRLP(s *rlp.Stream) (err error) { - var ext rpcNode - if err = s.Decode(&ext); err == nil { - n.Addr = &net.UDPAddr{IP: net.ParseIP(ext.IP), Port: int(ext.Port)} - n.ID = ext.ID - } - return err -} - func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr) *Table { - tab := &Table{net: t, self: &Node{ID: ourID, Addr: ourAddr}} + tab := &Table{net: t, self: newNode(ourID, ourAddr)} for i := range tab.buckets { - tab.buckets[i] = &bucket{} + tab.buckets[i] = new(bucket) } return tab } @@ -217,7 +181,7 @@ func (tab *Table) len() (n int) { func (tab *Table) bumpOrAdd(node NodeID, from *net.UDPAddr) (n *Node) { b := tab.buckets[logdist(tab.self.ID, node)] if n = b.bump(node); n == nil { - n = &Node{ID: node, Addr: from, active: time.Now()} + n = newNode(node, from) if len(b.entries) == bucketSize { tab.pingReplace(n, b) } else { @@ -238,6 +202,7 @@ func (tab *Table) pingReplace(n *Node, b *bucket) { tab.mutex.Lock() if len(b.entries) > 0 && b.entries[len(b.entries)-1] == old { // slide down other entries and put the new one in front. + // TODO: insert in correct position to keep the order copy(b.entries[1:], b.entries) b.entries[0] = n } @@ -312,157 +277,3 @@ func (h *nodesByDistance) push(n *Node, maxElems int) { h.entries[ix] = n } } - -// NodeID is a unique identifier for each node. -// The node identifier is a marshaled elliptic curve public key. -type NodeID [512 / 8]byte - -// NodeID prints as a long hexadecimal number. -func (n NodeID) String() string { - return fmt.Sprintf("%#x", n[:]) -} - -// The Go syntax representation of a NodeID is a call to HexID. -func (n NodeID) GoString() string { - return fmt.Sprintf("HexID(\"%#x\")", n[:]) -} - -// HexID converts a hex string to a NodeID. -// The string may be prefixed with 0x. -func HexID(in string) (NodeID, error) { - if strings.HasPrefix(in, "0x") { - in = in[2:] - } - var id NodeID - b, err := hex.DecodeString(in) - if err != nil { - return id, err - } else if len(b) != len(id) { - return id, fmt.Errorf("wrong length, need %d hex bytes", len(id)) - } - copy(id[:], b) - return id, nil -} - -// MustHexID converts a hex string to a NodeID. -// It panics if the string is not a valid NodeID. -func MustHexID(in string) NodeID { - id, err := HexID(in) - if err != nil { - panic(err) - } - return id -} - -func PubkeyID(pub *ecdsa.PublicKey) NodeID { - var id NodeID - pbytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y) - if len(pbytes)-1 != len(id) { - panic(fmt.Errorf("invalid key: need %d bit pubkey, got %d bits", (len(id)+1)*8, len(pbytes))) - } - copy(id[:], pbytes[1:]) - return id -} - -// recoverNodeID computes the public key used to sign the -// given hash from the signature. -func recoverNodeID(hash, sig []byte) (id NodeID, err error) { - pubkey, err := secp256k1.RecoverPubkey(hash, sig) - if err != nil { - return id, err - } - if len(pubkey)-1 != len(id) { - return id, fmt.Errorf("recovered pubkey has %d bits, want %d bits", len(pubkey)*8, (len(id)+1)*8) - } - for i := range id { - id[i] = pubkey[i+1] - } - return id, nil -} - -// distcmp compares the distances a->target and b->target. -// Returns -1 if a is closer to target, 1 if b is closer to target -// and 0 if they are equal. -func distcmp(target, a, b NodeID) int { - for i := range target { - da := a[i] ^ target[i] - db := b[i] ^ target[i] - if da > db { - return 1 - } else if da < db { - return -1 - } - } - return 0 -} - -// table of leading zero counts for bytes [0..255] -var lzcount = [256]int{ - 8, 7, 6, 6, 5, 5, 5, 5, - 4, 4, 4, 4, 4, 4, 4, 4, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 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, - 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, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, -} - -// logdist returns the logarithmic distance between a and b, log2(a ^ b). -func logdist(a, b NodeID) int { - lz := 0 - for i := range a { - x := a[i] ^ b[i] - if x == 0 { - lz += 8 - } else { - lz += lzcount[x] - break - } - } - return len(a)*8 - lz -} - -// randomID returns a random NodeID such that logdist(a, b) == n -func randomID(a NodeID, n int) (b NodeID) { - if n == 0 { - return a - } - // flip bit at position n, fill the rest with random bits - b = a - pos := len(a) - n/8 - 1 - bit := byte(0x01) << (byte(n%8) - 1) - if bit == 0 { - pos++ - bit = 0x80 - } - b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits - for i := pos + 1; i < len(a); i++ { - b[i] = byte(rand.Intn(255)) - } - return b -} diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index 90f89b147..9b05ce7f4 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -4,7 +4,6 @@ import ( "crypto/ecdsa" "errors" "fmt" - "math/big" "math/rand" "net" "reflect" @@ -15,107 +14,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -var ( - quickrand = rand.New(rand.NewSource(time.Now().Unix())) - quickcfg = &quick.Config{MaxCount: 5000, Rand: quickrand} -) - -func TestHexID(t *testing.T) { - ref := NodeID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} - id1 := MustHexID("0x000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") - id2 := MustHexID("000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") - - if id1 != ref { - t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:]) - } - if id2 != ref { - t.Errorf("wrong id2\ngot %v\nwant %v", id2[:], ref[:]) - } -} - -func TestNodeID_recover(t *testing.T) { - prv := newkey() - hash := make([]byte, 32) - sig, err := crypto.Sign(hash, prv) - if err != nil { - t.Fatalf("signing error: %v", err) - } - - pub := PubkeyID(&prv.PublicKey) - recpub, err := recoverNodeID(hash, sig) - if err != nil { - t.Fatalf("recovery error: %v", err) - } - if pub != recpub { - t.Errorf("recovered wrong pubkey:\ngot: %v\nwant: %v", recpub, pub) - } -} - -func TestNodeID_distcmp(t *testing.T) { - distcmpBig := func(target, a, b NodeID) int { - tbig := new(big.Int).SetBytes(target[:]) - abig := new(big.Int).SetBytes(a[:]) - bbig := new(big.Int).SetBytes(b[:]) - return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig)) - } - if err := quick.CheckEqual(distcmp, distcmpBig, quickcfg); err != nil { - t.Error(err) - } -} - -// the random tests is likely to miss the case where they're equal. -func TestNodeID_distcmpEqual(t *testing.T) { - base := NodeID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - x := NodeID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} - if distcmp(base, x, x) != 0 { - t.Errorf("distcmp(base, x, x) != 0") - } -} - -func TestNodeID_logdist(t *testing.T) { - logdistBig := func(a, b NodeID) int { - abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:]) - return new(big.Int).Xor(abig, bbig).BitLen() - } - if err := quick.CheckEqual(logdist, logdistBig, quickcfg); err != nil { - t.Error(err) - } -} - -// the random tests is likely to miss the case where they're equal. -func TestNodeID_logdistEqual(t *testing.T) { - x := NodeID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - if logdist(x, x) != 0 { - t.Errorf("logdist(x, x) != 0") - } -} - -func TestNodeID_randomID(t *testing.T) { - // we don't use quick.Check here because its output isn't - // very helpful when the test fails. - for i := 0; i < quickcfg.MaxCount; i++ { - a := gen(NodeID{}, quickrand).(NodeID) - dist := quickrand.Intn(len(NodeID{}) * 8) - result := randomID(a, dist) - actualdist := logdist(result, a) - - if dist != actualdist { - t.Log("a: ", a) - t.Log("result:", result) - t.Fatalf("#%d: distance of result is %d, want %d", i, actualdist, dist) - } - } -} - -func (NodeID) Generate(rand *rand.Rand, size int) reflect.Value { - var id NodeID - m := rand.Intn(len(id)) - for i := len(id) - 1; i > m; i-- { - id[i] = byte(rand.Uint32()) - } - return reflect.ValueOf(id) -} - func TestTable_bumpOrAddPingReplace(t *testing.T) { pingC := make(pingC) tab := newTable(pingC, NodeID{}, &net.UDPAddr{}) @@ -123,7 +21,7 @@ func TestTable_bumpOrAddPingReplace(t *testing.T) { // this bumpOrAdd should not replace the last node // because the node replies to ping. - new := tab.bumpOrAdd(randomID(tab.self.ID, 200), nil) + new := tab.bumpOrAdd(randomID(tab.self.ID, 200), &net.UDPAddr{}) pinged := <-pingC if pinged != last.ID { @@ -149,7 +47,7 @@ func TestTable_bumpOrAddPingTimeout(t *testing.T) { // this bumpOrAdd should replace the last node // because the node does not reply to ping. - new := tab.bumpOrAdd(randomID(tab.self.ID, 200), nil) + new := tab.bumpOrAdd(randomID(tab.self.ID, 200), &net.UDPAddr{}) // wait for async bucket update. damn. this needs to go away. time.Sleep(2 * time.Millisecond) @@ -329,19 +227,17 @@ type findnodeOracle struct { } func (t findnodeOracle) findnode(n *Node, target NodeID) ([]*Node, error) { - t.t.Logf("findnode query at dist %d", n.Addr.Port) + t.t.Logf("findnode query at dist %d", n.DiscPort) // current log distance is encoded in port number var result []*Node - switch port := n.Addr.Port; port { + switch n.DiscPort { case 0: panic("query to node at distance 0") - case 1: - result = append(result, &Node{ID: t.target, Addr: &net.UDPAddr{Port: 0}}) default: // TODO: add more randomness to distances - port-- + next := n.DiscPort - 1 for i := 0; i < bucketSize; i++ { - result = append(result, &Node{ID: randomID(t.target, port), Addr: &net.UDPAddr{Port: port}}) + result = append(result, &Node{ID: randomID(t.target, next), DiscPort: next}) } } return result, nil diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go index 449139c1d..67e349aa4 100644 --- a/p2p/discover/udp.go +++ b/p2p/discover/udp.go @@ -69,6 +69,12 @@ type ( } ) +type rpcNode struct { + IP string + Port uint16 + ID NodeID +} + // udp implements the RPC protocol. type udp struct { conn *net.UDPConn @@ -121,7 +127,7 @@ func ListenUDP(priv *ecdsa.PrivateKey, laddr string) (*Table, error) { return nil, err } net.Table = newTable(net, PubkeyID(&priv.PublicKey), realaddr) - log.Debugf("Listening on %v, my ID %x\n", realaddr, net.self.ID[:]) + log.Debugf("Listening, %v\n", net.self) return net.Table, nil } @@ -159,8 +165,8 @@ func (t *udp) ping(e *Node) error { // TODO: maybe check for ReplyTo field in callback to measure RTT errc := t.pending(e.ID, pongPacket, func(interface{}) bool { return true }) t.send(e, pingPacket, ping{ - IP: t.self.Addr.String(), - Port: uint16(t.self.Addr.Port), + IP: t.self.IP.String(), + Port: uint16(t.self.TCPPort), Expiration: uint64(time.Now().Add(expiration).Unix()), }) return <-errc @@ -176,7 +182,7 @@ func (t *udp) findnode(to *Node, target NodeID) ([]*Node, error) { for i := 0; i < len(reply.Nodes); i++ { nreceived++ n := reply.Nodes[i] - if validAddr(n.Addr) && n.ID != t.self.ID { + if n.ID != t.self.ID && n.isValid() { nodes = append(nodes, n) } } @@ -191,10 +197,6 @@ func (t *udp) findnode(to *Node, target NodeID) ([]*Node, error) { return nodes, err } -func validAddr(a *net.UDPAddr) bool { - return !a.IP.IsMulticast() && !a.IP.IsUnspecified() && a.Port != 0 -} - // pending adds a reply callback to the pending reply queue. // see the documentation of type pending for a detailed explanation. func (t *udp) pending(id NodeID, ptype byte, callback func(interface{}) bool) <-chan error { @@ -302,8 +304,9 @@ func (t *udp) send(to *Node, ptype byte, req interface{}) error { // the future. copy(packet, crypto.Sha3(packet[macSize:])) - log.DebugDetailf(">>> %v %T %v\n", to.Addr, req, req) - if _, err = t.conn.WriteToUDP(packet, to.Addr); err != nil { + toaddr := &net.UDPAddr{IP: to.IP, Port: to.DiscPort} + log.DebugDetailf(">>> %v %T %v\n", toaddr, req, req) + if _, err = t.conn.WriteToUDP(packet, toaddr); err != nil { log.DebugDetailln("UDP send failed:", err) } return err @@ -365,11 +368,14 @@ func (req *ping) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) er return errExpired } t.mutex.Lock() - // Note: we're ignoring the provided IP/Port right now. - e := t.bumpOrAdd(fromID, from) + // Note: we're ignoring the provided IP address right now + n := t.bumpOrAdd(fromID, from) + if req.Port != 0 { + n.TCPPort = int(req.Port) + } t.mutex.Unlock() - t.send(e, pongPacket, pong{ + t.send(n, pongPacket, pong{ ReplyTok: mac, Expiration: uint64(time.Now().Add(expiration).Unix()), }) diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go index 8ed6ec9c0..94680d6fc 100644 --- a/p2p/discover/udp_test.go +++ b/p2p/discover/udp_test.go @@ -4,6 +4,7 @@ import ( logpkg "log" "net" "os" + "reflect" "testing" "time" @@ -11,7 +12,7 @@ import ( ) func init() { - logger.AddLogSystem(logger.NewStdLogSystem(os.Stdout, logpkg.LstdFlags, logger.DebugLevel)) + logger.AddLogSystem(logger.NewStdLogSystem(os.Stdout, logpkg.LstdFlags, logger.ErrorLevel)) } func TestUDP_ping(t *testing.T) { @@ -52,7 +53,7 @@ func TestUDP_findnode(t *testing.T) { defer n1.Close() defer n2.Close() - entry := &Node{ID: NodeID{1}, Addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 15}} + entry := MustParseNode("enode://9d8a19597e312ef32d76e6b4903bb43d7bcd892d17b769d30b404bd3a4c2dca6c86184b17d0fdeeafe3b01e0e912d990ddc853db3f325d5419f31446543c30be@127.0.0.1:54194") n2.add([]*Node{entry}) target := randomID(n1.self.ID, 100) @@ -60,7 +61,7 @@ func TestUDP_findnode(t *testing.T) { if len(result) != 1 { t.Fatalf("wrong number of results: got %d, want 1", len(result)) } - if result[0].ID != entry.ID { + if !reflect.DeepEqual(result[0], entry) { t.Errorf("wrong result: got %v, want %v", result[0], entry) } } @@ -103,8 +104,10 @@ func TestUDP_findnodeMultiReply(t *testing.T) { nodes := make([]*Node, bucketSize) for i := range nodes { nodes[i] = &Node{ - Addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: i + 1}, - ID: randomID(n2.self.ID, i+1), + IP: net.IP{1, 2, 3, 4}, + DiscPort: i + 1, + TCPPort: i + 1, + ID: randomID(n2.self.ID, i+1), } } diff --git a/p2p/server.go b/p2p/server.go index 9b8030541..87be97a2f 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -136,7 +136,7 @@ func (srv *Server) PeerCount() int { // SuggestPeer creates a connection to the given Node if it // is not already connected. func (srv *Server) SuggestPeer(ip net.IP, port int, id discover.NodeID) { - srv.peerConnect <- &discover.Node{ID: id, Addr: &net.UDPAddr{IP: ip, Port: port}} + srv.peerConnect <- &discover.Node{ID: id, IP: ip, TCPPort: port} } // Broadcast sends an RLP-encoded message to all connected peers. @@ -364,8 +364,9 @@ func (srv *Server) dialLoop() { } func (srv *Server) dialNode(dest *discover.Node) { - srvlog.Debugf("Dialing %v\n", dest.Addr) - conn, err := srv.Dialer.Dial("tcp", dest.Addr.String()) + addr := &net.TCPAddr{IP: dest.IP, Port: dest.TCPPort} + srvlog.Debugf("Dialing %v\n", dest) + conn, err := srv.Dialer.Dial("tcp", addr.String()) if err != nil { srvlog.DebugDetailf("dial error: %v", err) return diff --git a/p2p/server_test.go b/p2p/server_test.go index a05e01014..89300cf1c 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -91,8 +91,7 @@ func TestServerDial(t *testing.T) { // tell the server to connect tcpAddr := listener.Addr().(*net.TCPAddr) - connAddr := &discover.Node{Addr: &net.UDPAddr{IP: tcpAddr.IP, Port: tcpAddr.Port}} - srv.peerConnect <- connAddr + srv.peerConnect <- &discover.Node{IP: tcpAddr.IP, TCPPort: tcpAddr.Port} select { case conn := <-accepted: -- cgit v1.2.3 From e34d1341022a51d8a86c4836c91e4e0ded888d27 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Sat, 7 Feb 2015 00:13:22 +0100 Subject: p2p: fixes for actual connections The unit test hooks were turned on 'in production'. --- p2p/message.go | 4 ++-- p2p/peer.go | 37 +++++++++++++++++++++---------------- p2p/peer_error.go | 2 +- p2p/peer_test.go | 19 ++++++++++--------- p2p/server.go | 4 +++- p2p/server_test.go | 1 + 6 files changed, 38 insertions(+), 29 deletions(-) (limited to 'p2p') diff --git a/p2p/message.go b/p2p/message.go index 6521d09c2..dfc33f349 100644 --- a/p2p/message.go +++ b/p2p/message.go @@ -174,10 +174,10 @@ func (rw *frameRW) ReadMsg() (msg Msg, err error) { // read magic and payload size start := make([]byte, 8) if _, err = io.ReadFull(rw.bufconn, start); err != nil { - return msg, newPeerError(errRead, "%v", err) + return msg, err } if !bytes.HasPrefix(start, magicToken) { - return msg, newPeerError(errMagicTokenMismatch, "got %x, want %x", start[:4], magicToken) + return msg, fmt.Errorf("bad magic token %x", start[:4], magicToken) } size := binary.BigEndian.Uint32(start[4:]) diff --git a/p2p/peer.go b/p2p/peer.go index 1fa8264a3..b61cf96da 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -1,6 +1,7 @@ package p2p import ( + "errors" "fmt" "io" "io/ioutil" @@ -71,7 +72,8 @@ type Peer struct { runlock sync.RWMutex // protects running running map[string]*proto - protocolHandshakeEnabled bool + // disables protocol handshake, for testing + noHandshake bool protoWG sync.WaitGroup protoErr chan error @@ -134,11 +136,11 @@ func (p *Peer) Disconnect(reason DiscReason) { // String implements fmt.Stringer. func (p *Peer) String() string { - return fmt.Sprintf("Peer %.8x %v", p.remoteID, p.RemoteAddr()) + return fmt.Sprintf("Peer %.8x %v", p.remoteID[:], p.RemoteAddr()) } func newPeer(conn net.Conn, protocols []Protocol, ourName string, ourID, remoteID *discover.NodeID) *Peer { - logtag := fmt.Sprintf("Peer %.8x %v", remoteID, conn.RemoteAddr()) + logtag := fmt.Sprintf("Peer %.8x %v", remoteID[:], conn.RemoteAddr()) return &Peer{ Logger: logger.NewLogger(logtag), rw: newFrameRW(conn, msgWriteTimeout), @@ -164,33 +166,35 @@ func (p *Peer) run() DiscReason { var readErr = make(chan error, 1) defer p.closeProtocols() defer close(p.closed) - defer p.rw.Close() - // start the read loop go func() { readErr <- p.readLoop() }() - if p.protocolHandshakeEnabled { + if !p.noHandshake { if err := writeProtocolHandshake(p.rw, p.ourName, *p.ourID, p.protocols); err != nil { p.DebugDetailf("Protocol handshake error: %v\n", err) + p.rw.Close() return DiscProtocolError } } - // wait for an error or disconnect + // Wait for an error or disconnect. var reason DiscReason select { case err := <-readErr: // We rely on protocols to abort if there is a write error. It // might be more robust to handle them here as well. p.DebugDetailf("Read error: %v\n", err) - reason = DiscNetworkError + p.rw.Close() + return DiscNetworkError + case err := <-p.protoErr: reason = discReasonForError(err) case reason = <-p.disc: } - if reason != DiscNetworkError { - p.politeDisconnect(reason) - } + p.politeDisconnect(reason) + + // Wait for readLoop. It will end because conn is now closed. + <-readErr p.Debugf("Disconnected: %v\n", reason) return reason } @@ -198,9 +202,9 @@ func (p *Peer) run() DiscReason { func (p *Peer) politeDisconnect(reason DiscReason) { done := make(chan struct{}) go func() { - // send reason EncodeMsg(p.rw, discMsg, uint(reason)) - // discard any data that might arrive + // Wait for the other side to close the connection. + // Discard any data that they send until then. io.Copy(ioutil.Discard, p.rw) close(done) }() @@ -208,10 +212,11 @@ func (p *Peer) politeDisconnect(reason DiscReason) { case <-done: case <-time.After(disconnectGracePeriod): } + p.rw.Close() } func (p *Peer) readLoop() error { - if p.protocolHandshakeEnabled { + if !p.noHandshake { if err := readProtocolHandshake(p, p.rw); err != nil { return err } @@ -264,7 +269,7 @@ func readProtocolHandshake(p *Peer, rw MsgReadWriter) error { return newPeerError(errProtocolBreach, "expected handshake, got %x", msg.Code) } if msg.Size > baseProtocolMaxMsgSize { - return newPeerError(errMisc, "message too big") + return newPeerError(errInvalidMsg, "message too big") } var hs handshake if err := msg.Decode(&hs); err != nil { @@ -326,7 +331,7 @@ func (p *Peer) startProto(offset uint64, impl Protocol) *proto { err := impl.Run(p, rw) if err == nil { p.DebugDetailf("Protocol %s/%d returned\n", impl.Name, impl.Version) - err = newPeerError(errMisc, "protocol returned") + err = errors.New("protocol returned") } else { p.DebugDetailf("Protocol %s/%d error: %v\n", impl.Name, impl.Version, err) } diff --git a/p2p/peer_error.go b/p2p/peer_error.go index 9133768f9..0ff4f4b43 100644 --- a/p2p/peer_error.go +++ b/p2p/peer_error.go @@ -123,7 +123,7 @@ func discReasonForError(err error) DiscReason { return DiscProtocolError case errPingTimeout: return DiscReadTimeout - case errRead, errWrite, errMisc: + case errRead, errWrite: return DiscNetworkError default: return DiscSubprotocolError diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 76d856d3e..68c9910a2 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -30,10 +30,10 @@ var discard = Protocol{ }, } -func testPeer(handshake bool, protos []Protocol) (*frameRW, *Peer, <-chan DiscReason) { +func testPeer(noHandshake bool, protos []Protocol) (*frameRW, *Peer, <-chan DiscReason) { conn1, conn2 := net.Pipe() peer := newPeer(conn1, protos, "name", &discover.NodeID{}, &discover.NodeID{}) - peer.protocolHandshakeEnabled = handshake + peer.noHandshake = noHandshake errc := make(chan DiscReason, 1) go func() { errc <- peer.run() }() return newFrameRW(conn2, msgWriteTimeout), peer, errc @@ -61,7 +61,7 @@ func TestPeerProtoReadMsg(t *testing.T) { }, } - rw, peer, errc := testPeer(false, []Protocol{proto}) + rw, peer, errc := testPeer(true, []Protocol{proto}) defer rw.Close() peer.startSubprotocols([]Cap{proto.cap()}) @@ -100,7 +100,7 @@ func TestPeerProtoReadLargeMsg(t *testing.T) { }, } - rw, peer, errc := testPeer(false, []Protocol{proto}) + rw, peer, errc := testPeer(true, []Protocol{proto}) defer rw.Close() peer.startSubprotocols([]Cap{proto.cap()}) @@ -130,7 +130,7 @@ func TestPeerProtoEncodeMsg(t *testing.T) { return nil }, } - rw, peer, _ := testPeer(false, []Protocol{proto}) + rw, peer, _ := testPeer(true, []Protocol{proto}) defer rw.Close() peer.startSubprotocols([]Cap{proto.cap()}) @@ -142,7 +142,7 @@ func TestPeerProtoEncodeMsg(t *testing.T) { func TestPeerWriteForBroadcast(t *testing.T) { defer testlog(t).detach() - rw, peer, peerErr := testPeer(false, []Protocol{discard}) + rw, peer, peerErr := testPeer(true, []Protocol{discard}) defer rw.Close() peer.startSubprotocols([]Cap{discard.cap()}) @@ -179,7 +179,7 @@ func TestPeerWriteForBroadcast(t *testing.T) { func TestPeerPing(t *testing.T) { defer testlog(t).detach() - rw, _, _ := testPeer(false, nil) + rw, _, _ := testPeer(true, nil) defer rw.Close() if err := EncodeMsg(rw, pingMsg); err != nil { t.Fatal(err) @@ -192,7 +192,7 @@ func TestPeerPing(t *testing.T) { func TestPeerDisconnect(t *testing.T) { defer testlog(t).detach() - rw, _, disc := testPeer(false, nil) + rw, _, disc := testPeer(true, nil) defer rw.Close() if err := EncodeMsg(rw, discMsg, DiscQuitting); err != nil { t.Fatal(err) @@ -233,7 +233,7 @@ func TestPeerHandshake(t *testing.T) { {Name: "c", Version: 3, Length: 1, Run: run}, {Name: "d", Version: 4, Length: 1, Run: run}, } - rw, p, disc := testPeer(true, protocols) + rw, p, disc := testPeer(false, protocols) p.remoteID = remote.ourID defer rw.Close() @@ -269,6 +269,7 @@ func TestPeerHandshake(t *testing.T) { } close(stop) + expectMsg(rw, discMsg, nil) t.Logf("disc reason: %v", <-disc) } diff --git a/p2p/server.go b/p2p/server.go index 87be97a2f..c6d7fc2e8 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -408,7 +408,9 @@ func (srv *Server) startPeer(conn net.Conn, dest *discover.Node) { return } - srv.newPeerHook(p) + if srv.newPeerHook != nil { + srv.newPeerHook(p) + } p.run() srv.removePeer(p) } diff --git a/p2p/server_test.go b/p2p/server_test.go index 89300cf1c..d1e1640fb 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -118,6 +118,7 @@ func TestServerBroadcast(t *testing.T) { srv := startTestServer(t, func(p *Peer) { p.protocols = []Protocol{discard} p.startSubprotocols([]Cap{discard.cap()}) + p.noHandshake = true connected.Done() }) defer srv.Stop() -- cgit v1.2.3 From 2cf4fed11b01bb99e08b838f7df2b9396f42f758 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Sat, 7 Feb 2015 00:15:04 +0100 Subject: cmd/mist, eth, javascript, p2p: use Node URLs for peer suggestions --- p2p/server.go | 4 ++-- p2p/server_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'p2p') diff --git a/p2p/server.go b/p2p/server.go index c6d7fc2e8..bb3101485 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -135,8 +135,8 @@ func (srv *Server) PeerCount() int { // SuggestPeer creates a connection to the given Node if it // is not already connected. -func (srv *Server) SuggestPeer(ip net.IP, port int, id discover.NodeID) { - srv.peerConnect <- &discover.Node{ID: id, IP: ip, TCPPort: port} +func (srv *Server) SuggestPeer(n *discover.Node) { + srv.peerConnect <- n } // Broadcast sends an RLP-encoded message to all connected peers. diff --git a/p2p/server_test.go b/p2p/server_test.go index d1e1640fb..aa2b3d243 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -91,7 +91,7 @@ func TestServerDial(t *testing.T) { // tell the server to connect tcpAddr := listener.Addr().(*net.TCPAddr) - srv.peerConnect <- &discover.Node{IP: tcpAddr.IP, TCPPort: tcpAddr.Port} + srv.SuggestPeer(&discover.Node{IP: tcpAddr.IP, TCPPort: tcpAddr.Port}) select { case conn := <-accepted: -- cgit v1.2.3 From 028775a0863946c1e9ad51fe7b22faa5c59b2605 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Sat, 7 Feb 2015 00:38:36 +0100 Subject: cmd/ethereum, cmd/mist: add flag for discovery bootstrap nodes --- p2p/discover/table.go | 4 ++-- p2p/server.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'p2p') diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 6025507eb..1cdd93a78 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -65,12 +65,12 @@ func (tab *Table) Close() { // to the network if the table is empty. Bootstrap will also attempt to // fill the table by performing random lookup operations on the // network. -func (tab *Table) Bootstrap(nodes []Node) { +func (tab *Table) Bootstrap(nodes []*Node) { tab.mutex.Lock() // TODO: maybe filter nodes with bad fields (nil, etc.) to avoid strange crashes tab.nursery = make([]*Node, 0, len(nodes)) for _, n := range nodes { - cpy := n + cpy := *n tab.nursery = append(tab.nursery, &cpy) } tab.mutex.Unlock() diff --git a/p2p/server.go b/p2p/server.go index bb3101485..3cab61102 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -50,7 +50,7 @@ type Server struct { // Bootstrap nodes are used to establish connectivity // with the rest of the network. - BootstrapNodes []discover.Node + BootstrapNodes []*discover.Node // Protocols should contain the protocols supported // by the server. Matching protocols are launched for -- cgit v1.2.3 From 9915d3c3be831fa2c95ac277459945d969bd7b06 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 9 Feb 2015 11:02:32 +0100 Subject: p2p/discover: deflake UDP tests --- p2p/discover/table.go | 3 +- p2p/discover/table_test.go | 12 ++++ p2p/discover/udp.go | 5 +- p2p/discover/udp_test.go | 162 ++++++++++++++++++++++++++++++--------------- 4 files changed, 123 insertions(+), 59 deletions(-) (limited to 'p2p') diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 1cdd93a78..1ff2c90d9 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -87,9 +87,10 @@ func (tab *Table) Lookup(target NodeID) []*Node { reply = make(chan []*Node, alpha) pendingQueries = 0 ) - // don't query further if we hit the target. + // don't query further if we hit the target or ourself. // unlikely to happen often in practice. asked[target] = true + asked[tab.self.ID] = true tab.mutex.Lock() // update last lookup stamp (for refresh logic) diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index 9b05ce7f4..08faea68e 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -14,6 +14,18 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) +func TestTable_bumpOrAddBucketAssign(t *testing.T) { + tab := newTable(nil, NodeID{}, &net.UDPAddr{}) + for i := 1; i < len(tab.buckets); i++ { + tab.bumpOrAdd(randomID(tab.self.ID, i), &net.UDPAddr{}) + } + for i, b := range tab.buckets { + if i > 0 && len(b.entries) != 1 { + t.Errorf("bucket %d has %d entries, want 1", i, len(b.entries)) + } + } +} + func TestTable_bumpOrAddPingReplace(t *testing.T) { pingC := make(pingC) tab := newTable(pingC, NodeID{}, &net.UDPAddr{}) diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go index 67e349aa4..4ad5d9cc4 100644 --- a/p2p/discover/udp.go +++ b/p2p/discover/udp.go @@ -179,10 +179,9 @@ func (t *udp) findnode(to *Node, target NodeID) ([]*Node, error) { nreceived := 0 errc := t.pending(to.ID, neighborsPacket, func(r interface{}) bool { reply := r.(*neighbors) - for i := 0; i < len(reply.Nodes); i++ { + for _, n := range reply.Nodes { nreceived++ - n := reply.Nodes[i] - if n.ID != t.self.ID && n.isValid() { + if n.isValid() { nodes = append(nodes, n) } } diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go index 94680d6fc..740d0a5d9 100644 --- a/p2p/discover/udp_test.go +++ b/p2p/discover/udp_test.go @@ -1,10 +1,10 @@ package discover import ( + "fmt" logpkg "log" "net" "os" - "reflect" "testing" "time" @@ -53,16 +53,37 @@ func TestUDP_findnode(t *testing.T) { defer n1.Close() defer n2.Close() - entry := MustParseNode("enode://9d8a19597e312ef32d76e6b4903bb43d7bcd892d17b769d30b404bd3a4c2dca6c86184b17d0fdeeafe3b01e0e912d990ddc853db3f325d5419f31446543c30be@127.0.0.1:54194") - n2.add([]*Node{entry}) - + // put a few nodes into n2. the exact distribution shouldn't + // matter much, altough we need to take care not to overflow + // any bucket. target := randomID(n1.self.ID, 100) - result, _ := n1.net.findnode(n2.self, target) - if len(result) != 1 { - t.Fatalf("wrong number of results: got %d, want 1", len(result)) + nodes := &nodesByDistance{target: target} + for i := 0; i < bucketSize; i++ { + n2.add([]*Node{&Node{ + IP: net.IP{1, 2, 3, byte(i)}, + DiscPort: i + 2, + TCPPort: i + 2, + ID: randomID(n2.self.ID, i+2), + }}) } - if !reflect.DeepEqual(result[0], entry) { - t.Errorf("wrong result: got %v, want %v", result[0], entry) + n2.add(nodes.entries) + n2.bumpOrAdd(n1.self.ID, &net.UDPAddr{IP: n1.self.IP, Port: n1.self.DiscPort}) + expected := n2.closest(target, bucketSize) + + err := runUDP(10, func() error { + result, _ := n1.net.findnode(n2.self, target) + if len(result) != bucketSize { + return fmt.Errorf("wrong number of results: got %d, want %d", len(result), bucketSize) + } + for i := range result { + if result[i].ID != expected.entries[i].ID { + return fmt.Errorf("result mismatch at %d:\n got: %v\n want: %v", i, result[i], expected.entries[i]) + } + } + return nil + }) + if err != nil { + t.Error(err) } } @@ -101,59 +122,90 @@ func TestUDP_findnodeMultiReply(t *testing.T) { defer n1.Close() defer n2.Close() - nodes := make([]*Node, bucketSize) - for i := range nodes { - nodes[i] = &Node{ - IP: net.IP{1, 2, 3, 4}, - DiscPort: i + 1, - TCPPort: i + 1, - ID: randomID(n2.self.ID, i+1), + err := runUDP(10, func() error { + nodes := make([]*Node, bucketSize) + for i := range nodes { + nodes[i] = &Node{ + IP: net.IP{1, 2, 3, 4}, + DiscPort: i + 1, + TCPPort: i + 1, + ID: randomID(n2.self.ID, i+1), + } + } + + // ask N2 for neighbors. it will send an empty reply back. + // the request will wait for up to bucketSize replies. + resultc := make(chan []*Node) + errc := make(chan error) + go func() { + ns, err := n1.net.findnode(n2.self, n1.self.ID) + if err != nil { + errc <- err + } else { + resultc <- ns + } + }() + + // send a few more neighbors packets to N1. + // it should collect those. + for end := 0; end < len(nodes); { + off := end + if end = end + 5; end > len(nodes) { + end = len(nodes) + } + udp2.send(n1.self, neighborsPacket, neighbors{ + Nodes: nodes[off:end], + Expiration: uint64(time.Now().Add(10 * time.Second).Unix()), + }) } - } - // ask N2 for neighbors. it will send an empty reply back. - // the request will wait for up to bucketSize replies. - resultC := make(chan []*Node) - go func() { - ns, err := n1.net.findnode(n2.self, n1.self.ID) - if err != nil { - t.Error("findnode error:", err) + // check that they are all returned. we cannot just check for + // equality because they might not be returned in the order they + // were sent. + var result []*Node + select { + case result = <-resultc: + case err := <-errc: + return err } - resultC <- ns - }() - - // send a few more neighbors packets to N1. - // it should collect those. - for end := 0; end < len(nodes); { - off := end - if end = end + 5; end > len(nodes) { - end = len(nodes) + if hasDuplicates(result) { + return fmt.Errorf("result slice contains duplicates") } - udp2.send(n1.self, neighborsPacket, neighbors{ - Nodes: nodes[off:end], - Expiration: uint64(time.Now().Add(10 * time.Second).Unix()), - }) + if len(result) != len(nodes) { + return fmt.Errorf("wrong number of nodes returned: got %d, want %d", len(result), len(nodes)) + } + matched := make(map[NodeID]bool) + for _, n := range result { + for _, expn := range nodes { + if n.ID == expn.ID { // && bytes.Equal(n.Addr.IP, expn.Addr.IP) && n.Addr.Port == expn.Addr.Port { + matched[n.ID] = true + } + } + } + if len(matched) != len(nodes) { + return fmt.Errorf("wrong number of matching nodes: got %d, want %d", len(matched), len(nodes)) + } + return nil + }) + if err != nil { + t.Error(err) } +} - // check that they are all returned. we cannot just check for - // equality because they might not be returned in the order they - // were sent. - result := <-resultC - if hasDuplicates(result) { - t.Error("result slice contains duplicates") - } - if len(result) != len(nodes) { - t.Errorf("wrong number of nodes returned: got %d, want %d", len(result), len(nodes)) - } - matched := make(map[NodeID]bool) - for _, n := range result { - for _, expn := range nodes { - if n.ID == expn.ID { // && bytes.Equal(n.Addr.IP, expn.Addr.IP) && n.Addr.Port == expn.Addr.Port { - matched[n.ID] = true - } +// runUDP runs a test n times and returns an error if the test failed +// in all n runs. This is necessary because UDP is unreliable even for +// connections on the local machine, causing test failures. +func runUDP(n int, test func() error) error { + errcount := 0 + errors := "" + for i := 0; i < n; i++ { + if err := test(); err != nil { + errors += fmt.Sprintf("\n#%d: %v", i, err) + errcount++ } } - if len(matched) != len(nodes) { - t.Errorf("wrong number of matching nodes: got %d, want %d", len(matched), len(nodes)) + if errcount == n { + return fmt.Errorf("failed on all %d iterations:%s", n, errors) } + return nil } -- cgit v1.2.3 From 1543833ca0b920d38e98994367f3871867d66781 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 10 Feb 2015 23:49:45 +0100 Subject: p2p/nat: new package for port mapping stuff I have verified that UPnP and NAT-PMP work against an older version of the MiniUPnP daemon running on pfSense. This code is kind of hard to test automatically. --- p2p/nat/nat.go | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++ p2p/nat/natpmp.go | 115 ++++++++++++++++++++++++++ p2p/nat/natupnp.go | 149 +++++++++++++++++++++++++++++++++ 3 files changed, 499 insertions(+) create mode 100644 p2p/nat/nat.go create mode 100644 p2p/nat/natpmp.go create mode 100644 p2p/nat/natupnp.go (limited to 'p2p') diff --git a/p2p/nat/nat.go b/p2p/nat/nat.go new file mode 100644 index 000000000..12d355ba1 --- /dev/null +++ b/p2p/nat/nat.go @@ -0,0 +1,235 @@ +// Package nat provides access to common port mapping protocols. +package nat + +import ( + "errors" + "fmt" + "net" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/logger" + "github.com/jackpal/go-nat-pmp" +) + +var log = logger.NewLogger("P2P NAT") + +// An implementation of nat.Interface can map local ports to ports +// accessible from the Internet. +type Interface interface { + // These methods manage a mapping between a port on the local + // machine to a port that can be connected to from the internet. + // + // protocol is "UDP" or "TCP". Some implementations allow setting + // a display name for the mapping. The mapping may be removed by + // the gateway when its lifetime ends. + AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error + DeleteMapping(protocol string, extport, intport int) error + + // This method should return the external (Internet-facing) + // address of the gateway device. + ExternalIP() (net.IP, error) + + // Should return name of the method. This is used for logging. + String() string +} + +// Parse parses a NAT interface description. +// The following formats are currently accepted. +// Note that mechanism names are not case-sensitive. +// +// "" or "none" return nil +// "extip:77.12.33.4" will assume the local machine is reachable on the given IP +// "any" uses the first auto-detected mechanism +// "upnp" uses the Universal Plug and Play protocol +// "pmp" uses NAT-PMP with an auto-detected gateway address +// "pmp:192.168.0.1" uses NAT-PMP with the given gateway address +func Parse(spec string) (Interface, error) { + var ( + parts = strings.SplitN(spec, ":", 2) + mech = strings.ToLower(parts[0]) + ip net.IP + ) + if len(parts) > 1 { + ip = net.ParseIP(parts[1]) + if ip == nil { + return nil, errors.New("invalid IP address") + } + } + switch mech { + case "", "none", "off": + return nil, nil + case "any", "auto", "on": + return Any(), nil + case "extip", "ip": + if ip == nil { + return nil, errors.New("missing IP address") + } + return ExtIP(ip), nil + case "upnp": + return UPnP(), nil + case "pmp", "natpmp", "nat-pmp": + return PMP(ip), nil + default: + return nil, fmt.Errorf("unknown mechanism %q", parts[0]) + } +} + +const ( + mapTimeout = 20 * time.Minute + mapUpdateInterval = 15 * time.Minute +) + +// Map adds a port mapping on m and keeps it alive until c is closed. +// This function is typically invoked in its own goroutine. +func Map(m Interface, c chan struct{}, protocol string, extport, intport int, name string) { + refresh := time.NewTimer(mapUpdateInterval) + defer func() { + refresh.Stop() + log.Debugf("Deleting port mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m) + m.DeleteMapping(protocol, extport, intport) + }() + log.Debugf("add mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m) + if err := m.AddMapping(protocol, intport, extport, name, mapTimeout); err != nil { + log.Errorf("mapping error: %v\n", err) + } + for { + select { + case _, ok := <-c: + if !ok { + return + } + case <-refresh.C: + log.DebugDetailf("refresh mapping: %s %d -> %d (%s) using %s\n", protocol, extport, intport, name, m) + if err := m.AddMapping(protocol, intport, extport, name, mapTimeout); err != nil { + log.Errorf("mapping error: %v\n", err) + } + refresh.Reset(mapUpdateInterval) + } + } +} + +// ExtIP assumes that the local machine is reachable on the given +// external IP address, and that any required ports were mapped manually. +// Mapping operations will not return an error but won't actually do anything. +func ExtIP(ip net.IP) Interface { + if ip == nil { + panic("IP must not be nil") + } + return extIP(ip) +} + +type extIP net.IP + +func (n extIP) ExternalIP() (net.IP, error) { return net.IP(n), nil } +func (n extIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) } + +// These do nothing. +func (extIP) AddMapping(string, int, int, string, time.Duration) error { return nil } +func (extIP) DeleteMapping(string, int, int) error { return nil } + +// Any returns a port mapper that tries to discover any supported +// mechanism on the local network. +func Any() Interface { + // TODO: attempt to discover whether the local machine has an + // Internet-class address. Return ExtIP in this case. + return startautodisc("UPnP or NAT-PMP", func() Interface { + found := make(chan Interface, 2) + go func() { found <- discoverUPnP() }() + go func() { found <- discoverPMP() }() + for i := 0; i < cap(found); i++ { + if c := <-found; c != nil { + return c + } + } + return nil + }) +} + +// UPnP returns a port mapper that uses UPnP. It will attempt to +// discover the address of your router using UDP broadcasts. +func UPnP() Interface { + return startautodisc("UPnP", discoverUPnP) +} + +// PMP returns a port mapper that uses NAT-PMP. The provided gateway +// address should be the IP of your router. If the given gateway +// address is nil, PMP will attempt to auto-discover the router. +func PMP(gateway net.IP) Interface { + if gateway != nil { + return &pmp{gw: gateway, c: natpmp.NewClient(gateway)} + } + return startautodisc("NAT-PMP", discoverPMP) +} + +// autodisc represents a port mapping mechanism that is still being +// auto-discovered. Calls to the Interface methods on this type will +// wait until the discovery is done and then call the method on the +// discovered mechanism. +// +// This type is useful because discovery can take a while but we +// want return an Interface value from UPnP, PMP and Auto immediately. +type autodisc struct { + what string + done <-chan Interface + + mu sync.Mutex + found Interface +} + +func startautodisc(what string, doit func() Interface) Interface { + // TODO: monitor network configuration and rerun doit when it changes. + done := make(chan Interface) + ad := &autodisc{what: what, done: done} + go func() { done <- doit(); close(done) }() + return ad +} + +func (n *autodisc) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error { + if err := n.wait(); err != nil { + return err + } + return n.found.AddMapping(protocol, extport, intport, name, lifetime) +} + +func (n *autodisc) DeleteMapping(protocol string, extport, intport int) error { + if err := n.wait(); err != nil { + return err + } + return n.found.DeleteMapping(protocol, extport, intport) +} + +func (n *autodisc) ExternalIP() (net.IP, error) { + if err := n.wait(); err != nil { + return nil, err + } + return n.found.ExternalIP() +} + +func (n *autodisc) String() string { + n.mu.Lock() + defer n.mu.Unlock() + if n.found == nil { + return n.what + } else { + return n.found.String() + } +} + +func (n *autodisc) wait() error { + n.mu.Lock() + found := n.found + n.mu.Unlock() + if found != nil { + // already discovered + return nil + } + if found = <-n.done; found == nil { + return errors.New("no devices discovered") + } + n.mu.Lock() + n.found = found + n.mu.Unlock() + return nil +} diff --git a/p2p/nat/natpmp.go b/p2p/nat/natpmp.go new file mode 100644 index 000000000..f249c6073 --- /dev/null +++ b/p2p/nat/natpmp.go @@ -0,0 +1,115 @@ +package nat + +import ( + "fmt" + "net" + "strings" + "time" + + "github.com/jackpal/go-nat-pmp" +) + +// natPMPClient adapts the NAT-PMP protocol implementation so it conforms to +// the common interface. +type pmp struct { + gw net.IP + c *natpmp.Client +} + +func (n *pmp) String() string { + return fmt.Sprintf("NAT-PMP(%v)", n.gw) +} + +func (n *pmp) ExternalIP() (net.IP, error) { + response, err := n.c.GetExternalAddress() + if err != nil { + return nil, err + } + return response.ExternalIPAddress[:], nil +} + +func (n *pmp) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error { + if lifetime <= 0 { + return fmt.Errorf("lifetime must not be <= 0") + } + // Note order of port arguments is switched between our + // AddMapping and the client's AddPortMapping. + _, err := n.c.AddPortMapping(strings.ToLower(protocol), intport, extport, int(lifetime/time.Second)) + return err +} + +func (n *pmp) DeleteMapping(protocol string, extport, intport int) (err error) { + // To destroy a mapping, send an add-port with an internalPort of + // the internal port to destroy, an external port of zero and a + // time of zero. + _, err = n.c.AddPortMapping(strings.ToLower(protocol), intport, 0, 0) + return err +} + +func discoverPMP() Interface { + // run external address lookups on all potential gateways + gws := potentialGateways() + found := make(chan *pmp, len(gws)) + for i := range gws { + gw := gws[i] + go func() { + c := natpmp.NewClient(gw) + if _, err := c.GetExternalAddress(); err != nil { + found <- nil + } else { + found <- &pmp{gw, c} + } + }() + } + // return the one that responds first. + // discovery needs to be quick, so we stop caring about + // any responses after a very short timeout. + timeout := time.NewTimer(1 * time.Second) + defer timeout.Stop() + for _ = range gws { + select { + case c := <-found: + if c != nil { + return c + } + case <-timeout.C: + return nil + } + } + return nil +} + +var ( + // LAN IP ranges + _, lan10, _ = net.ParseCIDR("10.0.0.0/8") + _, lan176, _ = net.ParseCIDR("172.16.0.0/12") + _, lan192, _ = net.ParseCIDR("192.168.0.0/16") +) + +// TODO: improve this. We currently assume that (on most networks) +// the router is X.X.X.1 in a local LAN range. +func potentialGateways() (gws []net.IP) { + ifaces, err := net.Interfaces() + if err != nil { + return nil + } + for _, iface := range ifaces { + ifaddrs, err := iface.Addrs() + if err != nil { + return gws + } + for _, addr := range ifaddrs { + switch x := addr.(type) { + case *net.IPNet: + if lan10.Contains(x.IP) || lan176.Contains(x.IP) || lan192.Contains(x.IP) { + ip := x.IP.Mask(x.Mask).To4() + if ip != nil { + ip[3] = ip[3] | 0x01 + gws = append(gws, ip) + } + } + } + } + } + return gws +} diff --git a/p2p/nat/natupnp.go b/p2p/nat/natupnp.go new file mode 100644 index 000000000..25cbc0d71 --- /dev/null +++ b/p2p/nat/natupnp.go @@ -0,0 +1,149 @@ +package nat + +import ( + "errors" + "fmt" + "net" + "strings" + "time" + + "github.com/fjl/goupnp" + "github.com/fjl/goupnp/dcps/internetgateway1" + "github.com/fjl/goupnp/dcps/internetgateway2" +) + +type upnp struct { + dev *goupnp.RootDevice + service string + client upnpClient +} + +type upnpClient interface { + GetExternalIPAddress() (string, error) + AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error + DeletePortMapping(string, uint16, string) error + GetNATRSIPStatus() (sip bool, nat bool, err error) +} + +func (n *upnp) ExternalIP() (addr net.IP, err error) { + ipString, err := n.client.GetExternalIPAddress() + if err != nil { + return nil, err + } + ip := net.ParseIP(ipString) + if ip == nil { + return nil, errors.New("bad IP in response") + } + return ip, nil +} + +func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) error { + ip, err := n.internalAddress() + if err != nil { + return nil + } + protocol = strings.ToUpper(protocol) + lifetimeS := uint32(lifetime / time.Second) + return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS) +} + +func (n *upnp) internalAddress() (net.IP, error) { + devaddr, err := net.ResolveUDPAddr("udp4", n.dev.URLBase.Host) + if err != nil { + return nil, err + } + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + return nil, err + } + for _, addr := range addrs { + switch x := addr.(type) { + case *net.IPNet: + if x.Contains(devaddr.IP) { + return x.IP, nil + } + } + } + } + return nil, fmt.Errorf("could not find local address in same net as %v", devaddr) +} + +func (n *upnp) DeleteMapping(protocol string, extport, intport int) error { + return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol)) +} + +func (n *upnp) String() string { + return "UPNP " + n.service +} + +// discoverUPnP searches for Internet Gateway Devices +// and returns the first one it can find on the local network. +func discoverUPnP() Interface { + found := make(chan *upnp, 2) + // IGDv1 + go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp { + switch sc.Service.ServiceType { + case internetgateway1.URN_WANIPConnection_1: + return &upnp{dev, "IGDv1-IP1", &internetgateway1.WANIPConnection1{sc}} + case internetgateway1.URN_WANPPPConnection_1: + return &upnp{dev, "IGDv1-PPP1", &internetgateway1.WANPPPConnection1{sc}} + } + return nil + }) + // IGDv2 + go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp { + switch sc.Service.ServiceType { + case internetgateway2.URN_WANIPConnection_1: + return &upnp{dev, "IGDv2-IP1", &internetgateway2.WANIPConnection1{sc}} + case internetgateway2.URN_WANIPConnection_2: + return &upnp{dev, "IGDv2-IP2", &internetgateway2.WANIPConnection2{sc}} + case internetgateway2.URN_WANPPPConnection_1: + return &upnp{dev, "IGDv2-PPP1", &internetgateway2.WANPPPConnection1{sc}} + } + return nil + }) + for i := 0; i < cap(found); i++ { + if c := <-found; c != nil { + return c + } + } + return nil +} + +func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) { + devs, err := goupnp.DiscoverDevices(target) + if err != nil { + return + } + found := false + for i := 0; i < len(devs) && !found; i++ { + if devs[i].Root == nil { + continue + } + devs[i].Root.Device.VisitServices(func(service *goupnp.Service) { + if found { + return + } + // check for a matching IGD service + sc := goupnp.ServiceClient{service.NewSOAPClient(), devs[i].Root, service} + upnp := matcher(devs[i].Root, sc) + if upnp == nil { + return + } + // check whether port mapping is enabled + if _, nat, err := upnp.client.GetNATRSIPStatus(); err != nil || !nat { + return + } + out <- upnp + found = true + }) + } + if !found { + out <- nil + } +} -- cgit v1.2.3 From d0a2e655c9599f462bb20bd49bc69b8e1e330a21 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 11 Feb 2015 17:19:31 +0100 Subject: cmd/ethereum, cmd/mist, eth, p2p: use package p2p/nat This deletes the old NAT implementation. --- p2p/nat.go | 23 ---- p2p/natpmp.go | 55 ---------- p2p/natupnp.go | 341 --------------------------------------------------------- p2p/server.go | 70 ++---------- 4 files changed, 11 insertions(+), 478 deletions(-) delete mode 100644 p2p/nat.go delete mode 100644 p2p/natpmp.go delete mode 100644 p2p/natupnp.go (limited to 'p2p') diff --git a/p2p/nat.go b/p2p/nat.go deleted file mode 100644 index 9b771c3e8..000000000 --- a/p2p/nat.go +++ /dev/null @@ -1,23 +0,0 @@ -package p2p - -import ( - "fmt" - "net" -) - -func ParseNAT(natType string, gateway string) (nat NAT, err error) { - switch natType { - case "UPNP": - nat = UPNP() - case "PMP": - ip := net.ParseIP(gateway) - if ip == nil { - return nil, fmt.Errorf("cannot resolve PMP gateway IP %s", gateway) - } - nat = PMP(ip) - case "": - default: - return nil, fmt.Errorf("unrecognised NAT type '%s'", natType) - } - return -} diff --git a/p2p/natpmp.go b/p2p/natpmp.go deleted file mode 100644 index 6714678c4..000000000 --- a/p2p/natpmp.go +++ /dev/null @@ -1,55 +0,0 @@ -package p2p - -import ( - "fmt" - "net" - "time" - - natpmp "github.com/jackpal/go-nat-pmp" -) - -// Adapt the NAT-PMP protocol to the NAT interface - -// TODO: -// + Register for changes to the external address. -// + Re-register port mapping when router reboots. -// + A mechanism for keeping a port mapping registered. -// + Discover gateway address automatically. - -type natPMPClient struct { - client *natpmp.Client -} - -// PMP returns a NAT traverser that uses NAT-PMP. The provided gateway -// address should be the IP of your router. -func PMP(gateway net.IP) (nat NAT) { - return &natPMPClient{natpmp.NewClient(gateway)} -} - -func (*natPMPClient) String() string { - return "NAT-PMP" -} - -func (n *natPMPClient) GetExternalAddress() (net.IP, error) { - response, err := n.client.GetExternalAddress() - if err != nil { - return nil, err - } - return response.ExternalIPAddress[:], nil -} - -func (n *natPMPClient) AddPortMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error { - if lifetime <= 0 { - return fmt.Errorf("lifetime must not be <= 0") - } - // Note order of port arguments is switched between our AddPortMapping and the client's AddPortMapping. - _, err := n.client.AddPortMapping(protocol, intport, extport, int(lifetime/time.Second)) - return err -} - -func (n *natPMPClient) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { - // To destroy a mapping, send an add-port with - // an internalPort of the internal port to destroy, an external port of zero and a time of zero. - _, err = n.client.AddPortMapping(protocol, internalPort, 0, 0) - return -} diff --git a/p2p/natupnp.go b/p2p/natupnp.go deleted file mode 100644 index 2e0d8ce8d..000000000 --- a/p2p/natupnp.go +++ /dev/null @@ -1,341 +0,0 @@ -package p2p - -// Just enough UPnP to be able to forward ports -// - -import ( - "bytes" - "encoding/xml" - "errors" - "fmt" - "net" - "net/http" - "os" - "strconv" - "strings" - "time" -) - -const ( - upnpDiscoverAttempts = 3 - upnpDiscoverTimeout = 5 * time.Second -) - -// UPNP returns a NAT port mapper that uses UPnP. It will attempt to -// discover the address of your router using UDP broadcasts. -func UPNP() NAT { - return &upnpNAT{} -} - -type upnpNAT struct { - serviceURL string - ourIP string -} - -func (n *upnpNAT) String() string { - return "UPNP" -} - -func (n *upnpNAT) discover() error { - if n.serviceURL != "" { - // already discovered - return nil - } - - ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") - if err != nil { - return err - } - // TODO: try on all network interfaces simultaneously. - // Broadcasting on 0.0.0.0 could select a random interface - // to send on (platform specific). - conn, err := net.ListenPacket("udp4", ":0") - if err != nil { - return err - } - defer conn.Close() - - conn.SetDeadline(time.Now().Add(10 * time.Second)) - st := "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" - buf := bytes.NewBufferString( - "M-SEARCH * HTTP/1.1\r\n" + - "HOST: 239.255.255.250:1900\r\n" + - st + - "MAN: \"ssdp:discover\"\r\n" + - "MX: 2\r\n\r\n") - message := buf.Bytes() - answerBytes := make([]byte, 1024) - for i := 0; i < upnpDiscoverAttempts; i++ { - _, err = conn.WriteTo(message, ssdp) - if err != nil { - return err - } - nn, _, err := conn.ReadFrom(answerBytes) - if err != nil { - continue - } - answer := string(answerBytes[0:nn]) - if strings.Index(answer, "\r\n"+st) < 0 { - continue - } - // HTTP header field names are case-insensitive. - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 - locString := "\r\nlocation: " - answer = strings.ToLower(answer) - locIndex := strings.Index(answer, locString) - if locIndex < 0 { - continue - } - loc := answer[locIndex+len(locString):] - endIndex := strings.Index(loc, "\r\n") - if endIndex < 0 { - continue - } - locURL := loc[0:endIndex] - var serviceURL string - serviceURL, err = getServiceURL(locURL) - if err != nil { - return err - } - var ourIP string - ourIP, err = getOurIP() - if err != nil { - return err - } - n.serviceURL = serviceURL - n.ourIP = ourIP - return nil - } - return errors.New("UPnP port discovery failed.") -} - -func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { - if err := n.discover(); err != nil { - return nil, err - } - info, err := n.getStatusInfo() - return net.ParseIP(info.externalIpAddress), err -} - -func (n *upnpNAT) AddPortMapping(protocol string, extport, intport int, description string, lifetime time.Duration) error { - if err := n.discover(); err != nil { - return err - } - - // A single concatenation would break ARM compilation. - message := "\r\n" + - "" + strconv.Itoa(extport) - message += "" + protocol + "" - message += "" + strconv.Itoa(extport) + "" + - "" + n.ourIP + "" + - "1" - message += description + - "" + fmt.Sprint(lifetime/time.Second) + - "" - - // TODO: check response to see if the port was forwarded - _, err := soapRequest(n.serviceURL, "AddPortMapping", message) - return err -} - -func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) error { - if err := n.discover(); err != nil { - return err - } - - message := "\r\n" + - "" + strconv.Itoa(externalPort) + - "" + protocol + "" + - "" - - // TODO: check response to see if the port was deleted - _, err := soapRequest(n.serviceURL, "DeletePortMapping", message) - return err -} - -type statusInfo struct { - externalIpAddress string -} - -func (n *upnpNAT) getStatusInfo() (info statusInfo, err error) { - message := "\r\n" + - "" - - var response *http.Response - response, err = soapRequest(n.serviceURL, "GetStatusInfo", message) - if err != nil { - return - } - - // TODO: Write a soap reply parser. It has to eat the Body and envelope tags... - - response.Body.Close() - return -} - -// service represents the Service type in an UPnP xml description. -// Only the parts we care about are present and thus the xml may have more -// fields than present in the structure. -type service struct { - ServiceType string `xml:"serviceType"` - ControlURL string `xml:"controlURL"` -} - -// deviceList represents the deviceList type in an UPnP xml description. -// Only the parts we care about are present and thus the xml may have more -// fields than present in the structure. -type deviceList struct { - XMLName xml.Name `xml:"deviceList"` - Device []device `xml:"device"` -} - -// serviceList represents the serviceList type in an UPnP xml description. -// Only the parts we care about are present and thus the xml may have more -// fields than present in the structure. -type serviceList struct { - XMLName xml.Name `xml:"serviceList"` - Service []service `xml:"service"` -} - -// device represents the device type in an UPnP xml description. -// Only the parts we care about are present and thus the xml may have more -// fields than present in the structure. -type device struct { - XMLName xml.Name `xml:"device"` - DeviceType string `xml:"deviceType"` - DeviceList deviceList `xml:"deviceList"` - ServiceList serviceList `xml:"serviceList"` -} - -// specVersion represents the specVersion in a UPnP xml description. -// Only the parts we care about are present and thus the xml may have more -// fields than present in the structure. -type specVersion struct { - XMLName xml.Name `xml:"specVersion"` - Major int `xml:"major"` - Minor int `xml:"minor"` -} - -// root represents the Root document for a UPnP xml description. -// Only the parts we care about are present and thus the xml may have more -// fields than present in the structure. -type root struct { - XMLName xml.Name `xml:"root"` - SpecVersion specVersion - Device device -} - -func getChildDevice(d *device, deviceType string) *device { - dl := d.DeviceList.Device - for i := 0; i < len(dl); i++ { - if dl[i].DeviceType == deviceType { - return &dl[i] - } - } - return nil -} - -func getChildService(d *device, serviceType string) *service { - sl := d.ServiceList.Service - for i := 0; i < len(sl); i++ { - if sl[i].ServiceType == serviceType { - return &sl[i] - } - } - return nil -} - -func getOurIP() (ip string, err error) { - hostname, err := os.Hostname() - if err != nil { - return - } - p, err := net.LookupIP(hostname) - if err != nil && len(p) > 0 { - return - } - return p[0].String(), nil -} - -func getServiceURL(rootURL string) (url string, err error) { - r, err := http.Get(rootURL) - if err != nil { - return - } - defer r.Body.Close() - if r.StatusCode >= 400 { - err = errors.New(string(r.StatusCode)) - return - } - var root root - err = xml.NewDecoder(r.Body).Decode(&root) - - if err != nil { - return - } - a := &root.Device - if a.DeviceType != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" { - err = errors.New("No InternetGatewayDevice") - return - } - b := getChildDevice(a, "urn:schemas-upnp-org:device:WANDevice:1") - if b == nil { - err = errors.New("No WANDevice") - return - } - c := getChildDevice(b, "urn:schemas-upnp-org:device:WANConnectionDevice:1") - if c == nil { - err = errors.New("No WANConnectionDevice") - return - } - d := getChildService(c, "urn:schemas-upnp-org:service:WANIPConnection:1") - if d == nil { - err = errors.New("No WANIPConnection") - return - } - url = combineURL(rootURL, d.ControlURL) - return -} - -func combineURL(rootURL, subURL string) string { - protocolEnd := "://" - protoEndIndex := strings.Index(rootURL, protocolEnd) - a := rootURL[protoEndIndex+len(protocolEnd):] - rootIndex := strings.Index(a, "/") - return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL -} - -func soapRequest(url, function, message string) (r *http.Response, err error) { - fullMessage := "" + - "\r\n" + - "" + message + "" - - req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage)) - if err != nil { - return - } - req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") - req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") - //req.Header.Set("Transfer-Encoding", "chunked") - req.Header.Set("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#"+function+"\"") - req.Header.Set("Connection", "Close") - req.Header.Set("Cache-Control", "no-cache") - req.Header.Set("Pragma", "no-cache") - - r, err = http.DefaultClient.Do(req) - if err != nil { - return - } - - if r.Body != nil { - defer r.Body.Close() - } - - if r.StatusCode >= 400 { - // log.Stderr(function, r.StatusCode) - err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function) - r = nil - return - } - return -} diff --git a/p2p/server.go b/p2p/server.go index 3cab61102..a0f2dee23 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -13,13 +13,12 @@ import ( "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/nat" ) const ( - defaultDialTimeout = 10 * time.Second - refreshPeersInterval = 30 * time.Second - portMappingUpdateInterval = 15 * time.Minute - portMappingTimeout = 20 * time.Minute + defaultDialTimeout = 10 * time.Second + refreshPeersInterval = 30 * time.Second ) var srvlog = logger.NewLogger("P2P Server") @@ -72,7 +71,7 @@ type Server struct { // If set to a non-nil value, the given NAT port mapper // is used to make the listening port available to the // Internet. - NAT NAT + NAT nat.Interface // If Dialer is set to a non-nil value, the given Dialer // is used to dial outbound peer connections. @@ -89,7 +88,6 @@ type Server struct { lock sync.RWMutex running bool listener net.Listener - laddr *net.TCPAddr // real listen addr peers map[discover.NodeID]*Peer ntab *discover.Table @@ -100,16 +98,6 @@ type Server struct { peerConnect chan *discover.Node } -// NAT is implemented by NAT traversal methods. -type NAT interface { - GetExternalAddress() (net.IP, error) - AddPortMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error - DeletePortMapping(protocol string, extport, intport int) error - - // Should return name of the method. - String() string -} - type handshakeFunc func(io.ReadWriter, *ecdsa.PrivateKey, *discover.Node) (discover.NodeID, []byte, error) type newPeerHook func(*Peer) @@ -220,14 +208,17 @@ func (srv *Server) startListening() error { if err != nil { return err } - srv.ListenAddr = listener.Addr().String() - srv.laddr = listener.Addr().(*net.TCPAddr) + laddr := listener.Addr().(*net.TCPAddr) + srv.ListenAddr = laddr.String() srv.listener = listener srv.loopWG.Add(1) go srv.listenLoop() - if !srv.laddr.IP.IsLoopback() && srv.NAT != nil { + if !laddr.IP.IsLoopback() && srv.NAT != nil { srv.loopWG.Add(1) - go srv.natLoop(srv.laddr.Port) + go func() { + nat.Map(srv.NAT, srv.quit, "tcp", laddr.Port, laddr.Port, "ethereum p2p") + srv.loopWG.Done() + }() } return nil } @@ -276,45 +267,6 @@ func (srv *Server) listenLoop() { } } -func (srv *Server) natLoop(port int) { - defer srv.loopWG.Done() - for { - srv.updatePortMapping(port) - select { - case <-time.After(portMappingUpdateInterval): - // one more round - case <-srv.quit: - srv.removePortMapping(port) - return - } - } -} - -func (srv *Server) updatePortMapping(port int) { - srvlog.Infoln("Attempting to map port", port, "with", srv.NAT) - err := srv.NAT.AddPortMapping("tcp", port, port, "ethereum p2p", portMappingTimeout) - if err != nil { - srvlog.Errorln("Port mapping error:", err) - return - } - extip, err := srv.NAT.GetExternalAddress() - if err != nil { - srvlog.Errorln("Error getting external IP:", err) - return - } - srv.lock.Lock() - extaddr := *(srv.listener.Addr().(*net.TCPAddr)) - extaddr.IP = extip - srvlog.Infoln("Mapped port, external addr is", &extaddr) - srv.laddr = &extaddr - srv.lock.Unlock() -} - -func (srv *Server) removePortMapping(port int) { - srvlog.Infoln("Removing port mapping for", port, "with", srv.NAT) - srv.NAT.DeletePortMapping("tcp", port, port) -} - func (srv *Server) dialLoop() { defer srv.loopWG.Done() refresh := time.NewTicker(refreshPeersInterval) -- cgit v1.2.3 From 82f0bd9009d8d577c86e800e9673a1972117113d Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 12 Feb 2015 11:59:52 +0100 Subject: p2p/discover: code review fixes --- p2p/discover/node.go | 4 +++- p2p/discover/table.go | 8 ++++---- p2p/discover/udp.go | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) (limited to 'p2p') diff --git a/p2p/discover/node.go b/p2p/discover/node.go index a49a2f547..c6d2e9766 100644 --- a/p2p/discover/node.go +++ b/p2p/discover/node.go @@ -18,6 +18,8 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +const nodeIDBits = 512 + // Node represents a host on the network. type Node struct { ID NodeID @@ -135,7 +137,7 @@ func (n *Node) DecodeRLP(s *rlp.Stream) (err error) { // NodeID is a unique identifier for each node. // The node identifier is a marshaled elliptic curve public key. -type NodeID [512 / 8]byte +type NodeID [nodeIDBits / 8]byte // NodeID prints as a long hexadecimal number. func (n NodeID) String() string { diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 1ff2c90d9..e3bec9328 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -14,9 +14,9 @@ import ( ) const ( - alpha = 3 // Kademlia concurrency factor - bucketSize = 16 // Kademlia bucket size - nBuckets = len(NodeID{})*8 + 1 // Number of buckets + alpha = 3 // Kademlia concurrency factor + bucketSize = 16 // Kademlia bucket size + nBuckets = nodeIDBits + 1 // Number of buckets ) type Table struct { @@ -100,7 +100,7 @@ func (tab *Table) Lookup(target NodeID) []*Node { tab.mutex.Unlock() for { - // ask the closest nodes that we haven't asked yet + // ask the alpha closest nodes that we haven't asked yet for i := 0; i < len(result.entries) && pendingQueries < alpha; i++ { n := result.entries[i] if !asked[n.ID] { diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go index 4ad5d9cc4..1f91641f3 100644 --- a/p2p/discover/udp.go +++ b/p2p/discover/udp.go @@ -28,7 +28,7 @@ var ( const ( respTimeout = 300 * time.Millisecond sendTimeout = 300 * time.Millisecond - expiration = 3 * time.Second + expiration = 20 * time.Second refreshInterval = 1 * time.Hour ) @@ -185,7 +185,7 @@ func (t *udp) findnode(to *Node, target NodeID) ([]*Node, error) { nodes = append(nodes, n) } } - return nreceived == bucketSize + return nreceived >= bucketSize }) t.send(to, findnodePacket, findnode{ -- cgit v1.2.3 From 170eb3ac684231dc2ddebc34e7006e0f2b5fc0c1 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 11:38:34 +0100 Subject: p2p/discover: map listening port using configured mechanism --- p2p/discover/udp.go | 23 +++++++++++++++++------ p2p/discover/udp_test.go | 14 +++++++------- p2p/server.go | 3 +-- 3 files changed, 25 insertions(+), 15 deletions(-) (limited to 'p2p') diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go index 1f91641f3..a9ed7fcb8 100644 --- a/p2p/discover/udp.go +++ b/p2p/discover/udp.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/rlp" ) @@ -82,6 +83,7 @@ type udp struct { addpending chan *pending replies chan reply closing chan struct{} + nat nat.Interface *Table } @@ -121,17 +123,26 @@ type reply struct { } // ListenUDP returns a new table that listens for UDP packets on laddr. -func ListenUDP(priv *ecdsa.PrivateKey, laddr string) (*Table, error) { - net, realaddr, err := listen(priv, laddr) +func ListenUDP(priv *ecdsa.PrivateKey, laddr string, natm nat.Interface) (*Table, error) { + t, realaddr, err := listen(priv, laddr, natm) if err != nil { return nil, err } - net.Table = newTable(net, PubkeyID(&priv.PublicKey), realaddr) - log.Debugf("Listening, %v\n", net.self) - return net.Table, nil + if natm != nil { + if !realaddr.IP.IsLoopback() { + go nat.Map(natm, t.closing, "udp", realaddr.Port, realaddr.Port, "ethereum discovery") + } + // TODO: react to external IP changes over time. + if ext, err := natm.ExternalIP(); err == nil { + realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port} + } + } + t.Table = newTable(t, PubkeyID(&priv.PublicKey), realaddr) + log.Infoln("Listening, ", t.self) + return t.Table, nil } -func listen(priv *ecdsa.PrivateKey, laddr string) (*udp, *net.UDPAddr, error) { +func listen(priv *ecdsa.PrivateKey, laddr string, nat nat.Interface) (*udp, *net.UDPAddr, error) { addr, err := net.ResolveUDPAddr("udp", laddr) if err != nil { return nil, nil, err diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go index 740d0a5d9..0a8ff6358 100644 --- a/p2p/discover/udp_test.go +++ b/p2p/discover/udp_test.go @@ -18,8 +18,8 @@ func init() { func TestUDP_ping(t *testing.T) { t.Parallel() - n1, _ := ListenUDP(newkey(), "127.0.0.1:0") - n2, _ := ListenUDP(newkey(), "127.0.0.1:0") + n1, _ := ListenUDP(newkey(), "127.0.0.1:0", nil) + n2, _ := ListenUDP(newkey(), "127.0.0.1:0", nil) defer n1.Close() defer n2.Close() @@ -48,8 +48,8 @@ func find(tab *Table, id NodeID) *Node { func TestUDP_findnode(t *testing.T) { t.Parallel() - n1, _ := ListenUDP(newkey(), "127.0.0.1:0") - n2, _ := ListenUDP(newkey(), "127.0.0.1:0") + n1, _ := ListenUDP(newkey(), "127.0.0.1:0", nil) + n2, _ := ListenUDP(newkey(), "127.0.0.1:0", nil) defer n1.Close() defer n2.Close() @@ -98,7 +98,7 @@ func TestUDP_replytimeout(t *testing.T) { } defer fd.Close() - n1, _ := ListenUDP(newkey(), "127.0.0.1:0") + n1, _ := ListenUDP(newkey(), "127.0.0.1:0", nil) defer n1.Close() n2 := n1.bumpOrAdd(randomID(n1.self.ID, 10), fd.LocalAddr().(*net.UDPAddr)) @@ -116,8 +116,8 @@ func TestUDP_replytimeout(t *testing.T) { func TestUDP_findnodeMultiReply(t *testing.T) { t.Parallel() - n1, _ := ListenUDP(newkey(), "127.0.0.1:0") - n2, _ := ListenUDP(newkey(), "127.0.0.1:0") + n1, _ := ListenUDP(newkey(), "127.0.0.1:0", nil) + n2, _ := ListenUDP(newkey(), "127.0.0.1:0", nil) udp2 := n2.net.(*udp) defer n1.Close() defer n2.Close() diff --git a/p2p/server.go b/p2p/server.go index a0f2dee23..e44f3d7ab 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -182,7 +182,7 @@ func (srv *Server) Start() (err error) { } // dial stuff - dt, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr) + dt, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr, srv.NAT) if err != nil { return err } @@ -194,7 +194,6 @@ func (srv *Server) Start() (err error) { srv.loopWG.Add(1) go srv.dialLoop() } - if srv.NoDial && srv.ListenAddr == "" { srvlog.Warnln("I will be kind-of useless, neither dialing nor listening.") } -- cgit v1.2.3 From 5110f80bba13e3758ae1836a88afee123df81e3e Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 14:44:00 +0100 Subject: p2p: improve read deadlines There are now two deadlines, frameReadTimeout and payloadReadTimeout. The frame timeout is longer and allows for connections that are idle. The message timeout is still short and ensures that we don't get stuck in the middle of a message. --- p2p/message.go | 28 +++++++++++++++++++++++++--- p2p/peer.go | 14 ++------------ 2 files changed, 27 insertions(+), 15 deletions(-) (limited to 'p2p') diff --git a/p2p/message.go b/p2p/message.go index dfc33f349..07916f7b3 100644 --- a/p2p/message.go +++ b/p2p/message.go @@ -18,6 +18,28 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +// parameters for frameRW +const ( + // maximum time allowed for reading a message header. + // this is effectively the amount of time a connection can be idle. + frameReadTimeout = 1 * time.Minute + + // maximum time allowed for reading the payload data of a message. + // this is shorter than (and distinct from) frameReadTimeout because + // the connection is not considered idle while a message is transferred. + // this also limits the payload size of messages to how much the connection + // can transfer within the timeout. + payloadReadTimeout = 5 * time.Second + + // maximum amount of time allowed for writing a complete message. + msgWriteTimeout = 5 * time.Second + + // messages smaller than this many bytes will be read at + // once before passing them to a protocol. this increases + // concurrency in the processing. + wholePayloadSize = 64 * 1024 +) + // Msg defines the structure of a p2p message. // // Note that a Msg can only be sent once since the Payload reader is @@ -167,9 +189,7 @@ func makeListHeader(length uint32) []byte { func (rw *frameRW) ReadMsg() (msg Msg, err error) { <-rw.rsync // wait until bufconn is ours - // this read timeout applies also to the payload. - // TODO: proper read timeout - rw.SetReadDeadline(time.Now().Add(msgReadTimeout)) + rw.SetReadDeadline(time.Now().Add(frameReadTimeout)) // read magic and payload size start := make([]byte, 8) @@ -193,6 +213,8 @@ func (rw *frameRW) ReadMsg() (msg Msg, err error) { } msg.Size = size - posr.p + rw.SetReadDeadline(time.Now().Add(payloadReadTimeout)) + if msg.Size <= wholePayloadSize { // msg is small, read all of it and move on to the next message. pbuf := make([]byte, msg.Size) diff --git a/p2p/peer.go b/p2p/peer.go index b61cf96da..f779c1c02 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -15,22 +15,12 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) -const ( - // maximum amount of time allowed for reading a message - msgReadTimeout = 5 * time.Second - // maximum amount of time allowed for writing a message - msgWriteTimeout = 5 * time.Second - // messages smaller than this many bytes will be read at - // once before passing them to a protocol. - wholePayloadSize = 64 * 1024 - - disconnectGracePeriod = 2 * time.Second -) - const ( baseProtocolVersion = 2 baseProtocolLength = uint64(16) baseProtocolMaxMsgSize = 10 * 1024 * 1024 + + disconnectGracePeriod = 2 * time.Second ) const ( -- cgit v1.2.3 From 22ee366ed6419516eece4436c9f7b5b63ea8a713 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 14:47:05 +0100 Subject: p2p: fix goroutine leak for invalid peers The deflect logic called Disconnect on the peer, but the peer never ran and wouldn't process the disconnect request. --- p2p/server.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'p2p') diff --git a/p2p/server.go b/p2p/server.go index e44f3d7ab..f61bb897e 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -351,19 +351,21 @@ func (srv *Server) startPeer(conn net.Conn, dest *discover.Node) { srvlog.Debugf("Encryption Handshake with %v failed: %v", conn.RemoteAddr(), err) return } - ourID := srv.ntab.Self() p := newPeer(conn, srv.Protocols, srv.Name, &ourID, &remoteID) if ok, reason := srv.addPeer(remoteID, p); !ok { - p.Disconnect(reason) + srvlog.DebugDetailf("Not adding %v (%v)\n", p, reason) + p.politeDisconnect(reason) return } + srvlog.Debugf("Added %v\n", p) if srv.newPeerHook != nil { srv.newPeerHook(p) } - p.run() + discreason := p.run() srv.removePeer(p) + srvlog.Debugf("Removed %v (%v)\n", p, discreason) } func (srv *Server) addPeer(id discover.NodeID, p *Peer) (bool, DiscReason) { @@ -381,14 +383,11 @@ func (srv *Server) addPeer(id discover.NodeID, p *Peer) (bool, DiscReason) { case id == srv.ntab.Self(): return false, DiscSelf } - srvlog.Debugf("Adding %v\n", p) srv.peers[id] = p return true, 0 } -// removes peer: sending disconnect msg, stop peer, remove rom list/table, release slot func (srv *Server) removePeer(p *Peer) { - srvlog.Debugf("Removing %v\n", p) srv.lock.Lock() delete(srv.peers, *p.remoteID) srv.lock.Unlock() -- cgit v1.2.3 From 7101f4499873fbf6a68cbe08a45797ff8ec71e74 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 14:49:49 +0100 Subject: p2p: add I/O timeout for encrytion handshake --- p2p/server.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/server.go b/p2p/server.go index f61bb897e..b93340150 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -17,6 +17,7 @@ import ( ) const ( + handshakeTimeout = 5 * time.Second defaultDialTimeout = 10 * time.Second refreshPeersInterval = 30 * time.Second ) @@ -344,7 +345,8 @@ func (srv *Server) findPeers() { } func (srv *Server) startPeer(conn net.Conn, dest *discover.Node) { - // TODO: I/O timeout, handle/store session token + // TODO: handle/store session token + conn.SetDeadline(time.Now().Add(handshakeTimeout)) remoteID, _, err := srv.handshakeFunc(conn, srv.PrivateKey, dest) if err != nil { conn.Close() -- cgit v1.2.3 From 5cc1256fd679cbb8cb80502494b8c02befc757c8 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 14:50:14 +0100 Subject: p2p: ensure we don't dial ourself addPeer doesn't allow self connects, but we can avoid opening connections in the first place. --- p2p/server.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/server.go b/p2p/server.go index b93340150..e510be521 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -289,10 +289,13 @@ func (srv *Server) dialLoop() { go srv.findPeers() case dest := <-srv.peerConnect: + // avoid dialing nodes that are already connected. + // there is another check for this in addPeer, + // which runs after the handshake. srv.lock.Lock() _, isconnected := srv.peers[dest.ID] srv.lock.Unlock() - if isconnected || dialing[dest.ID] { + if isconnected || dialing[dest.ID] || dest.ID == srv.ntab.Self() { continue } -- cgit v1.2.3 From cf754b9483a61075cf50eb4846eeecdc48ad37c0 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 15:04:30 +0100 Subject: p2p/discover: fix race in ListenUDP udp.Table was assigned after the readLoop started, so packets could arrive and be processed before the Table was there. --- p2p/discover/udp.go | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) (limited to 'p2p') diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go index a9ed7fcb8..b2a895442 100644 --- a/p2p/discover/udp.go +++ b/p2p/discover/udp.go @@ -124,35 +124,14 @@ type reply struct { // ListenUDP returns a new table that listens for UDP packets on laddr. func ListenUDP(priv *ecdsa.PrivateKey, laddr string, natm nat.Interface) (*Table, error) { - t, realaddr, err := listen(priv, laddr, natm) - if err != nil { - return nil, err - } - if natm != nil { - if !realaddr.IP.IsLoopback() { - go nat.Map(natm, t.closing, "udp", realaddr.Port, realaddr.Port, "ethereum discovery") - } - // TODO: react to external IP changes over time. - if ext, err := natm.ExternalIP(); err == nil { - realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port} - } - } - t.Table = newTable(t, PubkeyID(&priv.PublicKey), realaddr) - log.Infoln("Listening, ", t.self) - return t.Table, nil -} - -func listen(priv *ecdsa.PrivateKey, laddr string, nat nat.Interface) (*udp, *net.UDPAddr, error) { addr, err := net.ResolveUDPAddr("udp", laddr) if err != nil { - return nil, nil, err + return nil, err } conn, err := net.ListenUDP("udp", addr) if err != nil { - return nil, nil, err + return nil, err } - realaddr := conn.LocalAddr().(*net.UDPAddr) - udp := &udp{ conn: conn, priv: priv, @@ -160,9 +139,23 @@ func listen(priv *ecdsa.PrivateKey, laddr string, nat nat.Interface) (*udp, *net addpending: make(chan *pending), replies: make(chan reply), } + + realaddr := conn.LocalAddr().(*net.UDPAddr) + if natm != nil { + if !realaddr.IP.IsLoopback() { + go nat.Map(natm, udp.closing, "udp", realaddr.Port, realaddr.Port, "ethereum discovery") + } + // TODO: react to external IP changes over time. + if ext, err := natm.ExternalIP(); err == nil { + realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port} + } + } + udp.Table = newTable(udp, PubkeyID(&priv.PublicKey), realaddr) + go udp.loop() go udp.readLoop() - return udp, realaddr, nil + log.Infoln("Listening, ", udp.self) + return udp.Table, nil } func (t *udp) close() { -- cgit v1.2.3 From fd3e1061e01690c7046a0d80635284c1592b7699 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 15:06:06 +0100 Subject: p2p: handle disconnect before protocol handshake --- p2p/peer.go | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'p2p') diff --git a/p2p/peer.go b/p2p/peer.go index f779c1c02..6aa78045b 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -255,6 +255,13 @@ func readProtocolHandshake(p *Peer, rw MsgReadWriter) error { if err != nil { return err } + if msg.Code == discMsg { + // disconnect before protocol handshake is valid according to the + // spec and we send it ourself if Server.addPeer fails. + var reason DiscReason + rlp.Decode(msg.Payload, &reason) + return discRequestedError(reason) + } if msg.Code != handshakeMsg { return newPeerError(errProtocolBreach, "expected handshake, got %x", msg.Code) } -- cgit v1.2.3 From 32a9c0ca809508c1648b8f44f3e09725af7a80d3 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 15:08:40 +0100 Subject: p2p: bump devp2p protcol version to 3 For compatibility with cpp-ethereum --- p2p/peer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/peer.go b/p2p/peer.go index 6aa78045b..fd5bec7d5 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -16,7 +16,7 @@ import ( ) const ( - baseProtocolVersion = 2 + baseProtocolVersion = 3 baseProtocolLength = uint64(16) baseProtocolMaxMsgSize = 10 * 1024 * 1024 -- cgit v1.2.3 From 4bef3ce284574c7e0e9a76004e076fc686b13bf6 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 13 Feb 2015 23:54:34 +0100 Subject: p2p: print Cap as name/version --- p2p/protocol.go | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'p2p') diff --git a/p2p/protocol.go b/p2p/protocol.go index fe359fc54..5fa395eda 100644 --- a/p2p/protocol.go +++ b/p2p/protocol.go @@ -1,5 +1,7 @@ package p2p +import "fmt" + // Protocol represents a P2P subprotocol implementation. type Protocol struct { // Name should contain the official protocol name, @@ -37,6 +39,10 @@ func (cap Cap) RlpData() interface{} { return []interface{}{cap.Name, cap.Version} } +func (cap Cap) String() string { + return fmt.Sprintf("%s/%d", cap.Name, cap.Version) +} + type capsByName []Cap func (cs capsByName) Len() int { return len(cs) } -- cgit v1.2.3 From 84f7c966f725ef0f5c62b4427857d112c0d1e828 Mon Sep 17 00:00:00 2001 From: obscuren Date: Sat, 14 Feb 2015 00:25:47 +0100 Subject: Moved ECIES to repo & added secondary title for webview * ECIES moved from obscuren to ethereum * Added html META[name=badge] to reflect menuItem.secondaryTitle --- p2p/crypto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go index 2692d708c..7e4b43712 100644 --- a/p2p/crypto.go +++ b/p2p/crypto.go @@ -8,10 +8,10 @@ import ( "io" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" "github.com/ethereum/go-ethereum/crypto/secp256k1" ethlogger "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/p2p/discover" - "github.com/obscuren/ecies" ) var clogger = ethlogger.NewLogger("CRYPTOID") -- cgit v1.2.3 From 09e53367a24025564686af42c8349d2bd05729c3 Mon Sep 17 00:00:00 2001 From: obscuren Date: Sun, 15 Feb 2015 02:12:14 +0100 Subject: Use a mutex write-lock for a write operation --- p2p/server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'p2p') diff --git a/p2p/server.go b/p2p/server.go index e510be521..35b584a27 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -436,15 +436,15 @@ func (self *BlacklistMap) Exists(pubkey []byte) (ok bool) { } func (self *BlacklistMap) Put(pubkey []byte) error { - self.lock.RLock() - defer self.lock.RUnlock() + self.lock.Lock() + defer self.lock.Unlock() self.blacklist[string(pubkey)] = true return nil } func (self *BlacklistMap) Delete(pubkey []byte) error { - self.lock.RLock() - defer self.lock.RUnlock() + self.lock.Lock() + defer self.lock.Unlock() delete(self.blacklist, string(pubkey)) return nil } -- cgit v1.2.3 From 34d0e1b2c32d1bfe3aaa8519cf760ce499315ad5 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 17 Feb 2015 12:04:02 +0100 Subject: p2p: fix ecies dependency in tests We forgot to update this reference when moving ecies into the go-ethereum repo. --- p2p/crypto_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/crypto_test.go b/p2p/crypto_test.go index 0a9d49f96..6cf0f4818 100644 --- a/p2p/crypto_test.go +++ b/p2p/crypto_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/crypto" - "github.com/obscuren/ecies" + "github.com/ethereum/go-ethereum/crypto/ecies" ) func TestPublicKeyEncoding(t *testing.T) { -- cgit v1.2.3 From f965f41b6e8d177f4f06a8421ee071350813a1ac Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 17 Feb 2015 13:10:11 +0100 Subject: p2p/nat: switch to github.com/huin/goupnp My temporary fix was merged upstream. --- p2p/nat/natupnp.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'p2p') diff --git a/p2p/nat/natupnp.go b/p2p/nat/natupnp.go index 25cbc0d71..ef7765e8d 100644 --- a/p2p/nat/natupnp.go +++ b/p2p/nat/natupnp.go @@ -7,9 +7,9 @@ import ( "strings" "time" - "github.com/fjl/goupnp" - "github.com/fjl/goupnp/dcps/internetgateway1" - "github.com/fjl/goupnp/dcps/internetgateway2" + "github.com/huin/goupnp" + "github.com/huin/goupnp/dcps/internetgateway1" + "github.com/huin/goupnp/dcps/internetgateway2" ) type upnp struct { -- cgit v1.2.3 From 7ea131d4ffddaae15c697fe0d025ff431bacc01a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 17 Feb 2015 15:14:12 +0100 Subject: p2p/discover: fix pending replies iteration Range expressions capture the length of the slice once before the first iteration. A range expression cannot be used here since the loop modifies the slice variable (including length changes). --- p2p/discover/udp.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'p2p') diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go index b2a895442..69e9f3c2e 100644 --- a/p2p/discover/udp.go +++ b/p2p/discover/udp.go @@ -253,7 +253,8 @@ func (t *udp) loop() { case reply := <-t.replies: // run matching callbacks, remove if they return false. - for i, p := range pending { + for i := 0; i < len(pending); i++ { + p := pending[i] if reply.from == p.from && reply.ptype == p.ptype && p.callback(reply.data) { p.errc <- nil copy(pending[i:], pending[i+1:]) -- cgit v1.2.3 From 73f94f37559ca0c8739c7dddeaf46d36827fdf30 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 19 Feb 2015 01:52:03 +0100 Subject: p2p: disable encryption handshake The diff is a bit bigger than expected because the protocol handshake logic has moved out of Peer. This is necessary because the protocol handshake will have custom framing in the final protocol. --- p2p/crypto.go | 363 ----------------------------------------- p2p/crypto_test.go | 167 ------------------- p2p/handshake.go | 434 ++++++++++++++++++++++++++++++++++++++++++++++++++ p2p/handshake_test.go | 224 ++++++++++++++++++++++++++ p2p/message.go | 2 +- p2p/peer.go | 229 +++++++------------------- p2p/peer_test.go | 105 +++--------- p2p/server.go | 38 +++-- p2p/server_test.go | 12 +- 9 files changed, 767 insertions(+), 807 deletions(-) delete mode 100644 p2p/crypto.go delete mode 100644 p2p/crypto_test.go create mode 100644 p2p/handshake.go create mode 100644 p2p/handshake_test.go (limited to 'p2p') diff --git a/p2p/crypto.go b/p2p/crypto.go deleted file mode 100644 index 7e4b43712..000000000 --- a/p2p/crypto.go +++ /dev/null @@ -1,363 +0,0 @@ -package p2p - -import ( - // "binary" - "crypto/ecdsa" - "crypto/rand" - "fmt" - "io" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/ethereum/go-ethereum/crypto/secp256k1" - ethlogger "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/p2p/discover" -) - -var clogger = ethlogger.NewLogger("CRYPTOID") - -const ( - 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 - iHSLen = authMsgLen + eciesBytes // size of the final ECIES payload sent as initiator's handshake - rHSLen = authRespLen + eciesBytes // size of the final ECIES payload sent as receiver's handshake -) - -type hexkey []byte - -func (self hexkey) String() string { - return fmt.Sprintf("(%d) %x", len(self), []byte(self)) -} - -func encHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, dial *discover.Node) ( - remoteID discover.NodeID, - sessionToken []byte, - err error, -) { - if dial == nil { - var remotePubkey []byte - sessionToken, remotePubkey, err = inboundEncHandshake(conn, prv, nil) - copy(remoteID[:], remotePubkey) - } else { - remoteID = dial.ID - sessionToken, err = outboundEncHandshake(conn, prv, remoteID[:], nil) - } - return remoteID, sessionToken, err -} - -// outboundEncHandshake negotiates a session token on conn. -// it should be called on the dialing side of the connection. -// -// privateKey is the local client's private key -// remotePublicKey is the remote peer's node ID -// sessionToken is the token from a previous session with this node. -func outboundEncHandshake(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, remotePublicKey []byte, sessionToken []byte) ( - newSessionToken []byte, - err error, -) { - auth, initNonce, randomPrivKey, err := authMsg(prvKey, remotePublicKey, sessionToken) - if err != nil { - return nil, err - } - if sessionToken != nil { - clogger.Debugf("session-token: %v", hexkey(sessionToken)) - } - - clogger.Debugf("initiator-nonce: %v", hexkey(initNonce)) - clogger.Debugf("initiator-random-private-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) - randomPublicKeyS, _ := exportPublicKey(&randomPrivKey.PublicKey) - clogger.Debugf("initiator-random-public-key: %v", hexkey(randomPublicKeyS)) - if _, err = conn.Write(auth); err != nil { - return nil, err - } - clogger.Debugf("initiator handshake: %v", hexkey(auth)) - - response := make([]byte, rHSLen) - if _, err = io.ReadFull(conn, response); err != nil { - return nil, err - } - recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prvKey) - if err != nil { - return nil, err - } - - clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) - remoteRandomPubKeyS, _ := exportPublicKey(remoteRandomPubKey) - clogger.Debugf("receiver-random-public-key: %v", hexkey(remoteRandomPubKeyS)) - return newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) -} - -// authMsg creates the initiator handshake. -func authMsg(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte) ( - auth, initNonce []byte, - randomPrvKey *ecdsa.PrivateKey, - err error, -) { - // session init, common to both parties - remotePubKey, err := importPublicKey(remotePubKeyS) - if err != nil { - return - } - - var tokenFlag byte // = 0x00 - if sessionToken == 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 - if sessionToken, err = ecies.ImportECDSA(prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { - return - } - // tokenFlag = 0x00 // redundant - } else { - // for known peers, we use stored token from the previous session - tokenFlag = 0x01 - } - - //E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) - // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) - // allocate msgLen long message, - var msg []byte = make([]byte, authMsgLen) - initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1] - if _, err = rand.Read(initNonce); err != nil { - return - } - // create known message - // ecdh-shared-secret^nonce for new peers - // token^nonce for old peers - var sharedSecret = xor(sessionToken, initNonce) - - // generate random keypair to use for signing - if randomPrvKey, err = crypto.GenerateKey(); err != nil { - return - } - // sign shared secret (message known to both parties): shared-secret - var signature []byte - // signature = sign(ecdhe-random, shared-secret) - // uses secp256k1.Sign - if signature, err = crypto.Sign(sharedSecret, randomPrvKey); err != nil { - return - } - - // message - // signed-shared-secret || H(ecdhe-random-pubk) || pubk || nonce || 0x0 - copy(msg, signature) // copy signed-shared-secret - // H(ecdhe-random-pubk) - var randomPubKey64 []byte - if randomPubKey64, err = exportPublicKey(&randomPrvKey.PublicKey); err != nil { - return - } - var pubKey64 []byte - if pubKey64, err = exportPublicKey(&prvKey.PublicKey); err != nil { - return - } - copy(msg[sigLen:sigLen+shaLen], crypto.Sha3(randomPubKey64)) - // pubkey copied to the correct segment. - copy(msg[sigLen+shaLen:sigLen+shaLen+pubLen], pubKey64) - // nonce is already in the slice - // stick tokenFlag byte to the end - msg[authMsgLen-1] = tokenFlag - - // encrypt using remote-pubk - // auth = eciesEncrypt(remote-pubk, msg) - if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { - return - } - return -} - -// completeHandshake is called when the initiator receives an -// authentication response (aka receiver handshake). It completes the -// handshake by reading off parameters the remote peer provides needed -// to set up the secure session. -func completeHandshake(auth []byte, prvKey *ecdsa.PrivateKey) ( - respNonce []byte, - remoteRandomPubKey *ecdsa.PublicKey, - tokenFlag bool, - err error, -) { - var msg []byte - // they prove that msg is meant for me, - // I prove I possess private key if i can read it - if msg, err = crypto.Decrypt(prvKey, auth); err != nil { - return - } - - respNonce = msg[pubLen : pubLen+shaLen] - var remoteRandomPubKeyS = msg[:pubLen] - if remoteRandomPubKey, err = importPublicKey(remoteRandomPubKeyS); err != nil { - return - } - if msg[authRespLen-1] == 0x01 { - tokenFlag = true - } - return -} - -// inboundEncHandshake negotiates a session token on conn. -// it should be called on the listening side of the connection. -// -// privateKey is the local client's private key -// sessionToken is the token from a previous session with this node. -func inboundEncHandshake(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, sessionToken []byte) ( - token, remotePubKey []byte, - err error, -) { - // we are listening connection. we are responders in the - // handshake. Extract info from the authentication. The initiator - // starts by sending us a handshake that we need to respond to. so - // we read auth message first, then respond. - auth := make([]byte, iHSLen) - if _, err := io.ReadFull(conn, auth); err != nil { - return nil, nil, err - } - response, recNonce, initNonce, remotePubKey, randomPrivKey, remoteRandomPubKey, err := authResp(auth, sessionToken, prvKey) - if err != nil { - return nil, nil, err - } - clogger.Debugf("receiver-nonce: %v", hexkey(recNonce)) - clogger.Debugf("receiver-random-priv-key: %v", hexkey(crypto.FromECDSA(randomPrivKey))) - if _, err = conn.Write(response); err != nil { - return nil, nil, err - } - clogger.Debugf("receiver handshake:\n%v", hexkey(response)) - token, err = newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) - return token, remotePubKey, err -} - -// authResp is called by peer if it accepted (but not -// initiated) the connection from the remote. It is passed the initiator -// handshake received and the session token belonging to the -// remote initiator. -// -// The first return value is the authentication response (aka receiver -// handshake) that is to be sent to the remote initiator. -func authResp(auth, sessionToken []byte, prvKey *ecdsa.PrivateKey) ( - authResp, respNonce, initNonce, remotePubKeyS []byte, - randomPrivKey *ecdsa.PrivateKey, - remoteRandomPubKey *ecdsa.PublicKey, - err error, -) { - // they prove that msg is meant for me, - // I prove I possess private key if i can read it - msg, err := crypto.Decrypt(prvKey, auth) - if err != nil { - return - } - - remotePubKeyS = msg[sigLen+shaLen : sigLen+shaLen+pubLen] - remotePubKey, _ := importPublicKey(remotePubKeyS) - - var tokenFlag byte - if sessionToken == 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 - if sessionToken, err = ecies.ImportECDSA(prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { - return - } - // tokenFlag = 0x00 // redundant - } else { - // for known peers, we use stored token from the previous session - tokenFlag = 0x01 - } - - // the initiator nonce is read off the end of the message - initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1] - // I prove that i own prv key (to derive shared secret, and read - // nonce off encrypted msg) and that I own shared secret they - // prove they own the private key belonging to ecdhe-random-pubk - // we can now reconstruct the signed message and recover the peers - // pubkey - var signedMsg = xor(sessionToken, initNonce) - var remoteRandomPubKeyS []byte - if remoteRandomPubKeyS, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { - return - } - // convert to ECDSA standard - if remoteRandomPubKey, err = importPublicKey(remoteRandomPubKeyS); err != nil { - return - } - - // now we find ourselves a long task too, fill it random - var resp = make([]byte, authRespLen) - // generate shaLen long nonce - respNonce = resp[pubLen : pubLen+shaLen] - if _, err = rand.Read(respNonce); err != nil { - return - } - // generate random keypair for session - if randomPrivKey, err = crypto.GenerateKey(); err != nil { - return - } - // responder auth message - // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) - var randomPubKeyS []byte - if randomPubKeyS, err = exportPublicKey(&randomPrivKey.PublicKey); err != nil { - return - } - copy(resp[:pubLen], randomPubKeyS) - // nonce is already in the slice - resp[authRespLen-1] = tokenFlag - - // encrypt using remote-pubk - // auth = eciesEncrypt(remote-pubk, msg) - // why not encrypt with ecdhe-random-remote - if authResp, err = crypto.Encrypt(remotePubKey, resp); err != nil { - return - } - return -} - -// newSession is called after the handshake is completed. The -// arguments are values negotiated in the handshake. The return value -// is a new session Token to be remembered for the next time we -// connect with this peer. -func newSession(initNonce, respNonce []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) ([]byte, error) { - // 3) Now we can trust ecdhe-random-pubk to derive new keys - //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) - pubKey := ecies.ImportECDSAPublic(remoteRandomPubKey) - dhSharedSecret, err := ecies.ImportECDSA(privKey).GenerateShared(pubKey, sskLen, sskLen) - if err != nil { - return nil, err - } - sharedSecret := crypto.Sha3(dhSharedSecret, crypto.Sha3(respNonce, initNonce)) - sessionToken := crypto.Sha3(sharedSecret) - return sessionToken, nil -} - -// importPublicKey unmarshals 512 bit public keys. -func importPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err 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)) - } - return crypto.ToECDSAPub(pubKey65), nil -} - -func exportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { - if pubKeyEC == nil { - return nil, fmt.Errorf("no ECDSA public key given") - } - return crypto.FromECDSAPub(pubKeyEC)[1:], nil -} - -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/crypto_test.go b/p2p/crypto_test.go deleted file mode 100644 index 6cf0f4818..000000000 --- a/p2p/crypto_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package p2p - -import ( - "bytes" - "crypto/ecdsa" - "crypto/rand" - "net" - "testing" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" -) - -func TestPublicKeyEncoding(t *testing.T) { - prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) - pub0 := &prv0.PublicKey - pub0s := crypto.FromECDSAPub(pub0) - pub1, err := importPublicKey(pub0s) - if err != nil { - t.Errorf("%v", err) - } - eciesPub1 := ecies.ImportECDSAPublic(pub1) - if eciesPub1 == nil { - t.Errorf("invalid ecdsa public key") - } - pub1s, err := exportPublicKey(pub1) - if err != nil { - t.Errorf("%v", err) - } - if len(pub1s) != 64 { - t.Errorf("wrong length expect 64, got", len(pub1s)) - } - pub2, err := importPublicKey(pub1s) - if err != nil { - t.Errorf("%v", err) - } - pub2s, err := exportPublicKey(pub2) - if err != nil { - t.Errorf("%v", err) - } - if !bytes.Equal(pub1s, pub2s) { - t.Errorf("exports dont match") - } - pub2sEC := crypto.FromECDSAPub(pub2) - if !bytes.Equal(pub0s, pub2sEC) { - t.Errorf("exports dont match") - } -} - -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 TestCryptoHandshake(t *testing.T) { - testCryptoHandshake(newkey(), newkey(), nil, t) -} - -func TestCryptoHandshakeWithToken(t *testing.T) { - sessionToken := make([]byte, shaLen) - rand.Read(sessionToken) - testCryptoHandshake(newkey(), newkey(), sessionToken, t) -} - -func testCryptoHandshake(prv0, prv1 *ecdsa.PrivateKey, sessionToken []byte, t *testing.T) { - var err error - // pub0 := &prv0.PublicKey - pub1 := &prv1.PublicKey - - // pub0s := crypto.FromECDSAPub(pub0) - pub1s := crypto.FromECDSAPub(pub1) - - // simulate handshake by feeding output to input - // initiator sends handshake 'auth' - auth, initNonce, randomPrivKey, err := authMsg(prv0, pub1s, sessionToken) - if err != nil { - t.Errorf("%v", err) - } - t.Logf("-> %v", hexkey(auth)) - - // receiver reads auth and responds with response - response, remoteRecNonce, remoteInitNonce, _, remoteRandomPrivKey, remoteInitRandomPubKey, err := authResp(auth, sessionToken, prv1) - if err != nil { - t.Errorf("%v", err) - } - t.Logf("<- %v\n", hexkey(response)) - - // initiator reads receiver's response and the key exchange completes - recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prv0) - if err != nil { - t.Errorf("completeHandshake error: %v", err) - } - - // now both parties should have the same session parameters - initSessionToken, err := newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) - if err != nil { - t.Errorf("newSession error: %v", err) - } - - recSessionToken, err := newSession(remoteInitNonce, remoteRecNonce, remoteRandomPrivKey, remoteInitRandomPubKey) - if err != nil { - t.Errorf("newSession error: %v", err) - } - - // fmt.Printf("\nauth (%v) %x\n\nresp (%v) %x\n\n", len(auth), auth, len(response), response) - - // fmt.Printf("\nauth %x\ninitNonce %x\nresponse%x\nremoteRecNonce %x\nremoteInitNonce %x\nremoteRandomPubKey %x\nrecNonce %x\nremoteInitRandomPubKey %x\ninitSessionToken %x\n\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, remoteInitRandomPubKey, initSessionToken) - - if !bytes.Equal(initNonce, remoteInitNonce) { - t.Errorf("nonces do not match") - } - if !bytes.Equal(recNonce, remoteRecNonce) { - t.Errorf("receiver nonces do not match") - } - if !bytes.Equal(initSessionToken, recSessionToken) { - t.Errorf("session tokens do not match") - } -} - -func TestHandshake(t *testing.T) { - defer testlog(t).detach() - - prv0, _ := crypto.GenerateKey() - prv1, _ := crypto.GenerateKey() - pub0s, _ := exportPublicKey(&prv0.PublicKey) - pub1s, _ := exportPublicKey(&prv1.PublicKey) - rw0, rw1 := net.Pipe() - tokens := make(chan []byte) - - go func() { - token, err := outboundEncHandshake(rw0, prv0, pub1s, nil) - if err != nil { - t.Errorf("outbound side error: %v", err) - } - tokens <- token - }() - go func() { - token, remotePubkey, err := inboundEncHandshake(rw1, prv1, nil) - if err != nil { - t.Errorf("inbound side error: %v", err) - } - if !bytes.Equal(remotePubkey, pub0s) { - t.Errorf("inbound side returned wrong remote pubkey\n got: %x\n want: %x", remotePubkey, pub0s) - } - tokens <- token - }() - - t1, t2 := <-tokens, <-tokens - if !bytes.Equal(t1, t2) { - t.Error("session token mismatch") - } -} diff --git a/p2p/handshake.go b/p2p/handshake.go new file mode 100644 index 000000000..614711eaf --- /dev/null +++ b/p2p/handshake.go @@ -0,0 +1,434 @@ +package p2p + +import ( + "crypto/ecdsa" + "crypto/rand" + "errors" + "fmt" + "io" + "net" + + "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/p2p/discover" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + 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 + iHSLen = authMsgLen + eciesBytes // size of the final ECIES payload sent as initiator's handshake + rHSLen = authRespLen + eciesBytes // size of the final ECIES payload sent as receiver's handshake +) + +type conn struct { + *frameRW + *protoHandshake +} + +func newConn(fd net.Conn, hs *protoHandshake) *conn { + return &conn{newFrameRW(fd, msgWriteTimeout), hs} +} + +// encHandshake represents information about the remote end +// of a connection that is negotiated during the encryption handshake. +type encHandshake struct { + ID discover.NodeID + IngressMAC []byte + EgressMAC []byte + Token []byte +} + +// protoHandshake is the RLP structure of the protocol handshake. +type protoHandshake struct { + Version uint64 + Name string + Caps []Cap + ListenPort uint64 + ID discover.NodeID +} + +// setupConn starts a protocol session on the given connection. +// It runs the encryption handshake and the protocol handshake. +// If dial is non-nil, the connection the local node is the initiator. +func setupConn(fd net.Conn, prv *ecdsa.PrivateKey, our *protoHandshake, dial *discover.Node) (*conn, error) { + if dial == nil { + return setupInboundConn(fd, prv, our) + } else { + return setupOutboundConn(fd, prv, our, dial) + } +} + +func setupInboundConn(fd net.Conn, prv *ecdsa.PrivateKey, our *protoHandshake) (*conn, error) { + // var remotePubkey []byte + // sessionToken, remotePubkey, err = inboundEncHandshake(fd, prv, nil) + // copy(remoteID[:], remotePubkey) + + rw := newFrameRW(fd, msgWriteTimeout) + rhs, err := readProtocolHandshake(rw, our) + if err != nil { + return nil, err + } + if err := writeProtocolHandshake(rw, our); err != nil { + return nil, fmt.Errorf("protocol write error: %v", err) + } + return &conn{rw, rhs}, nil +} + +func setupOutboundConn(fd net.Conn, prv *ecdsa.PrivateKey, our *protoHandshake, dial *discover.Node) (*conn, error) { + // remoteID = dial.ID + // sessionToken, err = outboundEncHandshake(fd, prv, remoteID[:], nil) + + rw := newFrameRW(fd, msgWriteTimeout) + if err := writeProtocolHandshake(rw, our); err != nil { + return nil, fmt.Errorf("protocol write error: %v", err) + } + rhs, err := readProtocolHandshake(rw, our) + if err != nil { + return nil, fmt.Errorf("protocol handshake read error: %v", err) + } + if rhs.ID != dial.ID { + return nil, errors.New("dialed node id mismatch") + } + return &conn{rw, rhs}, nil +} + +// outboundEncHandshake negotiates a session token on conn. +// it should be called on the dialing side of the connection. +// +// privateKey is the local client's private key +// remotePublicKey is the remote peer's node ID +// sessionToken is the token from a previous session with this node. +func outboundEncHandshake(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, remotePublicKey []byte, sessionToken []byte) ( + newSessionToken []byte, + err error, +) { + auth, initNonce, randomPrivKey, err := authMsg(prvKey, remotePublicKey, sessionToken) + if err != nil { + return nil, err + } + if _, err = conn.Write(auth); err != nil { + return nil, err + } + + response := make([]byte, rHSLen) + if _, err = io.ReadFull(conn, response); err != nil { + return nil, err + } + recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prvKey) + if err != nil { + return nil, err + } + + return newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) +} + +// authMsg creates the initiator handshake. +func authMsg(prvKey *ecdsa.PrivateKey, remotePubKeyS, sessionToken []byte) ( + auth, initNonce []byte, + randomPrvKey *ecdsa.PrivateKey, + err error, +) { + // session init, common to both parties + remotePubKey, err := importPublicKey(remotePubKeyS) + if err != nil { + return + } + + var tokenFlag byte // = 0x00 + if sessionToken == 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 + if sessionToken, err = ecies.ImportECDSA(prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { + return + } + // tokenFlag = 0x00 // redundant + } else { + // for known peers, we use stored token from the previous session + tokenFlag = 0x01 + } + + //E(remote-pubk, S(ecdhe-random, ecdh-shared-secret^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x0) + // E(remote-pubk, S(ecdhe-random, token^nonce) || H(ecdhe-random-pubk) || pubk || nonce || 0x1) + // allocate msgLen long message, + var msg []byte = make([]byte, authMsgLen) + initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1] + if _, err = rand.Read(initNonce); err != nil { + return + } + // create known message + // ecdh-shared-secret^nonce for new peers + // token^nonce for old peers + var sharedSecret = xor(sessionToken, initNonce) + + // generate random keypair to use for signing + if randomPrvKey, err = crypto.GenerateKey(); err != nil { + return + } + // sign shared secret (message known to both parties): shared-secret + var signature []byte + // signature = sign(ecdhe-random, shared-secret) + // uses secp256k1.Sign + if signature, err = crypto.Sign(sharedSecret, randomPrvKey); err != nil { + return + } + + // message + // signed-shared-secret || H(ecdhe-random-pubk) || pubk || nonce || 0x0 + copy(msg, signature) // copy signed-shared-secret + // H(ecdhe-random-pubk) + var randomPubKey64 []byte + if randomPubKey64, err = exportPublicKey(&randomPrvKey.PublicKey); err != nil { + return + } + var pubKey64 []byte + if pubKey64, err = exportPublicKey(&prvKey.PublicKey); err != nil { + return + } + copy(msg[sigLen:sigLen+shaLen], crypto.Sha3(randomPubKey64)) + // pubkey copied to the correct segment. + copy(msg[sigLen+shaLen:sigLen+shaLen+pubLen], pubKey64) + // nonce is already in the slice + // stick tokenFlag byte to the end + msg[authMsgLen-1] = tokenFlag + + // encrypt using remote-pubk + // auth = eciesEncrypt(remote-pubk, msg) + if auth, err = crypto.Encrypt(remotePubKey, msg); err != nil { + return + } + return +} + +// completeHandshake is called when the initiator receives an +// authentication response (aka receiver handshake). It completes the +// handshake by reading off parameters the remote peer provides needed +// to set up the secure session. +func completeHandshake(auth []byte, prvKey *ecdsa.PrivateKey) ( + respNonce []byte, + remoteRandomPubKey *ecdsa.PublicKey, + tokenFlag bool, + err error, +) { + var msg []byte + // they prove that msg is meant for me, + // I prove I possess private key if i can read it + if msg, err = crypto.Decrypt(prvKey, auth); err != nil { + return + } + + respNonce = msg[pubLen : pubLen+shaLen] + var remoteRandomPubKeyS = msg[:pubLen] + if remoteRandomPubKey, err = importPublicKey(remoteRandomPubKeyS); err != nil { + return + } + if msg[authRespLen-1] == 0x01 { + tokenFlag = true + } + return +} + +// inboundEncHandshake negotiates a session token on conn. +// it should be called on the listening side of the connection. +// +// privateKey is the local client's private key +// sessionToken is the token from a previous session with this node. +func inboundEncHandshake(conn io.ReadWriter, prvKey *ecdsa.PrivateKey, sessionToken []byte) ( + token, remotePubKey []byte, + err error, +) { + // we are listening connection. we are responders in the + // handshake. Extract info from the authentication. The initiator + // starts by sending us a handshake that we need to respond to. so + // we read auth message first, then respond. + auth := make([]byte, iHSLen) + if _, err := io.ReadFull(conn, auth); err != nil { + return nil, nil, err + } + response, recNonce, initNonce, remotePubKey, randomPrivKey, remoteRandomPubKey, err := authResp(auth, sessionToken, prvKey) + if err != nil { + return nil, nil, err + } + if _, err = conn.Write(response); err != nil { + return nil, nil, err + } + token, err = newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) + return token, remotePubKey, err +} + +// authResp is called by peer if it accepted (but not +// initiated) the connection from the remote. It is passed the initiator +// handshake received and the session token belonging to the +// remote initiator. +// +// The first return value is the authentication response (aka receiver +// handshake) that is to be sent to the remote initiator. +func authResp(auth, sessionToken []byte, prvKey *ecdsa.PrivateKey) ( + authResp, respNonce, initNonce, remotePubKeyS []byte, + randomPrivKey *ecdsa.PrivateKey, + remoteRandomPubKey *ecdsa.PublicKey, + err error, +) { + // they prove that msg is meant for me, + // I prove I possess private key if i can read it + msg, err := crypto.Decrypt(prvKey, auth) + if err != nil { + return + } + + remotePubKeyS = msg[sigLen+shaLen : sigLen+shaLen+pubLen] + remotePubKey, _ := importPublicKey(remotePubKeyS) + + var tokenFlag byte + if sessionToken == 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 + if sessionToken, err = ecies.ImportECDSA(prvKey).GenerateShared(ecies.ImportECDSAPublic(remotePubKey), sskLen, sskLen); err != nil { + return + } + // tokenFlag = 0x00 // redundant + } else { + // for known peers, we use stored token from the previous session + tokenFlag = 0x01 + } + + // the initiator nonce is read off the end of the message + initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1] + // I prove that i own prv key (to derive shared secret, and read + // nonce off encrypted msg) and that I own shared secret they + // prove they own the private key belonging to ecdhe-random-pubk + // we can now reconstruct the signed message and recover the peers + // pubkey + var signedMsg = xor(sessionToken, initNonce) + var remoteRandomPubKeyS []byte + if remoteRandomPubKeyS, err = secp256k1.RecoverPubkey(signedMsg, msg[:sigLen]); err != nil { + return + } + // convert to ECDSA standard + if remoteRandomPubKey, err = importPublicKey(remoteRandomPubKeyS); err != nil { + return + } + + // now we find ourselves a long task too, fill it random + var resp = make([]byte, authRespLen) + // generate shaLen long nonce + respNonce = resp[pubLen : pubLen+shaLen] + if _, err = rand.Read(respNonce); err != nil { + return + } + // generate random keypair for session + if randomPrivKey, err = crypto.GenerateKey(); err != nil { + return + } + // responder auth message + // E(remote-pubk, ecdhe-random-pubk || nonce || 0x0) + var randomPubKeyS []byte + if randomPubKeyS, err = exportPublicKey(&randomPrivKey.PublicKey); err != nil { + return + } + copy(resp[:pubLen], randomPubKeyS) + // nonce is already in the slice + resp[authRespLen-1] = tokenFlag + + // encrypt using remote-pubk + // auth = eciesEncrypt(remote-pubk, msg) + // why not encrypt with ecdhe-random-remote + if authResp, err = crypto.Encrypt(remotePubKey, resp); err != nil { + return + } + return +} + +// newSession is called after the handshake is completed. The +// arguments are values negotiated in the handshake. The return value +// is a new session Token to be remembered for the next time we +// connect with this peer. +func newSession(initNonce, respNonce []byte, privKey *ecdsa.PrivateKey, remoteRandomPubKey *ecdsa.PublicKey) ([]byte, error) { + // 3) Now we can trust ecdhe-random-pubk to derive new keys + //ecdhe-shared-secret = ecdh.agree(ecdhe-random, remote-ecdhe-random-pubk) + pubKey := ecies.ImportECDSAPublic(remoteRandomPubKey) + dhSharedSecret, err := ecies.ImportECDSA(privKey).GenerateShared(pubKey, sskLen, sskLen) + if err != nil { + return nil, err + } + sharedSecret := crypto.Sha3(dhSharedSecret, crypto.Sha3(respNonce, initNonce)) + sessionToken := crypto.Sha3(sharedSecret) + return sessionToken, nil +} + +// importPublicKey unmarshals 512 bit public keys. +func importPublicKey(pubKey []byte) (pubKeyEC *ecdsa.PublicKey, err 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)) + } + return crypto.ToECDSAPub(pubKey65), nil +} + +func exportPublicKey(pubKeyEC *ecdsa.PublicKey) (pubKey []byte, err error) { + if pubKeyEC == nil { + return nil, fmt.Errorf("no ECDSA public key given") + } + return crypto.FromECDSAPub(pubKeyEC)[1:], nil +} + +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 +} + +func writeProtocolHandshake(w MsgWriter, our *protoHandshake) error { + return EncodeMsg(w, handshakeMsg, our.Version, our.Name, our.Caps, our.ListenPort, our.ID[:]) +} + +func readProtocolHandshake(r MsgReader, our *protoHandshake) (*protoHandshake, error) { + // read and handle remote handshake + msg, err := r.ReadMsg() + if err != nil { + return nil, err + } + if msg.Code == discMsg { + // disconnect before protocol handshake is valid according to the + // spec and we send it ourself if Server.addPeer fails. + var reason DiscReason + rlp.Decode(msg.Payload, &reason) + return nil, discRequestedError(reason) + } + if msg.Code != handshakeMsg { + return nil, fmt.Errorf("expected handshake, got %x", msg.Code) + } + if msg.Size > baseProtocolMaxMsgSize { + return nil, fmt.Errorf("message too big (%d > %d)", msg.Size, baseProtocolMaxMsgSize) + } + var hs protoHandshake + if err := msg.Decode(&hs); err != nil { + return nil, err + } + // validate handshake info + if hs.Version != our.Version { + return nil, newPeerError(errP2PVersionMismatch, "required version %d, received %d\n", baseProtocolVersion, hs.Version) + } + if (hs.ID == discover.NodeID{}) { + return nil, newPeerError(errPubkeyInvalid, "missing") + } + return &hs, nil +} diff --git a/p2p/handshake_test.go b/p2p/handshake_test.go new file mode 100644 index 000000000..06c6a6932 --- /dev/null +++ b/p2p/handshake_test.go @@ -0,0 +1,224 @@ +package p2p + +import ( + "bytes" + "crypto/ecdsa" + "crypto/rand" + "net" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/ethereum/go-ethereum/p2p/discover" +) + +func TestPublicKeyEncoding(t *testing.T) { + prv0, _ := crypto.GenerateKey() // = ecdsa.GenerateKey(crypto.S256(), rand.Reader) + pub0 := &prv0.PublicKey + pub0s := crypto.FromECDSAPub(pub0) + pub1, err := importPublicKey(pub0s) + if err != nil { + t.Errorf("%v", err) + } + eciesPub1 := ecies.ImportECDSAPublic(pub1) + if eciesPub1 == nil { + t.Errorf("invalid ecdsa public key") + } + pub1s, err := exportPublicKey(pub1) + if err != nil { + t.Errorf("%v", err) + } + if len(pub1s) != 64 { + t.Errorf("wrong length expect 64, got", len(pub1s)) + } + pub2, err := importPublicKey(pub1s) + if err != nil { + t.Errorf("%v", err) + } + pub2s, err := exportPublicKey(pub2) + if err != nil { + t.Errorf("%v", err) + } + if !bytes.Equal(pub1s, pub2s) { + t.Errorf("exports dont match") + } + pub2sEC := crypto.FromECDSAPub(pub2) + if !bytes.Equal(pub0s, pub2sEC) { + t.Errorf("exports dont match") + } +} + +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 TestCryptoHandshake(t *testing.T) { + testCryptoHandshake(newkey(), newkey(), nil, t) +} + +func TestCryptoHandshakeWithToken(t *testing.T) { + sessionToken := make([]byte, shaLen) + rand.Read(sessionToken) + testCryptoHandshake(newkey(), newkey(), sessionToken, t) +} + +func testCryptoHandshake(prv0, prv1 *ecdsa.PrivateKey, sessionToken []byte, t *testing.T) { + var err error + // pub0 := &prv0.PublicKey + pub1 := &prv1.PublicKey + + // pub0s := crypto.FromECDSAPub(pub0) + pub1s := crypto.FromECDSAPub(pub1) + + // simulate handshake by feeding output to input + // initiator sends handshake 'auth' + auth, initNonce, randomPrivKey, err := authMsg(prv0, pub1s, sessionToken) + if err != nil { + t.Errorf("%v", err) + } + // t.Logf("-> %v", hexkey(auth)) + + // receiver reads auth and responds with response + response, remoteRecNonce, remoteInitNonce, _, remoteRandomPrivKey, remoteInitRandomPubKey, err := authResp(auth, sessionToken, prv1) + if err != nil { + t.Errorf("%v", err) + } + // t.Logf("<- %v\n", hexkey(response)) + + // initiator reads receiver's response and the key exchange completes + recNonce, remoteRandomPubKey, _, err := completeHandshake(response, prv0) + if err != nil { + t.Errorf("completeHandshake error: %v", err) + } + + // now both parties should have the same session parameters + initSessionToken, err := newSession(initNonce, recNonce, randomPrivKey, remoteRandomPubKey) + if err != nil { + t.Errorf("newSession error: %v", err) + } + + recSessionToken, err := newSession(remoteInitNonce, remoteRecNonce, remoteRandomPrivKey, remoteInitRandomPubKey) + if err != nil { + t.Errorf("newSession error: %v", err) + } + + // fmt.Printf("\nauth (%v) %x\n\nresp (%v) %x\n\n", len(auth), auth, len(response), response) + + // fmt.Printf("\nauth %x\ninitNonce %x\nresponse%x\nremoteRecNonce %x\nremoteInitNonce %x\nremoteRandomPubKey %x\nrecNonce %x\nremoteInitRandomPubKey %x\ninitSessionToken %x\n\n", auth, initNonce, response, remoteRecNonce, remoteInitNonce, remoteRandomPubKey, recNonce, remoteInitRandomPubKey, initSessionToken) + + if !bytes.Equal(initNonce, remoteInitNonce) { + t.Errorf("nonces do not match") + } + if !bytes.Equal(recNonce, remoteRecNonce) { + t.Errorf("receiver nonces do not match") + } + if !bytes.Equal(initSessionToken, recSessionToken) { + t.Errorf("session tokens do not match") + } +} + +func TestEncHandshake(t *testing.T) { + defer testlog(t).detach() + + prv0, _ := crypto.GenerateKey() + prv1, _ := crypto.GenerateKey() + pub0s, _ := exportPublicKey(&prv0.PublicKey) + pub1s, _ := exportPublicKey(&prv1.PublicKey) + rw0, rw1 := net.Pipe() + tokens := make(chan []byte) + + go func() { + token, err := outboundEncHandshake(rw0, prv0, pub1s, nil) + if err != nil { + t.Errorf("outbound side error: %v", err) + } + tokens <- token + }() + go func() { + token, remotePubkey, err := inboundEncHandshake(rw1, prv1, nil) + if err != nil { + t.Errorf("inbound side error: %v", err) + } + if !bytes.Equal(remotePubkey, pub0s) { + t.Errorf("inbound side returned wrong remote pubkey\n got: %x\n want: %x", remotePubkey, pub0s) + } + tokens <- token + }() + + t1, t2 := <-tokens, <-tokens + if !bytes.Equal(t1, t2) { + t.Error("session token mismatch") + } +} + +func TestSetupConn(t *testing.T) { + prv0, _ := crypto.GenerateKey() + prv1, _ := crypto.GenerateKey() + node0 := &discover.Node{ + ID: discover.PubkeyID(&prv0.PublicKey), + IP: net.IP{1, 2, 3, 4}, + TCPPort: 33, + } + node1 := &discover.Node{ + ID: discover.PubkeyID(&prv1.PublicKey), + IP: net.IP{5, 6, 7, 8}, + TCPPort: 44, + } + hs0 := &protoHandshake{ + Version: baseProtocolVersion, + ID: node0.ID, + Caps: []Cap{{"a", 0}, {"b", 2}}, + } + hs1 := &protoHandshake{ + Version: baseProtocolVersion, + ID: node1.ID, + Caps: []Cap{{"c", 1}, {"d", 3}}, + } + fd0, fd1 := net.Pipe() + + done := make(chan struct{}) + go func() { + defer close(done) + conn0, err := setupConn(fd0, prv0, hs0, node1) + if err != nil { + t.Errorf("outbound side error: %v", err) + return + } + if conn0.ID != node1.ID { + t.Errorf("outbound conn id mismatch: got %v, want %v", conn0.ID, node1.ID) + } + if !reflect.DeepEqual(conn0.Caps, hs1.Caps) { + t.Errorf("outbound caps mismatch: got %v, want %v", conn0.Caps, hs1.Caps) + } + }() + + conn1, err := setupConn(fd1, prv1, hs1, nil) + if err != nil { + t.Fatalf("inbound side error: %v", err) + } + if conn1.ID != node0.ID { + t.Errorf("inbound conn id mismatch: got %v, want %v", conn1.ID, node0.ID) + } + if !reflect.DeepEqual(conn1.Caps, hs0.Caps) { + t.Errorf("inbound caps mismatch: got %v, want %v", conn1.Caps, hs0.Caps) + } + + <-done +} diff --git a/p2p/message.go b/p2p/message.go index 07916f7b3..7adad4b09 100644 --- a/p2p/message.go +++ b/p2p/message.go @@ -197,7 +197,7 @@ func (rw *frameRW) ReadMsg() (msg Msg, err error) { return msg, err } if !bytes.HasPrefix(start, magicToken) { - return msg, fmt.Errorf("bad magic token %x", start[:4], magicToken) + return msg, fmt.Errorf("bad magic token %x", start[:4]) } size := binary.BigEndian.Uint32(start[4:]) diff --git a/p2p/peer.go b/p2p/peer.go index fd5bec7d5..b9bf0fd73 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -33,37 +33,14 @@ const ( peersMsg = 0x05 ) -// handshake is the RLP structure of the protocol handshake. -type handshake struct { - Version uint64 - Name string - Caps []Cap - ListenPort uint64 - NodeID discover.NodeID -} - // Peer represents a connected remote node. type Peer struct { // Peers have all the log methods. // Use them to display messages related to the peer. *logger.Logger - infoMu sync.Mutex - name string - caps []Cap - - ourID, remoteID *discover.NodeID - ourName string - - rw *frameRW - - // These fields maintain the running protocols. - protocols []Protocol - runlock sync.RWMutex // protects running - running map[string]*proto - - // disables protocol handshake, for testing - noHandshake bool + rw *conn + running map[string]*protoRW protoWG sync.WaitGroup protoErr chan error @@ -73,36 +50,27 @@ type Peer struct { // NewPeer returns a peer for testing purposes. func NewPeer(id discover.NodeID, name string, caps []Cap) *Peer { - conn, _ := net.Pipe() - peer := newPeer(conn, nil, "", nil, &id) - peer.setHandshakeInfo(name, caps) + pipe, _ := net.Pipe() + conn := newConn(pipe, &protoHandshake{ID: id, Name: name, Caps: caps}) + peer := newPeer(conn, nil) close(peer.closed) // ensures Disconnect doesn't block return peer } // ID returns the node's public key. func (p *Peer) ID() discover.NodeID { - return *p.remoteID + return p.rw.ID } // Name returns the node name that the remote node advertised. func (p *Peer) Name() string { - // this needs a lock because the information is part of the - // protocol handshake. - p.infoMu.Lock() - name := p.name - p.infoMu.Unlock() - return name + return p.rw.Name } // Caps returns the capabilities (supported subprotocols) of the remote peer. func (p *Peer) Caps() []Cap { - // this needs a lock because the information is part of the - // protocol handshake. - p.infoMu.Lock() - caps := p.caps - p.infoMu.Unlock() - return caps + // TODO: maybe return copy + return p.rw.Caps } // RemoteAddr returns the remote address of the network connection. @@ -126,30 +94,20 @@ func (p *Peer) Disconnect(reason DiscReason) { // String implements fmt.Stringer. func (p *Peer) String() string { - return fmt.Sprintf("Peer %.8x %v", p.remoteID[:], p.RemoteAddr()) + return fmt.Sprintf("Peer %.8x %v", p.rw.ID[:], p.RemoteAddr()) } -func newPeer(conn net.Conn, protocols []Protocol, ourName string, ourID, remoteID *discover.NodeID) *Peer { - logtag := fmt.Sprintf("Peer %.8x %v", remoteID[:], conn.RemoteAddr()) - return &Peer{ - Logger: logger.NewLogger(logtag), - rw: newFrameRW(conn, msgWriteTimeout), - ourID: ourID, - ourName: ourName, - remoteID: remoteID, - protocols: protocols, - running: make(map[string]*proto), - disc: make(chan DiscReason), - protoErr: make(chan error), - closed: make(chan struct{}), +func newPeer(conn *conn, protocols []Protocol) *Peer { + logtag := fmt.Sprintf("Peer %.8x %v", conn.ID[:], conn.RemoteAddr()) + p := &Peer{ + Logger: logger.NewLogger(logtag), + rw: conn, + running: matchProtocols(protocols, conn.Caps, conn), + disc: make(chan DiscReason), + protoErr: make(chan error), + closed: make(chan struct{}), } -} - -func (p *Peer) setHandshakeInfo(name string, caps []Cap) { - p.infoMu.Lock() - p.name = name - p.caps = caps - p.infoMu.Unlock() + return p } func (p *Peer) run() DiscReason { @@ -157,16 +115,9 @@ func (p *Peer) run() DiscReason { defer p.closeProtocols() defer close(p.closed) + p.startProtocols() go func() { readErr <- p.readLoop() }() - if !p.noHandshake { - if err := writeProtocolHandshake(p.rw, p.ourName, *p.ourID, p.protocols); err != nil { - p.DebugDetailf("Protocol handshake error: %v\n", err) - p.rw.Close() - return DiscProtocolError - } - } - // Wait for an error or disconnect. var reason DiscReason select { @@ -206,11 +157,6 @@ func (p *Peer) politeDisconnect(reason DiscReason) { } func (p *Peer) readLoop() error { - if !p.noHandshake { - if err := readProtocolHandshake(p, p.rw); err != nil { - return err - } - } for { msg, err := p.rw.ReadMsg() if err != nil { @@ -249,105 +195,51 @@ func (p *Peer) handle(msg Msg) error { return nil } -func readProtocolHandshake(p *Peer, rw MsgReadWriter) error { - // read and handle remote handshake - msg, err := rw.ReadMsg() - if err != nil { - return err - } - if msg.Code == discMsg { - // disconnect before protocol handshake is valid according to the - // spec and we send it ourself if Server.addPeer fails. - var reason DiscReason - rlp.Decode(msg.Payload, &reason) - return discRequestedError(reason) - } - if msg.Code != handshakeMsg { - return newPeerError(errProtocolBreach, "expected handshake, got %x", msg.Code) - } - if msg.Size > baseProtocolMaxMsgSize { - return newPeerError(errInvalidMsg, "message too big") - } - var hs handshake - if err := msg.Decode(&hs); err != nil { - return err - } - // validate handshake info - if hs.Version != baseProtocolVersion { - return newPeerError(errP2PVersionMismatch, "required version %d, received %d\n", - baseProtocolVersion, hs.Version) - } - if hs.NodeID == *p.remoteID { - return newPeerError(errPubkeyForbidden, "node ID mismatch") - } - // TODO: remove Caps with empty name - p.setHandshakeInfo(hs.Name, hs.Caps) - p.startSubprotocols(hs.Caps) - return nil -} - -func writeProtocolHandshake(w MsgWriter, name string, id discover.NodeID, ps []Protocol) error { - var caps []interface{} - for _, proto := range ps { - caps = append(caps, proto.cap()) - } - return EncodeMsg(w, handshakeMsg, baseProtocolVersion, name, caps, 0, id) -} - -// startProtocols starts matching named subprotocols. -func (p *Peer) startSubprotocols(caps []Cap) { +// matchProtocols creates structures for matching named subprotocols. +func matchProtocols(protocols []Protocol, caps []Cap, rw MsgReadWriter) map[string]*protoRW { sort.Sort(capsByName(caps)) - p.runlock.Lock() - defer p.runlock.Unlock() offset := baseProtocolLength + result := make(map[string]*protoRW) outer: for _, cap := range caps { - for _, proto := range p.protocols { - if proto.Name == cap.Name && - proto.Version == cap.Version && - p.running[cap.Name] == nil { - p.running[cap.Name] = p.startProto(offset, proto) + for _, proto := range protocols { + if proto.Name == cap.Name && proto.Version == cap.Version && result[cap.Name] == nil { + result[cap.Name] = &protoRW{Protocol: proto, offset: offset, in: make(chan Msg), w: rw} offset += proto.Length continue outer } } } + return result } -func (p *Peer) startProto(offset uint64, impl Protocol) *proto { - p.DebugDetailf("Starting protocol %s/%d\n", impl.Name, impl.Version) - rw := &proto{ - name: impl.Name, - in: make(chan Msg), - offset: offset, - maxcode: impl.Length, - w: p.rw, +func (p *Peer) startProtocols() { + for _, proto := range p.running { + proto := proto + p.DebugDetailf("Starting protocol %s/%d\n", proto.Name, proto.Version) + p.protoWG.Add(1) + go func() { + err := proto.Run(p, proto) + if err == nil { + p.DebugDetailf("Protocol %s/%d returned\n", proto.Name, proto.Version) + err = errors.New("protocol returned") + } else { + p.DebugDetailf("Protocol %s/%d error: %v\n", proto.Name, proto.Version, err) + } + select { + case p.protoErr <- err: + case <-p.closed: + } + p.protoWG.Done() + }() } - p.protoWG.Add(1) - go func() { - err := impl.Run(p, rw) - if err == nil { - p.DebugDetailf("Protocol %s/%d returned\n", impl.Name, impl.Version) - err = errors.New("protocol returned") - } else { - p.DebugDetailf("Protocol %s/%d error: %v\n", impl.Name, impl.Version, err) - } - select { - case p.protoErr <- err: - case <-p.closed: - } - p.protoWG.Done() - }() - return rw } // getProto finds the protocol responsible for handling // the given message code. -func (p *Peer) getProto(code uint64) (*proto, error) { - p.runlock.RLock() - defer p.runlock.RUnlock() +func (p *Peer) getProto(code uint64) (*protoRW, error) { for _, proto := range p.running { - if code >= proto.offset && code < proto.offset+proto.maxcode { + if code >= proto.offset && code < proto.offset+proto.Length { return proto, nil } } @@ -355,46 +247,43 @@ func (p *Peer) getProto(code uint64) (*proto, error) { } func (p *Peer) closeProtocols() { - p.runlock.RLock() for _, p := range p.running { close(p.in) } - p.runlock.RUnlock() p.protoWG.Wait() } // writeProtoMsg sends the given message on behalf of the given named protocol. // this exists because of Server.Broadcast. func (p *Peer) writeProtoMsg(protoName string, msg Msg) error { - p.runlock.RLock() proto, ok := p.running[protoName] - p.runlock.RUnlock() if !ok { return fmt.Errorf("protocol %s not handled by peer", protoName) } - if msg.Code >= proto.maxcode { + if msg.Code >= proto.Length { return newPeerError(errInvalidMsgCode, "code %x is out of range for protocol %q", msg.Code, protoName) } msg.Code += proto.offset return p.rw.WriteMsg(msg) } -type proto struct { - name string - in chan Msg - maxcode, offset uint64 - w MsgWriter +type protoRW struct { + Protocol + + in chan Msg + offset uint64 + w MsgWriter } -func (rw *proto) WriteMsg(msg Msg) error { - if msg.Code >= rw.maxcode { +func (rw *protoRW) WriteMsg(msg Msg) error { + if msg.Code >= rw.Length { return newPeerError(errInvalidMsgCode, "not handled") } msg.Code += rw.offset return rw.w.WriteMsg(msg) } -func (rw *proto) ReadMsg() (Msg, error) { +func (rw *protoRW) ReadMsg() (Msg, error) { msg, ok := <-rw.in if !ok { return msg, io.EOF diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 68c9910a2..a1260adbd 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -6,11 +6,9 @@ import ( "io/ioutil" "net" "reflect" - "sort" "testing" "time" - "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/rlp" ) @@ -23,6 +21,7 @@ var discard = Protocol{ if err != nil { return err } + fmt.Printf("discarding %d\n", msg.Code) if err = msg.Discard(); err != nil { return err } @@ -30,13 +29,20 @@ var discard = Protocol{ }, } -func testPeer(noHandshake bool, protos []Protocol) (*frameRW, *Peer, <-chan DiscReason) { - conn1, conn2 := net.Pipe() - peer := newPeer(conn1, protos, "name", &discover.NodeID{}, &discover.NodeID{}) - peer.noHandshake = noHandshake +func testPeer(protos []Protocol) (*conn, *Peer, <-chan DiscReason) { + fd1, fd2 := net.Pipe() + hs1 := &protoHandshake{ID: randomID(), Version: baseProtocolVersion} + hs2 := &protoHandshake{ID: randomID(), Version: baseProtocolVersion} + for _, p := range protos { + hs1.Caps = append(hs1.Caps, p.cap()) + hs2.Caps = append(hs2.Caps, p.cap()) + } + + peer := newPeer(newConn(fd1, hs1), protos) errc := make(chan DiscReason, 1) go func() { errc <- peer.run() }() - return newFrameRW(conn2, msgWriteTimeout), peer, errc + + return newConn(fd2, hs2), peer, errc } func TestPeerProtoReadMsg(t *testing.T) { @@ -61,9 +67,8 @@ func TestPeerProtoReadMsg(t *testing.T) { }, } - rw, peer, errc := testPeer(true, []Protocol{proto}) + rw, _, errc := testPeer([]Protocol{proto}) defer rw.Close() - peer.startSubprotocols([]Cap{proto.cap()}) EncodeMsg(rw, baseProtocolLength+2, 1) EncodeMsg(rw, baseProtocolLength+3, 2) @@ -100,9 +105,8 @@ func TestPeerProtoReadLargeMsg(t *testing.T) { }, } - rw, peer, errc := testPeer(true, []Protocol{proto}) + rw, _, errc := testPeer([]Protocol{proto}) defer rw.Close() - peer.startSubprotocols([]Cap{proto.cap()}) EncodeMsg(rw, 18, make([]byte, msgsize)) select { @@ -130,9 +134,8 @@ func TestPeerProtoEncodeMsg(t *testing.T) { return nil }, } - rw, peer, _ := testPeer(true, []Protocol{proto}) + rw, _, _ := testPeer([]Protocol{proto}) defer rw.Close() - peer.startSubprotocols([]Cap{proto.cap()}) if err := expectMsg(rw, 17, []string{"foo", "bar"}); err != nil { t.Error(err) @@ -142,9 +145,8 @@ func TestPeerProtoEncodeMsg(t *testing.T) { func TestPeerWriteForBroadcast(t *testing.T) { defer testlog(t).detach() - rw, peer, peerErr := testPeer(true, []Protocol{discard}) + rw, peer, peerErr := testPeer([]Protocol{discard}) defer rw.Close() - peer.startSubprotocols([]Cap{discard.cap()}) // test write errors if err := peer.writeProtoMsg("b", NewMsg(3)); err == nil { @@ -160,7 +162,7 @@ func TestPeerWriteForBroadcast(t *testing.T) { read := make(chan struct{}) go func() { if err := expectMsg(rw, 16, nil); err != nil { - t.Error() + t.Error(err) } close(read) }() @@ -179,7 +181,7 @@ func TestPeerWriteForBroadcast(t *testing.T) { func TestPeerPing(t *testing.T) { defer testlog(t).detach() - rw, _, _ := testPeer(true, nil) + rw, _, _ := testPeer(nil) defer rw.Close() if err := EncodeMsg(rw, pingMsg); err != nil { t.Fatal(err) @@ -192,7 +194,7 @@ func TestPeerPing(t *testing.T) { func TestPeerDisconnect(t *testing.T) { defer testlog(t).detach() - rw, _, disc := testPeer(true, nil) + rw, _, disc := testPeer(nil) defer rw.Close() if err := EncodeMsg(rw, discMsg, DiscQuitting); err != nil { t.Fatal(err) @@ -206,73 +208,6 @@ func TestPeerDisconnect(t *testing.T) { } } -func TestPeerHandshake(t *testing.T) { - defer testlog(t).detach() - - // remote has two matching protocols: a and c - remote := NewPeer(randomID(), "", []Cap{{"a", 1}, {"b", 999}, {"c", 3}}) - remoteID := randomID() - remote.ourID = &remoteID - remote.ourName = "remote peer" - - start := make(chan string) - stop := make(chan struct{}) - run := func(p *Peer, rw MsgReadWriter) error { - name := rw.(*proto).name - if name != "a" && name != "c" { - t.Errorf("protocol %q should not be started", name) - } else { - start <- name - } - <-stop - return nil - } - protocols := []Protocol{ - {Name: "a", Version: 1, Length: 1, Run: run}, - {Name: "b", Version: 2, Length: 1, Run: run}, - {Name: "c", Version: 3, Length: 1, Run: run}, - {Name: "d", Version: 4, Length: 1, Run: run}, - } - rw, p, disc := testPeer(false, protocols) - p.remoteID = remote.ourID - defer rw.Close() - - // run the handshake - remoteProtocols := []Protocol{protocols[0], protocols[2]} - if err := writeProtocolHandshake(rw, "remote peer", remoteID, remoteProtocols); err != nil { - t.Fatalf("handshake write error: %v", err) - } - if err := readProtocolHandshake(remote, rw); err != nil { - t.Fatalf("handshake read error: %v", err) - } - - // check that all protocols have been started - var started []string - for i := 0; i < 2; i++ { - select { - case name := <-start: - started = append(started, name) - case <-time.After(100 * time.Millisecond): - } - } - sort.Strings(started) - if !reflect.DeepEqual(started, []string{"a", "c"}) { - t.Errorf("wrong protocols started: %v", started) - } - - // check that metadata has been set - if p.ID() != remoteID { - t.Errorf("peer has wrong node ID: got %v, want %v", p.ID(), remoteID) - } - if p.Name() != remote.ourName { - t.Errorf("peer has wrong node name: got %q, want %q", p.Name(), remote.ourName) - } - - close(stop) - expectMsg(rw, discMsg, nil) - t.Logf("disc reason: %v", <-disc) -} - func TestNewPeer(t *testing.T) { name := "nodename" caps := []Cap{{"foo", 2}, {"bar", 3}} diff --git a/p2p/server.go b/p2p/server.go index 35b584a27..194dc3f1c 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -5,7 +5,6 @@ import ( "crypto/ecdsa" "errors" "fmt" - "io" "net" "runtime" "sync" @@ -83,9 +82,11 @@ type Server struct { // Hooks for testing. These are useful because we can inhibit // the whole protocol stack. - handshakeFunc + setupFunc newPeerHook + ourHandshake *protoHandshake + lock sync.RWMutex running bool listener net.Listener @@ -99,7 +100,7 @@ type Server struct { peerConnect chan *discover.Node } -type handshakeFunc func(io.ReadWriter, *ecdsa.PrivateKey, *discover.Node) (discover.NodeID, []byte, error) +type setupFunc func(net.Conn, *ecdsa.PrivateKey, *protoHandshake, *discover.Node) (*conn, error) type newPeerHook func(*Peer) // Peers returns all connected peers. @@ -170,8 +171,8 @@ func (srv *Server) Start() (err error) { srv.peers = make(map[discover.NodeID]*Peer) srv.peerConnect = make(chan *discover.Node) - if srv.handshakeFunc == nil { - srv.handshakeFunc = encHandshake + if srv.setupFunc == nil { + srv.setupFunc = setupConn } if srv.Blacklist == nil { srv.Blacklist = NewBlacklist() @@ -183,11 +184,17 @@ func (srv *Server) Start() (err error) { } // dial stuff - dt, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr, srv.NAT) + ntab, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr, srv.NAT) if err != nil { return err } - srv.ntab = dt + srv.ntab = ntab + + srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: ntab.Self()} + for _, p := range srv.Protocols { + srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap()) + } + if srv.Dialer == nil { srv.Dialer = &net.Dialer{Timeout: defaultDialTimeout} } @@ -347,18 +354,17 @@ func (srv *Server) findPeers() { } } -func (srv *Server) startPeer(conn net.Conn, dest *discover.Node) { +func (srv *Server) startPeer(fd net.Conn, dest *discover.Node) { // TODO: handle/store session token - conn.SetDeadline(time.Now().Add(handshakeTimeout)) - remoteID, _, err := srv.handshakeFunc(conn, srv.PrivateKey, dest) + fd.SetDeadline(time.Now().Add(handshakeTimeout)) + conn, err := srv.setupFunc(fd, srv.PrivateKey, srv.ourHandshake, dest) if err != nil { - conn.Close() - srvlog.Debugf("Encryption Handshake with %v failed: %v", conn.RemoteAddr(), err) + fd.Close() + srvlog.Debugf("Handshake with %v failed: %v", fd.RemoteAddr(), err) return } - ourID := srv.ntab.Self() - p := newPeer(conn, srv.Protocols, srv.Name, &ourID, &remoteID) - if ok, reason := srv.addPeer(remoteID, p); !ok { + p := newPeer(conn, srv.Protocols) + if ok, reason := srv.addPeer(conn.ID, p); !ok { srvlog.DebugDetailf("Not adding %v (%v)\n", p, reason) p.politeDisconnect(reason) return @@ -394,7 +400,7 @@ func (srv *Server) addPeer(id discover.NodeID, p *Peer) (bool, DiscReason) { func (srv *Server) removePeer(p *Peer) { srv.lock.Lock() - delete(srv.peers, *p.remoteID) + delete(srv.peers, p.ID()) srv.lock.Unlock() srv.peerWG.Done() } diff --git a/p2p/server_test.go b/p2p/server_test.go index aa2b3d243..c109fffb9 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -21,8 +21,12 @@ func startTestServer(t *testing.T, pf newPeerHook) *Server { ListenAddr: "127.0.0.1:0", PrivateKey: newkey(), newPeerHook: pf, - handshakeFunc: func(io.ReadWriter, *ecdsa.PrivateKey, *discover.Node) (id discover.NodeID, st []byte, err error) { - return randomID(), nil, err + setupFunc: func(fd net.Conn, prv *ecdsa.PrivateKey, our *protoHandshake, dial *discover.Node) (*conn, error) { + id := randomID() + return &conn{ + frameRW: newFrameRW(fd, msgWriteTimeout), + protoHandshake: &protoHandshake{ID: id, Version: baseProtocolVersion}, + }, nil }, } if err := server.Start(); err != nil { @@ -116,9 +120,7 @@ func TestServerBroadcast(t *testing.T) { var connected sync.WaitGroup srv := startTestServer(t, func(p *Peer) { - p.protocols = []Protocol{discard} - p.startSubprotocols([]Cap{discard.cap()}) - p.noHandshake = true + p.running = matchProtocols([]Protocol{discard}, []Cap{discard.cap()}, p.rw) connected.Done() }) defer srv.Stop() -- cgit v1.2.3 From 3dbd32093cd1060c339b3351fbb676b8c7cc31f0 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 19 Feb 2015 16:53:52 +0100 Subject: p2p: enable devp2p ping This should prevent connection drops. --- p2p/peer.go | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) (limited to 'p2p') diff --git a/p2p/peer.go b/p2p/peer.go index b9bf0fd73..fb027c834 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -21,6 +21,7 @@ const ( baseProtocolMaxMsgSize = 10 * 1024 * 1024 disconnectGracePeriod = 2 * time.Second + pingInterval = 15 * time.Second ) const ( @@ -118,19 +119,33 @@ func (p *Peer) run() DiscReason { p.startProtocols() go func() { readErr <- p.readLoop() }() + ping := time.NewTicker(pingInterval) + defer ping.Stop() + // Wait for an error or disconnect. var reason DiscReason - select { - case err := <-readErr: - // We rely on protocols to abort if there is a write error. It - // might be more robust to handle them here as well. - p.DebugDetailf("Read error: %v\n", err) - p.rw.Close() - return DiscNetworkError - - case err := <-p.protoErr: - reason = discReasonForError(err) - case reason = <-p.disc: +loop: + for { + select { + case <-ping.C: + go func() { + if err := EncodeMsg(p.rw, pingMsg, nil); err != nil { + p.protoErr <- err + return + } + }() + case err := <-readErr: + // We rely on protocols to abort if there is a write error. It + // might be more robust to handle them here as well. + p.DebugDetailf("Read error: %v\n", err) + p.rw.Close() + return DiscNetworkError + case err := <-p.protoErr: + reason = discReasonForError(err) + break loop + case reason = <-p.disc: + break loop + } } p.politeDisconnect(reason) -- cgit v1.2.3 From dd871e791cdd2aefd001f6c3a11ac9378dde0bf5 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 19 Feb 2015 17:08:18 +0100 Subject: p2p: initialize Server.ourHandshake before accepting connections --- p2p/server.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'p2p') diff --git a/p2p/server.go b/p2p/server.go index 194dc3f1c..923716011 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -160,7 +160,7 @@ func (srv *Server) Start() (err error) { } srvlog.Infoln("Starting Server") - // initialize all the fields + // static fields if srv.PrivateKey == nil { return fmt.Errorf("Server.PrivateKey must be set to a non-nil key") } @@ -170,31 +170,32 @@ func (srv *Server) Start() (err error) { srv.quit = make(chan struct{}) srv.peers = make(map[discover.NodeID]*Peer) srv.peerConnect = make(chan *discover.Node) - if srv.setupFunc == nil { srv.setupFunc = setupConn } if srv.Blacklist == nil { srv.Blacklist = NewBlacklist() } - if srv.ListenAddr != "" { - if err := srv.startListening(); err != nil { - return err - } - } - // dial stuff + // node table ntab, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr, srv.NAT) if err != nil { return err } srv.ntab = ntab + // handshake srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: ntab.Self()} for _, p := range srv.Protocols { srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap()) } + // listen/dial + if srv.ListenAddr != "" { + if err := srv.startListening(); err != nil { + return err + } + } if srv.Dialer == nil { srv.Dialer = &net.Dialer{Timeout: defaultDialTimeout} } -- cgit v1.2.3 From 3719db352a3de1c7daece71895abdd871616a2aa Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 19 Feb 2015 17:09:33 +0100 Subject: p2p: emit JSON connect/disconnect events --- p2p/server.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'p2p') diff --git a/p2p/server.go b/p2p/server.go index 923716011..3ea2538d1 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -22,6 +22,7 @@ const ( ) var srvlog = logger.NewLogger("P2P Server") +var srvjslog = logger.NewJsonLogger() // MakeName creates a node name that follows the ethereum convention // for such names. It adds the operation system name and Go runtime version @@ -370,14 +371,26 @@ func (srv *Server) startPeer(fd net.Conn, dest *discover.Node) { p.politeDisconnect(reason) return } + srvlog.Debugf("Added %v\n", p) + srvjslog.LogJson(&logger.P2PConnected{ + RemoteId: fmt.Sprintf("%x", conn.ID[:]), + RemoteAddress: conn.RemoteAddr().String(), + RemoteVersionString: conn.Name, + NumConnections: srv.PeerCount(), + }) if srv.newPeerHook != nil { srv.newPeerHook(p) } discreason := p.run() srv.removePeer(p) + srvlog.Debugf("Removed %v (%v)\n", p, discreason) + srvjslog.LogJson(&logger.P2PDisconnected{ + RemoteId: fmt.Sprintf("%x", conn.ID[:]), + NumConnections: srv.PeerCount(), + }) } func (srv *Server) addPeer(id discover.NodeID, p *Peer) (bool, DiscReason) { -- cgit v1.2.3