Skip to content

Commit 16a0032

Browse files
committed
feat: resolve multiaddrs before dial
1 parent 4c7a89b commit 16a0032

File tree

8 files changed

+212
-15
lines changed

8 files changed

+212
-15
lines changed

doc/CONFIGURATION.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ Dialing in libp2p can be configured to limit the rate of dialing, and how long d
465465
| maxParallelDials | `number` | How many multiaddrs we can dial in parallel. |
466466
| maxDialsPerPeer | `number` | How many multiaddrs we can dial per peer, in parallel. |
467467
| dialTimeout | `number` | Second dial timeout per peer in ms. |
468+
| resolvers | `object` | Dial [Resolvers](https://github.com/multiformats/js-multiaddr/blob/master/src/resolvers/index.js) for resolving multiaddrs |
468469

469470
The below configuration example shows how the dialer should be configured, with the current defaults:
470471

@@ -474,6 +475,8 @@ const TCP = require('libp2p-tcp')
474475
const MPLEX = require('libp2p-mplex')
475476
const { NOISE } = require('libp2p-noise')
476477

478+
const { dnsaddrResolver } = require('multiaddr/src/resolvers')
479+
477480
const node = await Libp2p.create({
478481
modules: {
479482
transport: [TCP],
@@ -483,7 +486,10 @@ const node = await Libp2p.create({
483486
dialer: {
484487
maxParallelDials: 100,
485488
maxDialsPerPeer: 4,
486-
dialTimeout: 30e3
489+
dialTimeout: 30e3,
490+
resolvers: {
491+
dnsaddr: dnsaddrResolver
492+
}
487493
}
488494
```
489495

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"mafmt": "^8.0.0",
6565
"merge-options": "^2.0.0",
6666
"moving-average": "^1.0.0",
67-
"multiaddr": "^8.0.0",
67+
"multiaddr": "multiformats/js-multiaddr#feat/resolve-multiaddrs",
6868
"multicodec": "^2.0.0",
6969
"multistream-select": "^1.0.0",
7070
"mutable-proxy": "^1.0.0",

src/config.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict'
22

33
const mergeOptions = require('merge-options')
4+
const { dnsaddrResolver } = require('multiaddr/src/resolvers')
5+
46
const Constants = require('./constants')
57

68
const { FaultTolerance } = require('./transport-manager')
@@ -20,7 +22,10 @@ const DefaultConfig = {
2022
dialer: {
2123
maxParallelDials: Constants.MAX_PARALLEL_DIALS,
2224
maxDialsPerPeer: Constants.MAX_PER_PEER_DIALS,
23-
dialTimeout: Constants.DIAL_TIMEOUT
25+
dialTimeout: Constants.DIAL_TIMEOUT,
26+
resolvers: {
27+
dnsaddr: dnsaddrResolver
28+
}
2429
},
2530
metrics: {
2631
enabled: false

src/dialer/index.js

+61-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const {
1818
MAX_PER_PEER_DIALS
1919
} = require('../constants')
2020

21+
const dns4Code = 54
22+
const dns6Code = 55
23+
2124
class Dialer {
2225
/**
2326
* @class
@@ -27,19 +30,22 @@ class Dialer {
2730
* @param {number} [options.concurrency = MAX_PARALLEL_DIALS] - Number of max concurrent dials.
2831
* @param {number} [options.perPeerLimit = MAX_PER_PEER_DIALS] - Number of max concurrent dials per peer.
2932
* @param {number} [options.timeout = DIAL_TIMEOUT] - How long a dial attempt is allowed to take.
33+
* @param {object} [options.resolvers = {}]
3034
*/
3135
constructor ({
3236
transportManager,
3337
peerStore,
3438
concurrency = MAX_PARALLEL_DIALS,
3539
timeout = DIAL_TIMEOUT,
36-
perPeerLimit = MAX_PER_PEER_DIALS
40+
perPeerLimit = MAX_PER_PEER_DIALS,
41+
resolvers = {}
3742
}) {
3843
this.transportManager = transportManager
3944
this.peerStore = peerStore
4045
this.concurrency = concurrency
4146
this.timeout = timeout
4247
this.perPeerLimit = perPeerLimit
48+
this.resolvers = resolvers
4349
this.tokens = [...new Array(concurrency)].map((_, index) => index)
4450
this._pendingDials = new Map()
4551
}
@@ -69,7 +75,7 @@ class Dialer {
6975
* @returns {Promise<Connection>}
7076
*/
7177
async connectToPeer (peer, options = {}) {
72-
const dialTarget = this._createDialTarget(peer)
78+
const dialTarget = await this._createDialTarget(peer)
7379

7480
if (!dialTarget.addrs.length) {
7581
throw errCode(new Error('The dial request has no addresses'), codes.ERR_NO_VALID_ADDRESSES)
@@ -105,22 +111,28 @@ class Dialer {
105111
*
106112
* @private
107113
* @param {PeerId|Multiaddr|string} peer - A PeerId or Multiaddr
108-
* @returns {DialTarget}
114+
* @returns {Promise<DialTarget>}
109115
*/
110-
_createDialTarget (peer) {
116+
async _createDialTarget (peer) {
111117
const { id, multiaddrs } = getPeer(peer)
112118

113119
if (multiaddrs) {
114120
this.peerStore.addressBook.add(id, multiaddrs)
115121
}
116122

117-
let addrs = this.peerStore.addressBook.getMultiaddrsForPeer(id) || []
123+
let knownAddrs = this.peerStore.addressBook.getMultiaddrsForPeer(id) || []
118124

119125
// If received a multiaddr to dial, it should be the first to use
120126
// But, if we know other multiaddrs for the peer, we should try them too.
121127
if (multiaddr.isMultiaddr(peer)) {
122-
addrs = addrs.filter((addr) => !peer.equals(addr))
123-
addrs.unshift(peer)
128+
knownAddrs = knownAddrs.filter((addr) => !peer.equals(addr))
129+
knownAddrs.unshift(peer)
130+
}
131+
132+
const addrs = []
133+
for (const a of knownAddrs) {
134+
const resolvedAddrs = await this._resolve(a)
135+
resolvedAddrs.forEach(ra => addrs.push(ra))
124136
}
125137

126138
return {
@@ -190,6 +202,48 @@ class Dialer {
190202
log('token %d released', token)
191203
this.tokens.push(token)
192204
}
205+
206+
/**
207+
* Resolve multiaddr recursively.
208+
*
209+
* @param {Multiaddr} ma
210+
* @returns {Promise<Array<Multiaddr>>}
211+
*/
212+
async _resolve (ma) {
213+
// TODO: recursive logic should live in multiaddr once dns4/dns6 support is in place
214+
const resolvableProto = ma.protos().find((p) => p.resolvable)
215+
216+
// Multiaddr is not resolvable (including exception for dns4/dns6)? End recursion!
217+
if (!resolvableProto || resolvableProto.code === dns4Code || resolvableProto === dns6Code) {
218+
return [ma]
219+
}
220+
221+
const resolvedMultiaddrs = await this._resolveRecord(ma)
222+
const recursiveMultiaddrs = await Promise.all(resolvedMultiaddrs.map((nm) => {
223+
return this._resolve(nm)
224+
}))
225+
226+
return recursiveMultiaddrs.flat().reduce((array, newM) => {
227+
if (!array.find(m => m.equals(newM))) {
228+
array.push(newM)
229+
}
230+
return array
231+
}, []) // Unique addresses
232+
}
233+
234+
/**
235+
* Add dialer resolvers to multiaddr and resolve multiaddr.
236+
*
237+
* @param {Multiaddr} ma
238+
* @returns {Promise<Array<Multiaddr>>}
239+
*/
240+
_resolveRecord (ma) {
241+
for (const [key, value] of Object.entries(this.resolvers)) {
242+
ma.resolvers.set(key, value)
243+
}
244+
245+
return ma.resolve()
246+
}
193247
}
194248

195249
module.exports = Dialer

src/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ class Libp2p extends EventEmitter {
134134
peerStore: this.peerStore,
135135
concurrency: this._options.dialer.maxParallelDials,
136136
perPeerLimit: this._options.dialer.maxDialsPerPeer,
137-
timeout: this._options.dialer.dialTimeout
137+
timeout: this._options.dialer.dialTimeout,
138+
resolvers: this._options.dialer.resolvers
138139
})
139140

140141
this._modules.transport.forEach((Transport) => {

test/dialing/direct.node.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ describe('Dialing (direct, TCP)', () => {
156156

157157
it('should dial to the max concurrency', async () => {
158158
const addrs = [
159-
'/ip4/0.0.0.0/tcp/8000',
160-
'/ip4/0.0.0.0/tcp/8001',
161-
'/ip4/0.0.0.0/tcp/8002'
159+
multiaddr('/ip4/0.0.0.0/tcp/8000'),
160+
multiaddr('/ip4/0.0.0.0/tcp/8001'),
161+
multiaddr('/ip4/0.0.0.0/tcp/8002')
162162
]
163163
const dialer = new Dialer({
164164
transportManager: localTM,

test/dialing/direct.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ describe('Dialing (direct, WebSockets)', () => {
263263

264264
describe('libp2p.dialer', () => {
265265
let libp2p
266-
let remoteLibp2p
266+
let remoteLibp2p // TODO rem
267267

268268
afterEach(async () => {
269269
sinon.restore()

test/dialing/resolver.spec.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use strict'
2+
/* eslint-env mocha */
3+
4+
const { expect } = require('aegir/utils/chai')
5+
const sinon = require('sinon')
6+
7+
const multiaddr = require('multiaddr')
8+
const { Resolver } = require('multiaddr/src/resolvers/dns')
9+
10+
const peerUtils = require('../utils/creators/peer')
11+
const baseOptions = require('../utils/base-options.browser')
12+
13+
const { MULTIADDRS_WEBSOCKETS } = require('../fixtures/browser')
14+
const relayAddr = MULTIADDRS_WEBSOCKETS[0]
15+
16+
const getDnsaddrStub = (peerId) => [
17+
[`dnsaddr=/dnsaddr/ams-1.bootstrap.libp2p.io/p2p/${peerId}`],
18+
[`dnsaddr=/dnsaddr/ams-2.bootstrap.libp2p.io/p2p/${peerId}`],
19+
[`dnsaddr=/dnsaddr/lon-1.bootstrap.libp2p.io/p2p/${peerId}`],
20+
[`dnsaddr=/dnsaddr/nrt-1.bootstrap.libp2p.io/p2p/${peerId}`],
21+
[`dnsaddr=/dnsaddr/nyc-1.bootstrap.libp2p.io/p2p/${peerId}`],
22+
[`dnsaddr=/dnsaddr/sfo-2.bootstrap.libp2p.io/p2p/${peerId}`]
23+
]
24+
25+
const relayedAddr = (peerId) => `${relayAddr}/p2p-circuit/p2p/${peerId}`
26+
27+
const getDnsRelayedAddrStub = (peerId) => [
28+
[`dnsaddr=${relayedAddr(peerId)}`]
29+
]
30+
31+
describe('Dialing (resolvable addresses)', () => {
32+
let libp2p, remoteLibp2p
33+
34+
beforeEach(async () => {
35+
[libp2p, remoteLibp2p] = await peerUtils.createPeer({
36+
number: 2,
37+
config: {
38+
modules: baseOptions.modules,
39+
addresses: {
40+
listen: [multiaddr(`${relayAddr}/p2p-circuit`)]
41+
},
42+
config: {
43+
peerDiscovery: {
44+
autoDial: false
45+
}
46+
}
47+
},
48+
started: true,
49+
populateAddressBooks: false
50+
})
51+
})
52+
53+
afterEach(async () => {
54+
sinon.restore()
55+
await Promise.all([libp2p, remoteLibp2p].map(n => n.stop()))
56+
})
57+
58+
it('resolves dnsaddr to ws local address', async () => {
59+
const remoteId = remoteLibp2p.peerId.toB58String()
60+
const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId}`)
61+
const relayedAddrFetched = multiaddr(relayedAddr(remoteId))
62+
63+
// Transport spy
64+
const transport = libp2p.transportManager._transports.get('Circuit')
65+
sinon.spy(transport, 'dial')
66+
67+
// Resolver stub
68+
const stub = sinon.stub(Resolver.prototype, 'resolveTxt')
69+
stub.onCall(0).returns(Promise.resolve(getDnsRelayedAddrStub(remoteId)))
70+
71+
// Dial with address resolve
72+
const connection = await libp2p.dial(dialAddr)
73+
expect(connection).to.exist()
74+
expect(connection.remoteAddr.equals(relayedAddrFetched))
75+
76+
const dialArgs = transport.dial.firstCall.args
77+
expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true)
78+
})
79+
80+
it('resolves a dnsaddr recursively', async () => {
81+
const remoteId = remoteLibp2p.peerId.toB58String()
82+
const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId}`)
83+
const relayedAddrFetched = multiaddr(relayedAddr(remoteId))
84+
85+
// Transport spy
86+
const transport = libp2p.transportManager._transports.get('Circuit')
87+
sinon.spy(transport, 'dial')
88+
89+
// Resolver stub
90+
const stub = sinon.stub(Resolver.prototype, 'resolveTxt')
91+
let alreadyCalled = false
92+
stub.callsFake(() => {
93+
if (!alreadyCalled) {
94+
alreadyCalled = true
95+
// Return an array of dnsaddr
96+
return Promise.resolve(getDnsaddrStub(remoteId))
97+
}
98+
return Promise.resolve(getDnsRelayedAddrStub(remoteId))
99+
})
100+
101+
// Dial with address resolve
102+
const connection = await libp2p.dial(dialAddr)
103+
expect(connection).to.exist()
104+
expect(connection.remoteAddr.equals(relayedAddrFetched))
105+
106+
const dialArgs = transport.dial.firstCall.args
107+
expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true)
108+
})
109+
110+
// TODO: Temporary solution does not resolve dns4/dns6
111+
it('stops recursive resolve if finds dns4/dns6 and dials it', async () => {
112+
const remoteId = remoteLibp2p.peerId.toB58String()
113+
const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId}`)
114+
115+
// Stub resolver
116+
const dnsMa = multiaddr(`/dns4/ams-1.remote.libp2p.io/tcp/443/wss/p2p/${remoteId}`)
117+
const stubResolve = sinon.stub(Resolver.prototype, 'resolveTxt')
118+
stubResolve.returns(Promise.resolve([
119+
[`dnsaddr=${dnsMa}`]
120+
]))
121+
122+
// Stub transport
123+
const transport = libp2p.transportManager._transports.get('WebSockets')
124+
const stubTransport = sinon.stub(transport, 'dial')
125+
stubTransport.callsFake((multiaddr) => {
126+
expect(multiaddr.equals(dnsMa)).to.eql(true)
127+
})
128+
129+
await libp2p.dial(dialAddr)
130+
})
131+
})

0 commit comments

Comments
 (0)