diff --git a/src/identify/index.js b/src/identify/index.js index cde3275033..304352afe7 100644 --- a/src/identify/index.js +++ b/src/identify/index.js @@ -1,7 +1,11 @@ 'use strict' -const { Buffer } = require('buffer') const debug = require('debug') +const log = debug('libp2p:identify') +log.error = debug('libp2p:identify:error') + +const errCode = require('err-code') +const { Buffer } = require('buffer') const pb = require('it-protocol-buffers') const lp = require('it-length-prefixed') const pipe = require('it-pipe') @@ -13,8 +17,8 @@ const { toBuffer } = require('it-buffer') const Message = require('./message') -const log = debug('libp2p:identify') -log.error = debug('libp2p:identify:error') +const Envelope = require('../record/envelope') +const PeerRecord = require('../record/peer-record') const { MULTICODEC_IDENTIFY, @@ -23,7 +27,6 @@ const { PROTOCOL_VERSION } = require('./consts') -const errCode = require('err-code') const { codes } = require('../errors') class IdentifyService { @@ -79,6 +82,9 @@ class IdentifyService { this._protocols = protocols this.handleMessage = this.handleMessage.bind(this) + + // TODO: this should be stored in the certified AddressBook in follow up PR + this._selfRecord = undefined } /** @@ -86,15 +92,20 @@ class IdentifyService { * @param {Array} connections * @returns {Promise} */ - push (connections) { + async push (connections) { + const signedPeerRecord = await this._getSelfPeerRecord() + const listenAddrs = this._libp2p.multiaddrs.map((ma) => ma.buffer) + const protocols = Array.from(this._protocols.keys()) + const pushes = connections.map(async connection => { try { const { stream } = await connection.newStream(MULTICODEC_IDENTIFY_PUSH) await pipe( [{ - listenAddrs: this._libp2p.multiaddrs.map((ma) => ma.buffer), - protocols: Array.from(this._protocols.keys()) + listenAddrs, + signedPeerRecord, + protocols }], pb.encode(Message), stream, @@ -160,7 +171,8 @@ class IdentifyService { publicKey, listenAddrs, protocols, - observedAddr + observedAddr, + signedPeerRecord } = message const id = await PeerId.createFromPubKey(publicKey) @@ -172,8 +184,26 @@ class IdentifyService { // Get the observedAddr if there is one observedAddr = IdentifyService.getCleanMultiaddr(observedAddr) + let addresses + + try { + const envelope = await Envelope.openAndCertify(signedPeerRecord, PeerRecord.DOMAIN) + const peerRecord = await PeerRecord.createFromProtobuf(envelope.payload) + + addresses = peerRecord.multiaddrs + } catch (err) { + log('received invalid envelope, discard it and fallback to listenAddrs is available', err) + // Try Legacy + addresses = listenAddrs + } + // Update peers data in PeerStore - this.peerStore.addressBook.set(id, listenAddrs.map((addr) => multiaddr(addr))) + try { + this.peerStore.addressBook.set(id, addresses.map((addr) => multiaddr(addr))) + } catch (err) { + log.error('received invalid addrs', err) + } + this.peerStore.protoBook.set(id, protocols) // TODO: Track our observed address so that we can score it @@ -201,8 +231,8 @@ class IdentifyService { } /** - * Sends the `Identify` response to the requesting peer over the - * given `connection` + * Sends the `Identify` response with the Signed Peer Record + * to the requesting peer over the given `connection` * @private * @param {object} options * @param {*} options.stream @@ -214,11 +244,14 @@ class IdentifyService { publicKey = this.peerId.pubKey.bytes } + const signedPeerRecord = await this._getSelfPeerRecord() + const message = Message.encode({ protocolVersion: PROTOCOL_VERSION, agentVersion: AGENT_VERSION, publicKey, listenAddrs: this._libp2p.multiaddrs.map((ma) => ma.buffer), + signedPeerRecord, observedAddr: connection.remoteAddr.buffer, protocols: Array.from(this._protocols.keys()) }) @@ -258,17 +291,56 @@ class IdentifyService { return log.error('received invalid message', err) } - // Update peers data in PeerStore const id = connection.remotePeer + + let addresses + try { - this.peerStore.addressBook.set(id, message.listenAddrs.map((addr) => multiaddr(addr))) + const envelope = await Envelope.openAndCertify(message.signedPeerRecord, PeerRecord.DOMAIN) + const peerRecord = await PeerRecord.createFromProtobuf(envelope.payload) + + addresses = peerRecord.multiaddrs } catch (err) { - return log.error('received invalid listen addrs', err) + log('received invalid envelope, discard it and fallback to listenAddrs is available', err) + // Try Legacy + addresses = message.listenAddrs + } + + try { + this.peerStore.addressBook.set(id, addresses.map((addr) => multiaddr(addr))) + } catch (err) { + log.error('received invalid addrs', err) } // Update the protocols this.peerStore.protoBook.set(id, message.protocols) } + + /** + * Get self signed peer record raw envelope. + * @return {Buffer} + */ + async _getSelfPeerRecord () { + // TODO: support invalidation when dynamic multiaddrs are supported + if (this._selfRecord) { + return this._selfRecord + } + + try { + const peerRecord = new PeerRecord({ + peerId: this.peerId, + multiaddrs: this._libp2p.multiaddrs + }) + const envelope = await Envelope.seal(peerRecord, this.peerId) + + this._selfRecord = envelope.marshal() + + return this._selfRecord + } catch (err) { + log.error('failed to get self peer record') + } + return null + } } module.exports.IdentifyService = IdentifyService diff --git a/src/identify/message.js b/src/identify/message.js index d81b1fd4b4..25b003f5c0 100644 --- a/src/identify/message.js +++ b/src/identify/message.js @@ -24,6 +24,11 @@ message Identify { optional bytes observedAddr = 4; repeated string protocols = 3; + + // signedPeerRecord contains a serialized SignedEnvelope containing a PeerRecord, + // signed by the sending node. It contains the same addresses as the listenAddrs field, but + // in a form that lets us share authenticated addrs with other peers. + optional bytes signedPeerRecord = 8; } ` diff --git a/src/record/peer-record/index.js b/src/record/peer-record/index.js index 68f987e060..432f95010b 100644 --- a/src/record/peer-record/index.js +++ b/src/record/peer-record/index.js @@ -95,4 +95,6 @@ PeerRecord.createFromProtobuf = (buf) => { return new PeerRecord({ peerId, multiaddrs, seqNumber }) } +PeerRecord.DOMAIN = ENVELOPE_DOMAIN_PEER_RECORD + module.exports = PeerRecord diff --git a/test/identify/index.spec.js b/test/identify/index.spec.js index 9ca892ff05..4560bfd3d7 100644 --- a/test/identify/index.spec.js +++ b/test/identify/index.spec.js @@ -18,10 +18,12 @@ const { codes: Errors } = require('../../src/errors') const { IdentifyService, multicodecs } = require('../../src/identify') const Peers = require('../fixtures/peers') const Libp2p = require('../../src') +const Envelope = require('../../src/record/envelope') const baseOptions = require('../utils/base-options.browser') const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') const remoteAddr = MULTIADDRS_WEBSOCKETS[0] +const listenMaddrs = [multiaddr('/ip4/127.0.0.1/tcp/15002/ws')] describe('Identify', () => { let localPeer @@ -55,15 +57,16 @@ describe('Identify', () => { set: () => { } } }, - multiaddrs: [] + multiaddrs: listenMaddrs }, protocols }) + const remoteIdentify = new IdentifyService({ libp2p: { peerId: remotePeer, connectionManager: new EventEmitter(), - multiaddrs: [] + multiaddrs: listenMaddrs }, protocols }) @@ -90,9 +93,73 @@ describe('Identify', () => { expect(localIdentify.peerStore.addressBook.set.callCount).to.equal(1) expect(localIdentify.peerStore.protoBook.set.callCount).to.equal(1) + // Validate the remote peer gets updated in the peer store const call = localIdentify.peerStore.addressBook.set.firstCall expect(call.args[0].id.bytes).to.equal(remotePeer.bytes) + expect(call.args[1]).to.exist() + expect(call.args[1]).have.lengthOf(listenMaddrs.length) + expect(call.args[1][0].equals(listenMaddrs[0])) + }) + + // LEGACY + it('should be able to identify another peer with no certified peer records support', async () => { + const localIdentify = new IdentifyService({ + libp2p: { + peerId: localPeer, + connectionManager: new EventEmitter(), + peerStore: { + addressBook: { + set: () => {} + }, + protoBook: { + set: () => {} + } + }, + multiaddrs: listenMaddrs + }, + protocols + }) + + const remoteIdentify = new IdentifyService({ + libp2p: { + peerId: remotePeer, + connectionManager: new EventEmitter(), + multiaddrs: listenMaddrs + }, + protocols + }) + + const observedAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') + const localConnectionMock = { newStream: () => {}, remotePeer } + const remoteConnectionMock = { remoteAddr: observedAddr } + + const [local, remote] = duplexPair() + sinon.stub(localConnectionMock, 'newStream').returns({ stream: local, protocol: multicodecs.IDENTIFY }) + sinon.stub(Envelope, 'openAndCertify').throws() + + sinon.spy(localIdentify.peerStore.addressBook, 'set') + sinon.spy(localIdentify.peerStore.protoBook, 'set') + + // Run identify + await Promise.all([ + localIdentify.identify(localConnectionMock), + remoteIdentify.handleMessage({ + connection: remoteConnectionMock, + stream: remote, + protocol: multicodecs.IDENTIFY + }) + ]) + + expect(localIdentify.peerStore.addressBook.set.callCount).to.equal(1) + expect(localIdentify.peerStore.protoBook.set.callCount).to.equal(1) + + // Validate the remote peer gets updated in the peer store + const call = localIdentify.peerStore.addressBook.set.firstCall + expect(call.args[0].id.bytes).to.equal(remotePeer.bytes) + expect(call.args[1]).to.exist() + expect(call.args[1]).have.lengthOf(listenMaddrs.length) + expect(call.args[1][0].equals(listenMaddrs[0])) }) it('should throw if identified peer is the wrong peer', async () => { @@ -145,15 +212,78 @@ describe('Identify', () => { describe('push', () => { it('should be able to push identify updates to another peer', async () => { - const listeningAddr = multiaddr('/ip4/127.0.0.1/tcp/1234') const connectionManager = new EventEmitter() - connectionManager.getConnection = () => {} + connectionManager.getConnection = () => { } + + const localIdentify = new IdentifyService({ + libp2p: { + peerId: localPeer, + connectionManager: new EventEmitter(), + multiaddrs: listenMaddrs + }, + protocols: new Map([ + [multicodecs.IDENTIFY], + [multicodecs.IDENTIFY_PUSH], + ['/echo/1.0.0'] + ]) + }) + const remoteIdentify = new IdentifyService({ + libp2p: { + peerId: remotePeer, + connectionManager, + peerStore: { + addressBook: { + set: () => { } + }, + protoBook: { + set: () => { } + } + }, + multiaddrs: [] + } + }) + + // Setup peer protocols and multiaddrs + const localProtocols = new Set([multicodecs.IDENTIFY, multicodecs.IDENTIFY_PUSH, '/echo/1.0.0']) + const localConnectionMock = { newStream: () => { } } + const remoteConnectionMock = { remotePeer: localPeer } + + const [local, remote] = duplexPair() + sinon.stub(localConnectionMock, 'newStream').returns({ stream: local, protocol: multicodecs.IDENTIFY_PUSH }) + + sinon.spy(remoteIdentify.peerStore.addressBook, 'set') + sinon.spy(remoteIdentify.peerStore.protoBook, 'set') + + // Run identify + await Promise.all([ + localIdentify.push([localConnectionMock]), + remoteIdentify.handleMessage({ + connection: remoteConnectionMock, + stream: remote, + protocol: multicodecs.IDENTIFY_PUSH + }) + ]) + + expect(remoteIdentify.peerStore.addressBook.set.callCount).to.equal(1) + expect(remoteIdentify.peerStore.protoBook.set.callCount).to.equal(1) + const [peerId, multiaddrs] = remoteIdentify.peerStore.addressBook.set.firstCall.args + expect(peerId.bytes).to.eql(localPeer.bytes) + expect(multiaddrs).to.eql(listenMaddrs) + const [peerId2, protocols] = remoteIdentify.peerStore.protoBook.set.firstCall.args + expect(peerId2.bytes).to.eql(localPeer.bytes) + expect(protocols).to.eql(Array.from(localProtocols)) + }) + + // LEGACY + it('should be able to push identify updates to another peer with no certified peer records support', async () => { + const connectionManager = new EventEmitter() + connectionManager.getConnection = () => { } const localIdentify = new IdentifyService({ libp2p: { peerId: localPeer, connectionManager: new EventEmitter(), - multiaddrs: [listeningAddr] + multiaddrs: listenMaddrs }, protocols: new Map([ [multicodecs.IDENTIFY], @@ -184,6 +314,7 @@ describe('Identify', () => { const [local, remote] = duplexPair() sinon.stub(localConnectionMock, 'newStream').returns({ stream: local, protocol: multicodecs.IDENTIFY_PUSH }) + sinon.stub(Envelope, 'openAndCertify').throws() sinon.spy(remoteIdentify.peerStore.addressBook, 'set') sinon.spy(remoteIdentify.peerStore.protoBook, 'set') @@ -202,7 +333,7 @@ describe('Identify', () => { expect(remoteIdentify.peerStore.protoBook.set.callCount).to.equal(1) const [peerId, multiaddrs] = remoteIdentify.peerStore.addressBook.set.firstCall.args expect(peerId.bytes).to.eql(localPeer.bytes) - expect(multiaddrs).to.eql([listeningAddr]) + expect(multiaddrs).to.eql(listenMaddrs) const [peerId2, protocols] = remoteIdentify.peerStore.protoBook.set.firstCall.args expect(peerId2.bytes).to.eql(localPeer.bytes) expect(protocols).to.eql(Array.from(localProtocols)) @@ -234,6 +365,8 @@ describe('Identify', () => { peerId }) + await libp2p.start() + sinon.spy(libp2p.identifyService, 'identify') const peerStoreSpySet = sinon.spy(libp2p.peerStore.addressBook, 'set') const peerStoreSpyAdd = sinon.spy(libp2p.peerStore.addressBook, 'add') @@ -257,6 +390,8 @@ describe('Identify', () => { peerId }) + await libp2p.start() + sinon.spy(libp2p.identifyService, 'identify') sinon.spy(libp2p.identifyService, 'push')