diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 4983754c2e..527ee35733 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -465,6 +465,7 @@ Dialing in libp2p can be configured to limit the rate of dialing, and how long d | maxParallelDials | `number` | How many multiaddrs we can dial in parallel. | | maxDialsPerPeer | `number` | How many multiaddrs we can dial per peer, in parallel. | | dialTimeout | `number` | Second dial timeout per peer in ms. | +| resolvers | `object` | Dial [Resolvers](https://github.com/multiformats/js-multiaddr/blob/master/src/resolvers/index.js) for resolving multiaddrs | The below configuration example shows how the dialer should be configured, with the current defaults: @@ -474,6 +475,8 @@ const TCP = require('libp2p-tcp') const MPLEX = require('libp2p-mplex') const { NOISE } = require('libp2p-noise') +const { dnsaddrResolver } = require('multiaddr/src/resolvers') + const node = await Libp2p.create({ modules: { transport: [TCP], @@ -483,7 +486,10 @@ const node = await Libp2p.create({ dialer: { maxParallelDials: 100, maxDialsPerPeer: 4, - dialTimeout: 30e3 + dialTimeout: 30e3, + resolvers: { + dnsaddr: dnsaddrResolver + } } ``` diff --git a/doc/GETTING_STARTED.md b/doc/GETTING_STARTED.md index e7398cc1d7..fd180e8428 100644 --- a/doc/GETTING_STARTED.md +++ b/doc/GETTING_STARTED.md @@ -204,8 +204,8 @@ const Bootstrap = require('libp2p-bootstrap') // Known peers addresses const bootstrapMultiaddrs = [ - '/dns4/ams-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', - '/dns4/lon-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3' + '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN' ] const node = await Libp2p.create({ diff --git a/examples/libp2p-in-the-browser/index.js b/examples/libp2p-in-the-browser/index.js index be03f08b6c..fe5e31afed 100644 --- a/examples/libp2p-in-the-browser/index.js +++ b/examples/libp2p-in-the-browser/index.js @@ -31,12 +31,11 @@ document.addEventListener('DOMContentLoaded', async () => { [Bootstrap.tag]: { enabled: true, list: [ - '/dns4/ams-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', - '/dns4/lon-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3', - '/dns4/sfo-3.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM', - '/dns4/sgp-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu', - '/dns4/nyc-1.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm', - '/dns4/nyc-2.bootstrap.libp2p.io/tcp/443/wss/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64' + '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt' ] } } diff --git a/package.json b/package.json index c52c8f630e..a1ea90db83 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "mafmt": "^8.0.0", "merge-options": "^2.0.0", "moving-average": "^1.0.0", - "multiaddr": "^8.0.0", + "multiaddr": "^8.1.0", "multicodec": "^2.0.0", "multistream-select": "^1.0.0", "mutable-proxy": "^1.0.0", diff --git a/src/config.js b/src/config.js index 7e3d630efe..1cc0f097b4 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,8 @@ 'use strict' const mergeOptions = require('merge-options') +const { dnsaddrResolver } = require('multiaddr/src/resolvers') + const Constants = require('./constants') const { FaultTolerance } = require('./transport-manager') @@ -20,7 +22,10 @@ const DefaultConfig = { dialer: { maxParallelDials: Constants.MAX_PARALLEL_DIALS, maxDialsPerPeer: Constants.MAX_PER_PEER_DIALS, - dialTimeout: Constants.DIAL_TIMEOUT + dialTimeout: Constants.DIAL_TIMEOUT, + resolvers: { + dnsaddr: dnsaddrResolver + } }, metrics: { enabled: false diff --git a/src/dialer/index.js b/src/dialer/index.js index fc02b357a1..49d77e467d 100644 --- a/src/dialer/index.js +++ b/src/dialer/index.js @@ -27,13 +27,15 @@ class Dialer { * @param {number} [options.concurrency = MAX_PARALLEL_DIALS] - Number of max concurrent dials. * @param {number} [options.perPeerLimit = MAX_PER_PEER_DIALS] - Number of max concurrent dials per peer. * @param {number} [options.timeout = DIAL_TIMEOUT] - How long a dial attempt is allowed to take. + * @param {object} [options.resolvers = {}] - multiaddr resolvers to use when dialing */ constructor ({ transportManager, peerStore, concurrency = MAX_PARALLEL_DIALS, timeout = DIAL_TIMEOUT, - perPeerLimit = MAX_PER_PEER_DIALS + perPeerLimit = MAX_PER_PEER_DIALS, + resolvers = {} }) { this.transportManager = transportManager this.peerStore = peerStore @@ -42,6 +44,10 @@ class Dialer { this.perPeerLimit = perPeerLimit this.tokens = [...new Array(concurrency)].map((_, index) => index) this._pendingDials = new Map() + + for (const [key, value] of Object.entries(resolvers)) { + multiaddr.resolvers.set(key, value) + } } /** @@ -69,7 +75,7 @@ class Dialer { * @returns {Promise} */ async connectToPeer (peer, options = {}) { - const dialTarget = this._createDialTarget(peer) + const dialTarget = await this._createDialTarget(peer) if (!dialTarget.addrs.length) { throw errCode(new Error('The dial request has no addresses'), codes.ERR_NO_VALID_ADDRESSES) @@ -105,22 +111,28 @@ class Dialer { * * @private * @param {PeerId|Multiaddr|string} peer - A PeerId or Multiaddr - * @returns {DialTarget} + * @returns {Promise} */ - _createDialTarget (peer) { + async _createDialTarget (peer) { const { id, multiaddrs } = getPeer(peer) if (multiaddrs) { this.peerStore.addressBook.add(id, multiaddrs) } - let addrs = this.peerStore.addressBook.getMultiaddrsForPeer(id) || [] + let knownAddrs = this.peerStore.addressBook.getMultiaddrsForPeer(id) || [] // If received a multiaddr to dial, it should be the first to use // But, if we know other multiaddrs for the peer, we should try them too. if (multiaddr.isMultiaddr(peer)) { - addrs = addrs.filter((addr) => !peer.equals(addr)) - addrs.unshift(peer) + knownAddrs = knownAddrs.filter((addr) => !peer.equals(addr)) + knownAddrs.unshift(peer) + } + + const addrs = [] + for (const a of knownAddrs) { + const resolvedAddrs = await this._resolve(a) + resolvedAddrs.forEach(ra => addrs.push(ra)) } return { @@ -190,6 +202,52 @@ class Dialer { log('token %d released', token) this.tokens.push(token) } + + /** + * Resolve multiaddr recursively. + * + * @param {Multiaddr} ma + * @returns {Promise>} + */ + async _resolve (ma) { + // TODO: recursive logic should live in multiaddr once dns4/dns6 support is in place + // Now only supporting resolve for dnsaddr + const resolvableProto = ma.protoNames().includes('dnsaddr') + + // Multiaddr is not resolvable? End recursion! + if (!resolvableProto) { + return [ma] + } + + const resolvedMultiaddrs = await this._resolveRecord(ma) + const recursiveMultiaddrs = await Promise.all(resolvedMultiaddrs.map((nm) => { + return this._resolve(nm) + })) + + return recursiveMultiaddrs.flat().reduce((array, newM) => { + if (!array.find(m => m.equals(newM))) { + array.push(newM) + } + return array + }, []) // Unique addresses + } + + /** + * Resolve a given multiaddr. If this fails, an empty array will be returned + * + * @param {Multiaddr} ma + * @returns {Promise>} + */ + async _resolveRecord (ma) { + try { + ma = multiaddr(ma.toString()) // Use current multiaddr module + const multiaddrs = await ma.resolve() + return multiaddrs + } catch (_) { + log.error(`multiaddr ${ma} could not be resolved`) + return [] + } + } } module.exports = Dialer diff --git a/src/index.js b/src/index.js index cd900457d6..85547f4702 100644 --- a/src/index.js +++ b/src/index.js @@ -134,7 +134,8 @@ class Libp2p extends EventEmitter { peerStore: this.peerStore, concurrency: this._options.dialer.maxParallelDials, perPeerLimit: this._options.dialer.maxDialsPerPeer, - timeout: this._options.dialer.dialTimeout + timeout: this._options.dialer.dialTimeout, + resolvers: this._options.dialer.resolvers }) this._modules.transport.forEach((Transport) => { diff --git a/test/dialing/direct.node.js b/test/dialing/direct.node.js index 752f462e06..6b89fee4af 100644 --- a/test/dialing/direct.node.js +++ b/test/dialing/direct.node.js @@ -158,9 +158,9 @@ describe('Dialing (direct, TCP)', () => { it('should dial to the max concurrency', async () => { const addrs = [ - '/ip4/0.0.0.0/tcp/8000', - '/ip4/0.0.0.0/tcp/8001', - '/ip4/0.0.0.0/tcp/8002' + multiaddr('/ip4/0.0.0.0/tcp/8000'), + multiaddr('/ip4/0.0.0.0/tcp/8001'), + multiaddr('/ip4/0.0.0.0/tcp/8002') ] const dialer = new Dialer({ transportManager: localTM, diff --git a/test/dialing/direct.spec.js b/test/dialing/direct.spec.js index e3d60e5960..540f528b65 100644 --- a/test/dialing/direct.spec.js +++ b/test/dialing/direct.spec.js @@ -263,7 +263,6 @@ describe('Dialing (direct, WebSockets)', () => { describe('libp2p.dialer', () => { let libp2p - let remoteLibp2p afterEach(async () => { sinon.restore() @@ -271,10 +270,6 @@ describe('Dialing (direct, WebSockets)', () => { libp2p = null }) - after(async () => { - remoteLibp2p && await remoteLibp2p.stop() - }) - it('should create a dialer', () => { libp2p = new Libp2p({ peerId, diff --git a/test/dialing/resolver.spec.js b/test/dialing/resolver.spec.js new file mode 100644 index 0000000000..ba81a5c8b2 --- /dev/null +++ b/test/dialing/resolver.spec.js @@ -0,0 +1,176 @@ +'use strict' +/* eslint-env mocha */ + +const { expect } = require('aegir/utils/chai') +const sinon = require('sinon') + +const multiaddr = require('multiaddr') +const { Resolver } = require('multiaddr/src/resolvers/dns') + +const { codes: ErrorCodes } = require('../../src/errors') + +const peerUtils = require('../utils/creators/peer') +const baseOptions = require('../utils/base-options.browser') + +const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser') +const relayAddr = MULTIADDRS_WEBSOCKETS[0] + +const getDnsaddrStub = (peerId) => [ + [`dnsaddr=/dnsaddr/ams-1.bootstrap.libp2p.io/p2p/${peerId}`], + [`dnsaddr=/dnsaddr/ams-2.bootstrap.libp2p.io/p2p/${peerId}`], + [`dnsaddr=/dnsaddr/lon-1.bootstrap.libp2p.io/p2p/${peerId}`], + [`dnsaddr=/dnsaddr/nrt-1.bootstrap.libp2p.io/p2p/${peerId}`], + [`dnsaddr=/dnsaddr/nyc-1.bootstrap.libp2p.io/p2p/${peerId}`], + [`dnsaddr=/dnsaddr/sfo-2.bootstrap.libp2p.io/p2p/${peerId}`] +] + +const relayedAddr = (peerId) => `${relayAddr}/p2p-circuit/p2p/${peerId}` + +const getDnsRelayedAddrStub = (peerId) => [ + [`dnsaddr=${relayedAddr(peerId)}`] +] + +describe('Dialing (resolvable addresses)', () => { + let libp2p, remoteLibp2p + + beforeEach(async () => { + [libp2p, remoteLibp2p] = await peerUtils.createPeer({ + number: 2, + config: { + modules: baseOptions.modules, + addresses: { + listen: [multiaddr(`${relayAddr}/p2p-circuit`)] + }, + config: { + peerDiscovery: { + autoDial: false + } + } + }, + started: true, + populateAddressBooks: false + }) + }) + + afterEach(async () => { + sinon.restore() + await Promise.all([libp2p, remoteLibp2p].map(n => n.stop())) + }) + + it('resolves dnsaddr to ws local address', async () => { + const remoteId = remoteLibp2p.peerId.toB58String() + const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId}`) + const relayedAddrFetched = multiaddr(relayedAddr(remoteId)) + + // Transport spy + const transport = libp2p.transportManager._transports.get('Circuit') + sinon.spy(transport, 'dial') + + // Resolver stub + const stub = sinon.stub(Resolver.prototype, 'resolveTxt') + stub.onCall(0).returns(Promise.resolve(getDnsRelayedAddrStub(remoteId))) + + // Dial with address resolve + const connection = await libp2p.dial(dialAddr) + expect(connection).to.exist() + expect(connection.remoteAddr.equals(relayedAddrFetched)) + + const dialArgs = transport.dial.firstCall.args + expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true) + }) + + it('resolves a dnsaddr recursively', async () => { + const remoteId = remoteLibp2p.peerId.toB58String() + const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId}`) + const relayedAddrFetched = multiaddr(relayedAddr(remoteId)) + + // Transport spy + const transport = libp2p.transportManager._transports.get('Circuit') + sinon.spy(transport, 'dial') + + // Resolver stub + const stub = sinon.stub(Resolver.prototype, 'resolveTxt') + let firstCall = false + stub.callsFake(() => { + if (!firstCall) { + firstCall = true + // Return an array of dnsaddr + return Promise.resolve(getDnsaddrStub(remoteId)) + } + return Promise.resolve(getDnsRelayedAddrStub(remoteId)) + }) + + // Dial with address resolve + const connection = await libp2p.dial(dialAddr) + expect(connection).to.exist() + expect(connection.remoteAddr.equals(relayedAddrFetched)) + + const dialArgs = transport.dial.firstCall.args + expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true) + }) + + // TODO: Temporary solution does not resolve dns4/dns6 + // Resolver just returns the received multiaddrs + it('stops recursive resolve if finds dns4/dns6 and dials it', async () => { + const remoteId = remoteLibp2p.peerId.toB58String() + const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId}`) + + // Stub resolver + const dnsMa = multiaddr(`/dns4/ams-1.remote.libp2p.io/tcp/443/wss/p2p/${remoteId}`) + const stubResolve = sinon.stub(Resolver.prototype, 'resolveTxt') + stubResolve.returns(Promise.resolve([ + [`dnsaddr=${dnsMa}`] + ])) + + // Stub transport + const transport = libp2p.transportManager._transports.get('WebSockets') + const stubTransport = sinon.stub(transport, 'dial') + stubTransport.callsFake((multiaddr) => { + expect(multiaddr.equals(dnsMa)).to.eql(true) + }) + + await libp2p.dial(dialAddr) + }) + + it('resolves a dnsaddr recursively not failing if one address fails to resolve', async () => { + const remoteId = remoteLibp2p.peerId.toB58String() + const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId}`) + const relayedAddrFetched = multiaddr(relayedAddr(remoteId)) + + // Transport spy + const transport = libp2p.transportManager._transports.get('Circuit') + sinon.spy(transport, 'dial') + + // Resolver stub + const stub = sinon.stub(Resolver.prototype, 'resolveTxt') + stub.onCall(0).callsFake(() => Promise.resolve(getDnsaddrStub(remoteId))) + stub.onCall(1).callsFake(() => Promise.reject(new Error())) + stub.callsFake(() => Promise.resolve(getDnsRelayedAddrStub(remoteId))) + + // Dial with address resolve + const connection = await libp2p.dial(dialAddr) + expect(connection).to.exist() + expect(connection.remoteAddr.equals(relayedAddrFetched)) + + const dialArgs = transport.dial.firstCall.args + expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true) + }) + + it('fails to dial if resolve fails and there are no addresses to dial', async () => { + const remoteId = remoteLibp2p.peerId.toB58String() + const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId}`) + + // Stub resolver + const stubResolve = sinon.stub(Resolver.prototype, 'resolveTxt') + stubResolve.returns(Promise.reject(new Error())) + + // Stub transport + const transport = libp2p.transportManager._transports.get('WebSockets') + const spy = sinon.spy(transport, 'dial') + + await expect(libp2p.dial(dialAddr)) + .to.eventually.be.rejectedWith(Error) + .and.to.have.nested.property('.code', ErrorCodes.ERR_NO_VALID_ADDRESSES) + expect(spy.callCount).to.eql(0) + }) +})