From f4f1bc9962c464066563aff874d3081f9441bd40 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 8 Apr 2019 16:13:20 +0100 Subject: [PATCH 01/11] feat: compatibility with go-libp2p-mdns This PR adds a compatibility class that allows a js-libp2p node to find a go-libp2p node (and vice versa) over MDNS. It's implemented as a separate class so the two differing implementations do not get confused. I've verified this is working correctly by running a go-ipfs and js-ipfs node with no boostrap nodes (and no other discovery methods) and verifying they find each other. TODO: * [ ] Add tests! Some tips if you want to try this out: * After you've run `ipfs init`, remember to remove the bootstrap nodes from the config file (`~/.ipfs/config`) of each node before you start up * Use `ipfs log level mdns debug` for some go-ipfs mdns logs * You can use the following script (after `npm link`ing this branch) to start a js-ipfs node with no bootstrap nodes and no discovery modules other than MDNS: ```js const IPFS = require('ipfs') const MDNS = require('libp2p-mdns') const TCP = require('libp2p-tcp') const ipfs = new IPFS({ repo: '/tmp/ipfs-mdns', config: { Bootstrap: [] }, libp2p: { modules: { peerDiscovery: [MDNS], transport: [TCP] } } }) ipfs.on('ready', async () => { console.log('ipfs is ready') console.log('My Peer ID:', (await ipfs.id()).id) setInterval(async () => { const peers = await ipfs.swarm.peers() console.log(peers.length, 'peers:') peers.forEach(p => console.log(p.peer.toB58String())) }, 10000) }) ``` License: MIT Signed-off-by: Alan Shaw --- package.json | 2 +- src/compat/constants.js | 4 ++ src/compat/index.js | 46 +++++++++++++ src/compat/querier.js | 144 ++++++++++++++++++++++++++++++++++++++++ src/compat/responder.js | 94 ++++++++++++++++++++++++++ src/index.js | 27 ++++++-- 6 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 src/compat/constants.js create mode 100644 src/compat/index.js create mode 100644 src/compat/querier.js create mode 100644 src/compat/responder.js diff --git a/package.json b/package.json index f60a681..222c538 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "homepage": "https://github.com/libp2p/js-libp2p-mdns", "devDependencies": { "aegir": "^18.0.2", - "async": "^2.6.1", + "async": "^2.6.2", "chai": "^4.2.0", "dirty-chai": "^2.0.1" }, diff --git a/src/compat/constants.js b/src/compat/constants.js new file mode 100644 index 0000000..f54f350 --- /dev/null +++ b/src/compat/constants.js @@ -0,0 +1,4 @@ +exports.SERVICE_TAG = '_ipfs-discovery._udp' +exports.SERVICE_TAG_LOCAL = `${exports.SERVICE_TAG}.local` +exports.MULTICAST_IP = '224.0.0.251' +exports.MULTICAST_PORT = 5353 diff --git a/src/compat/index.js b/src/compat/index.js new file mode 100644 index 0000000..6c27a5b --- /dev/null +++ b/src/compat/index.js @@ -0,0 +1,46 @@ +'use strict' + +// Compatibility with Go libp2p MDNS + +const EE = require('events') +const parallel = require('async/parallel') +const Responder = require('./responder') +const Querier = require('./querier') + +class GoMulticastDNS extends EE { + constructor (peerInfo) { + super() + this._started = false + this._responder = new Responder(peerInfo) + this._querier = new Querier(peerInfo.id) + this._querier.on('peer', peerInfo => this.emit('peer', peerInfo)) + } + + start (callback) { + if (this._started) { + return callback(new Error('MulticastDNS service is already started')) + } + + this._started = true + + parallel([ + cb => this._responder.start(cb), + cb => this._querier.start(cb) + ], callback) + } + + stop (callback) { + if (!this._started) { + return callback(new Error('MulticastDNS service is not started')) + } + + this._started = false + + parallel([ + cb => this._responder.stop(cb), + cb => this._querier.stop(cb) + ], callback) + } +} + +module.exports = GoMulticastDNS diff --git a/src/compat/querier.js b/src/compat/querier.js new file mode 100644 index 0000000..f0e32ec --- /dev/null +++ b/src/compat/querier.js @@ -0,0 +1,144 @@ +'use strict' + +const assert = require('assert') +const EE = require('events') +const MDNS = require('multicast-dns') +const Multiaddr = require('multiaddr') +const PeerInfo = require('peer-info') +const PeerId = require('peer-id') +const log = require('debug')('libp2p:mdns:compat:querier') +const { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } = require('./constants') + +class Querier extends EE { + constructor (peerId, options) { + super() + assert(peerId, 'missing peerId parameter') + this._peerIdStr = peerId.toB58String() + this._options = options || {} + this._options.queryPeriod = this._options.queryPeriod || 5000 + this._onResponse = this._onResponse.bind(this) + } + + start (callback) { + this._handle = periodically(() => { + // Create a querier that queries multicast but gets responses unicast + const querier = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) + + querier.on('response', this._onResponse) + + querier.query({ + id: nextId(), // id > 0 for unicast response + questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }] + }, null, { + address: MULTICAST_IP, + port: MULTICAST_PORT + }) + + return { stop: callback => querier.destroy(callback) } + }, this._options.queryPeriod) + + setImmediate(() => callback()) + } + + _onResponse (event, info) { + const answers = event.answers || [] + const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL) + + // Only deal with responses for our service tag + if (!ptrRecord) return + + log('got response', event, info) + + const txtRecord = answers.find(a => a.type === 'TXT') + if (!txtRecord) return log('missing TXT record in response') + + let peerIdStr + try { + peerIdStr = txtRecord.data[0].toString() + } catch (err) { + return log('failed to extract peer ID from TXT record data', txtRecord, err) + } + + if (this._peerIdStr === peerIdStr) return // replied to myself, ignore + + let peerId + try { + peerId = PeerId.createFromB58String(peerIdStr) + } catch (err) { + return log('failed to create peer ID from TXT record data', peerIdStr, err) + } + + PeerInfo.create(peerId, (err, info) => { + if (err) return log('failed to create peer info from peer ID', peerId, err) + + const srvRecord = answers.find(a => a.type === 'SRV') + if (!srvRecord) return log('missing SRV record in response') + + log('peer found', peerIdStr) + + const { port } = srvRecord.data || {} + const protos = { A: 'ip4', AAAA: 'ip6' } + + const multiaddrs = answers + .filter(a => ['A', 'AAAA'].includes(a.type)) + .reduce((addrs, a) => { + const maStr = `/${protos[a.type]}/${a.data}/tcp/${port}` + try { + addrs.push(new Multiaddr(maStr)) + log(maStr) + } catch (err) { + log(`failed to create multiaddr from ${a.type} record data`, maStr, port, err) + } + return addrs + }, []) + + multiaddrs.forEach(addr => info.multiaddrs.add(addr)) + this.emit('peer', info) + }) + } + + stop (callback) { + this._handle.stop(callback) + } +} + +module.exports = Querier + +function periodically (run, period) { + let handle, timeoutId + let stopped = false + + const reRun = () => { + handle = run() + timeoutId = setTimeout(() => { + handle.stop(err => { + if (err) log(err) + if (!stopped) reRun() + }) + handle = null + }, period) + } + + reRun() + + return { + stop (callback) { + stopped = true + clearTimeout(timeoutId) + if (handle) { + handle.stop(callback) + } else { + callback() + } + } + } +} + +const nextId = (() => { + let id = 1 + return () => { + id++ + if (id === Number.MAX_SAFE_INTEGER) id = 1 + return id + } +})() diff --git a/src/compat/responder.js b/src/compat/responder.js new file mode 100644 index 0000000..782fdfd --- /dev/null +++ b/src/compat/responder.js @@ -0,0 +1,94 @@ +'use strict' + +const OS = require('os') +const assert = require('assert') +const MDNS = require('multicast-dns') +const log = require('debug')('libp2p:mdns:compat:responder') +const TCP = require('libp2p-tcp') +const tcp = new TCP() +const { SERVICE_TAG_LOCAL } = require('./constants') + +class Responder { + constructor (peerInfo) { + assert(peerInfo, 'missing peerInfo parameter') + this._peerInfo = peerInfo + this._peerIdStr = peerInfo.id.toB58String() + this._onQuery = this._onQuery.bind(this) + } + + start (callback) { + this._mdns = MDNS() + this._mdns.on('query', this._onQuery) + setImmediate(() => callback()) + } + + _onQuery (event, info) { + const multiaddrs = tcp.filter(this._peerInfo.multiaddrs.toArray()) + // Only announce TCP for now + if (!multiaddrs.length) return + + const questions = event.questions || [] + + // Only respond to queires for our service tag + if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return + + log('got query', event, info) + + const answers = [] + const peerServiceTagLocal = `${this._peerIdStr}.${SERVICE_TAG_LOCAL}` + + answers.push({ + name: SERVICE_TAG_LOCAL, + type: 'PTR', + class: 'IN', + ttl: 120, + data: peerServiceTagLocal + }) + + // Only announce TCP multiaddrs for now + const port = multiaddrs[0].toString().split('/')[4] + + answers.push({ + name: peerServiceTagLocal, + type: 'SRV', + class: 'IN', + ttl: 120, + data: { + priority: 10, + weight: 1, + port, + target: OS.hostname() + } + }) + + answers.push({ + name: peerServiceTagLocal, + type: 'TXT', + class: 'IN', + ttl: 120, + data: this._peerIdStr + }) + + multiaddrs.forEach((ma) => { + const proto = ma.protoNames()[0] + if (proto === 'ip4' || proto === 'ip6') { + answers.push({ + name: OS.hostname(), + type: proto === 'ip4' ? 'A' : 'AAAA', + class: 'IN', + ttl: 120, + data: ma.toString().split('/')[2] + }) + } + }) + + log('responding to query', answers) + this._mdns.respond(answers) + } + + stop (callback) { + this._mdns.destroy(callback) + } +} + +module.exports = Responder diff --git a/src/index.js b/src/index.js index d2c3ef3..b0d0cff 100644 --- a/src/index.js +++ b/src/index.js @@ -3,9 +3,12 @@ const multicastDNS = require('multicast-dns') const EventEmitter = require('events').EventEmitter const assert = require('assert') +const setImmediate = require('async/setImmediate') +const parallel = require('async/parallel') const debug = require('debug') const log = debug('libp2p:mdns') const query = require('./query') +const GoMulticastDNS = require('./compat') class MulticastDNS extends EventEmitter { constructor (options) { @@ -18,6 +21,10 @@ class MulticastDNS extends EventEmitter { this.port = options.port || 5353 this.peerInfo = options.peerInfo this._queryInterval = null + + if (options.compat !== false) { + this._goMdns = new GoMulticastDNS(options.peerInfo) + } } start (callback) { @@ -42,15 +49,27 @@ class MulticastDNS extends EventEmitter { query.gotQuery(event, this.mdns, this.peerInfo, this.serviceTag, this.broadcast) }) - setImmediate(() => callback()) + if (this._goMdns) { + this._goMdns.start(callback) + } else { + setImmediate(() => callback()) + } } stop (callback) { if (!this.mdns) { - callback(new Error('MulticastDNS service had not started yet')) + return callback(new Error('MulticastDNS service had not started yet')) + } + + clearInterval(this._queryInterval) + this._queryInterval = null + + if (this._goMdns) { + parallel([ + cb => this._goMdns.stop(cb), + cb => this.mdns.destroy(cb) + ], callback) } else { - clearInterval(this._queryInterval) - this._queryInterval = null this.mdns.destroy(callback) this.mdns = undefined } From 6d4d730d0128f82552a47386bc8cbacf0c1e7a00 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 8 Apr 2019 16:16:50 +0100 Subject: [PATCH 02/11] chore: appease linter License: MIT Signed-off-by: Alan Shaw --- src/compat/constants.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/compat/constants.js b/src/compat/constants.js index f54f350..df83a3f 100644 --- a/src/compat/constants.js +++ b/src/compat/constants.js @@ -1,3 +1,5 @@ +'use strict' + exports.SERVICE_TAG = '_ipfs-discovery._udp' exports.SERVICE_TAG_LOCAL = `${exports.SERVICE_TAG}.local` exports.MULTICAST_IP = '224.0.0.251' From ab9839c24b864b95e62b56c3efda522b9a5c8e06 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 8 Apr 2019 16:17:01 +0100 Subject: [PATCH 03/11] fix: move async to dependencies License: MIT Signed-off-by: Alan Shaw --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 222c538..a739ef4 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,11 @@ "homepage": "https://github.com/libp2p/js-libp2p-mdns", "devDependencies": { "aegir": "^18.0.2", - "async": "^2.6.2", "chai": "^4.2.0", "dirty-chai": "^2.0.1" }, "dependencies": { + "async": "^2.6.2", "debug": "^4.1.1", "libp2p-tcp": "~0.13.0", "multiaddr": "^6.0.2", From 019437f8dcc4bcbb16f6b8d8c84c5f99b299f472 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 9 Apr 2019 11:05:48 +0100 Subject: [PATCH 04/11] fix: typo in comment Co-Authored-By: alanshaw --- src/compat/responder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compat/responder.js b/src/compat/responder.js index 782fdfd..8d05eb3 100644 --- a/src/compat/responder.js +++ b/src/compat/responder.js @@ -29,7 +29,7 @@ class Responder { const questions = event.questions || [] - // Only respond to queires for our service tag + // Only respond to queries for our service tag if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return log('got query', event, info) From 96ddf658f7eac3222692ac9c3c527c45fa8f9ebc Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 9 Apr 2019 11:52:13 +0100 Subject: [PATCH 05/11] refactor: pr feedback License: MIT Signed-off-by: Alan Shaw --- src/compat/index.js | 24 +++++++++++++++++++----- src/compat/querier.js | 20 ++++++++++++++------ src/compat/responder.js | 9 ++++++--- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/compat/index.js b/src/compat/index.js index 6c27a5b..495f3cb 100644 --- a/src/compat/index.js +++ b/src/compat/index.js @@ -11,9 +11,8 @@ class GoMulticastDNS extends EE { constructor (peerInfo) { super() this._started = false - this._responder = new Responder(peerInfo) - this._querier = new Querier(peerInfo.id) - this._querier.on('peer', peerInfo => this.emit('peer', peerInfo)) + this._peerInfo = peerInfo + this._onPeer = this._onPeer.bind(this) } start (callback) { @@ -22,6 +21,10 @@ class GoMulticastDNS extends EE { } this._started = true + this._responder = new Responder(this._peerInfo) + this._querier = new Querier(this._peerInfo.id) + + this._querier.on('peer', this._onPeer) parallel([ cb => this._responder.start(cb), @@ -29,16 +32,27 @@ class GoMulticastDNS extends EE { ], callback) } + _onPeer (peerInfo) { + this.emit('peer', peerInfo) + } + stop (callback) { if (!this._started) { return callback(new Error('MulticastDNS service is not started')) } + const responder = this._responder + const querier = this._querier + this._started = false + this._responder = null + this._querier = null + + querier.removeListener('peer', this._onPeer) parallel([ - cb => this._responder.stop(cb), - cb => this._querier.stop(cb) + cb => responder.stop(cb), + cb => querier.stop(cb) ], callback) } } diff --git a/src/compat/querier.js b/src/compat/querier.js index f0e32ec..238e23f 100644 --- a/src/compat/querier.js +++ b/src/compat/querier.js @@ -6,6 +6,7 @@ const MDNS = require('multicast-dns') const Multiaddr = require('multiaddr') const PeerInfo = require('peer-info') const PeerId = require('peer-id') +const nextTick = require('async/nextTick') const log = require('debug')('libp2p:mdns:compat:querier') const { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } = require('./constants') @@ -22,11 +23,11 @@ class Querier extends EE { start (callback) { this._handle = periodically(() => { // Create a querier that queries multicast but gets responses unicast - const querier = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) + const mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) - querier.on('response', this._onResponse) + mdns.on('response', this._onResponse) - querier.query({ + mdns.query({ id: nextId(), // id > 0 for unicast response questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }] }, null, { @@ -34,10 +35,15 @@ class Querier extends EE { port: MULTICAST_PORT }) - return { stop: callback => querier.destroy(callback) } + return { + stop: callback => { + mdns.removeListener('response', this._onResponse) + mdns.destroy(callback) + } + } }, this._options.queryPeriod) - setImmediate(() => callback()) + nextTick(() => callback()) } _onResponse (event, info) { @@ -59,7 +65,9 @@ class Querier extends EE { return log('failed to extract peer ID from TXT record data', txtRecord, err) } - if (this._peerIdStr === peerIdStr) return // replied to myself, ignore + if (this._peerIdStr === peerIdStr) { + return log('ignoring reply to myself') + } let peerId try { diff --git a/src/compat/responder.js b/src/compat/responder.js index 8d05eb3..9bb532f 100644 --- a/src/compat/responder.js +++ b/src/compat/responder.js @@ -5,9 +5,11 @@ const assert = require('assert') const MDNS = require('multicast-dns') const log = require('debug')('libp2p:mdns:compat:responder') const TCP = require('libp2p-tcp') -const tcp = new TCP() +const nextTick = require('async/nextTick') const { SERVICE_TAG_LOCAL } = require('./constants') +const tcp = new TCP() + class Responder { constructor (peerInfo) { assert(peerInfo, 'missing peerInfo parameter') @@ -19,7 +21,7 @@ class Responder { start (callback) { this._mdns = MDNS() this._mdns.on('query', this._onQuery) - setImmediate(() => callback()) + nextTick(() => callback()) } _onQuery (event, info) { @@ -66,7 +68,7 @@ class Responder { type: 'TXT', class: 'IN', ttl: 120, - data: this._peerIdStr + data: [Buffer.from(this._peerIdStr)] }) multiaddrs.forEach((ma) => { @@ -87,6 +89,7 @@ class Responder { } stop (callback) { + this._mdns.removeListener('query', this._onQuery) this._mdns.destroy(callback) } } From b5949e7ad7d7f4e1691a008f327531e4dc5f6ffe Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 9 Apr 2019 12:27:53 +0100 Subject: [PATCH 06/11] fix: respond directly to querier License: MIT Signed-off-by: Alan Shaw --- src/compat/responder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compat/responder.js b/src/compat/responder.js index 9bb532f..da3ec4c 100644 --- a/src/compat/responder.js +++ b/src/compat/responder.js @@ -85,7 +85,7 @@ class Responder { }) log('responding to query', answers) - this._mdns.respond(answers) + this._mdns.respond(answers, info) } stop (callback) { From a539f00d94b3fd9137f0504d083cd8ecae1e8e08 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 9 Apr 2019 12:41:14 +0100 Subject: [PATCH 07/11] fix: reemit the peer event from GoMulticastDNS License: MIT Signed-off-by: Alan Shaw --- src/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index b0d0cff..e600703 100644 --- a/src/index.js +++ b/src/index.js @@ -21,14 +21,15 @@ class MulticastDNS extends EventEmitter { this.port = options.port || 5353 this.peerInfo = options.peerInfo this._queryInterval = null + this._onPeer = this._onPeer.bind(this) if (options.compat !== false) { this._goMdns = new GoMulticastDNS(options.peerInfo) + this._goMdns.on('peer', this._onPeer) } } start (callback) { - const self = this const mdns = multicastDNS({ port: this.port }) this.mdns = mdns @@ -41,7 +42,7 @@ class MulticastDNS extends EventEmitter { return log('Error processing peer response', err) } - self.emit('peer', foundPeer) + this._onPeer(foundPeer) }) }) @@ -56,6 +57,10 @@ class MulticastDNS extends EventEmitter { } } + _onPeer (peerInfo) { + this.emit('peer', peerInfo) + } + stop (callback) { if (!this.mdns) { return callback(new Error('MulticastDNS service had not started yet')) @@ -65,6 +70,7 @@ class MulticastDNS extends EventEmitter { this._queryInterval = null if (this._goMdns) { + this._goMdns.removeListener('peer', this._onPeer) parallel([ cb => this._goMdns.stop(cb), cb => this.mdns.destroy(cb) From 1a4fe078bc97553e3e61773ba56311acec6a97b2 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 9 Apr 2019 15:13:26 +0100 Subject: [PATCH 08/11] refactor: simply advertise with a go compatible repsonse every 10s License: MIT Signed-off-by: Alan Shaw --- src/compat/constants.js | 2 - src/compat/index.js | 111 ++++++++++++++++++----------- src/compat/querier.js | 152 ---------------------------------------- src/compat/responder.js | 97 ------------------------- src/index.js | 2 - 5 files changed, 72 insertions(+), 292 deletions(-) delete mode 100644 src/compat/querier.js delete mode 100644 src/compat/responder.js diff --git a/src/compat/constants.js b/src/compat/constants.js index df83a3f..f512d7b 100644 --- a/src/compat/constants.js +++ b/src/compat/constants.js @@ -2,5 +2,3 @@ exports.SERVICE_TAG = '_ipfs-discovery._udp' exports.SERVICE_TAG_LOCAL = `${exports.SERVICE_TAG}.local` -exports.MULTICAST_IP = '224.0.0.251' -exports.MULTICAST_PORT = 5353 diff --git a/src/compat/index.js b/src/compat/index.js index 495f3cb..1596185 100644 --- a/src/compat/index.js +++ b/src/compat/index.js @@ -1,59 +1,92 @@ 'use strict' -// Compatibility with Go libp2p MDNS +const OS = require('os') +const assert = require('assert') +const MDNS = require('multicast-dns') +const TCP = require('libp2p-tcp') +const nextTick = require('async/nextTick') +const { SERVICE_TAG_LOCAL } = require('./constants') -const EE = require('events') -const parallel = require('async/parallel') -const Responder = require('./responder') -const Querier = require('./querier') +const tcp = new TCP() -class GoMulticastDNS extends EE { - constructor (peerInfo) { - super() - this._started = false +// Simply advertise ourselves every 10 seconds with a go-libp2p-mdns compatible +// MDNS response. We can't discover go-libp2p nodes but they'll discover and +// connect to us. +class GoMulticastDNS { + constructor (peerInfo, options) { + assert(peerInfo, 'missing peerInfo parameter') this._peerInfo = peerInfo - this._onPeer = this._onPeer.bind(this) + this._peerIdStr = peerInfo.id.toB58String() + this._options = options + this._onTick = this._onTick.bind(this) } start (callback) { - if (this._started) { - return callback(new Error('MulticastDNS service is already started')) - } + this._mdns = MDNS() + this._intervalId = setInterval(this._onTick, this._options.interval || 10000) + nextTick(() => callback()) + } - this._started = true - this._responder = new Responder(this._peerInfo) - this._querier = new Querier(this._peerInfo.id) + _onTick () { + const multiaddrs = tcp.filter(this._peerInfo.multiaddrs.toArray()) + // Only announce TCP for now + if (!multiaddrs.length) return - this._querier.on('peer', this._onPeer) + const answers = [] + const peerServiceTagLocal = `${this._peerIdStr}.${SERVICE_TAG_LOCAL}` - parallel([ - cb => this._responder.start(cb), - cb => this._querier.start(cb) - ], callback) - } + answers.push({ + name: SERVICE_TAG_LOCAL, + type: 'PTR', + class: 'IN', + ttl: 120, + data: peerServiceTagLocal + }) - _onPeer (peerInfo) { - this.emit('peer', peerInfo) - } + // Only announce TCP multiaddrs for now + const port = multiaddrs[0].toString().split('/')[4] - stop (callback) { - if (!this._started) { - return callback(new Error('MulticastDNS service is not started')) - } + answers.push({ + name: peerServiceTagLocal, + type: 'SRV', + class: 'IN', + ttl: 120, + data: { + priority: 10, + weight: 1, + port, + target: OS.hostname() + } + }) - const responder = this._responder - const querier = this._querier + answers.push({ + name: peerServiceTagLocal, + type: 'TXT', + class: 'IN', + ttl: 120, + data: [Buffer.from(this._peerIdStr)] + }) - this._started = false - this._responder = null - this._querier = null + multiaddrs.forEach((ma) => { + const proto = ma.protoNames()[0] + if (proto === 'ip4' || proto === 'ip6') { + answers.push({ + name: OS.hostname(), + type: proto === 'ip4' ? 'A' : 'AAAA', + class: 'IN', + ttl: 120, + data: ma.toString().split('/')[2] + }) + } + }) - querier.removeListener('peer', this._onPeer) + this._mdns.respond(answers) + } - parallel([ - cb => responder.stop(cb), - cb => querier.stop(cb) - ], callback) + stop (callback) { + clearInterval(this._intervalId) + this._mdns.removeListener('query', this._onQuery) + this._mdns.destroy(callback) } } diff --git a/src/compat/querier.js b/src/compat/querier.js deleted file mode 100644 index 238e23f..0000000 --- a/src/compat/querier.js +++ /dev/null @@ -1,152 +0,0 @@ -'use strict' - -const assert = require('assert') -const EE = require('events') -const MDNS = require('multicast-dns') -const Multiaddr = require('multiaddr') -const PeerInfo = require('peer-info') -const PeerId = require('peer-id') -const nextTick = require('async/nextTick') -const log = require('debug')('libp2p:mdns:compat:querier') -const { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } = require('./constants') - -class Querier extends EE { - constructor (peerId, options) { - super() - assert(peerId, 'missing peerId parameter') - this._peerIdStr = peerId.toB58String() - this._options = options || {} - this._options.queryPeriod = this._options.queryPeriod || 5000 - this._onResponse = this._onResponse.bind(this) - } - - start (callback) { - this._handle = periodically(() => { - // Create a querier that queries multicast but gets responses unicast - const mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) - - mdns.on('response', this._onResponse) - - mdns.query({ - id: nextId(), // id > 0 for unicast response - questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }] - }, null, { - address: MULTICAST_IP, - port: MULTICAST_PORT - }) - - return { - stop: callback => { - mdns.removeListener('response', this._onResponse) - mdns.destroy(callback) - } - } - }, this._options.queryPeriod) - - nextTick(() => callback()) - } - - _onResponse (event, info) { - const answers = event.answers || [] - const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL) - - // Only deal with responses for our service tag - if (!ptrRecord) return - - log('got response', event, info) - - const txtRecord = answers.find(a => a.type === 'TXT') - if (!txtRecord) return log('missing TXT record in response') - - let peerIdStr - try { - peerIdStr = txtRecord.data[0].toString() - } catch (err) { - return log('failed to extract peer ID from TXT record data', txtRecord, err) - } - - if (this._peerIdStr === peerIdStr) { - return log('ignoring reply to myself') - } - - let peerId - try { - peerId = PeerId.createFromB58String(peerIdStr) - } catch (err) { - return log('failed to create peer ID from TXT record data', peerIdStr, err) - } - - PeerInfo.create(peerId, (err, info) => { - if (err) return log('failed to create peer info from peer ID', peerId, err) - - const srvRecord = answers.find(a => a.type === 'SRV') - if (!srvRecord) return log('missing SRV record in response') - - log('peer found', peerIdStr) - - const { port } = srvRecord.data || {} - const protos = { A: 'ip4', AAAA: 'ip6' } - - const multiaddrs = answers - .filter(a => ['A', 'AAAA'].includes(a.type)) - .reduce((addrs, a) => { - const maStr = `/${protos[a.type]}/${a.data}/tcp/${port}` - try { - addrs.push(new Multiaddr(maStr)) - log(maStr) - } catch (err) { - log(`failed to create multiaddr from ${a.type} record data`, maStr, port, err) - } - return addrs - }, []) - - multiaddrs.forEach(addr => info.multiaddrs.add(addr)) - this.emit('peer', info) - }) - } - - stop (callback) { - this._handle.stop(callback) - } -} - -module.exports = Querier - -function periodically (run, period) { - let handle, timeoutId - let stopped = false - - const reRun = () => { - handle = run() - timeoutId = setTimeout(() => { - handle.stop(err => { - if (err) log(err) - if (!stopped) reRun() - }) - handle = null - }, period) - } - - reRun() - - return { - stop (callback) { - stopped = true - clearTimeout(timeoutId) - if (handle) { - handle.stop(callback) - } else { - callback() - } - } - } -} - -const nextId = (() => { - let id = 1 - return () => { - id++ - if (id === Number.MAX_SAFE_INTEGER) id = 1 - return id - } -})() diff --git a/src/compat/responder.js b/src/compat/responder.js deleted file mode 100644 index da3ec4c..0000000 --- a/src/compat/responder.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict' - -const OS = require('os') -const assert = require('assert') -const MDNS = require('multicast-dns') -const log = require('debug')('libp2p:mdns:compat:responder') -const TCP = require('libp2p-tcp') -const nextTick = require('async/nextTick') -const { SERVICE_TAG_LOCAL } = require('./constants') - -const tcp = new TCP() - -class Responder { - constructor (peerInfo) { - assert(peerInfo, 'missing peerInfo parameter') - this._peerInfo = peerInfo - this._peerIdStr = peerInfo.id.toB58String() - this._onQuery = this._onQuery.bind(this) - } - - start (callback) { - this._mdns = MDNS() - this._mdns.on('query', this._onQuery) - nextTick(() => callback()) - } - - _onQuery (event, info) { - const multiaddrs = tcp.filter(this._peerInfo.multiaddrs.toArray()) - // Only announce TCP for now - if (!multiaddrs.length) return - - const questions = event.questions || [] - - // Only respond to queries for our service tag - if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return - - log('got query', event, info) - - const answers = [] - const peerServiceTagLocal = `${this._peerIdStr}.${SERVICE_TAG_LOCAL}` - - answers.push({ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }) - - // Only announce TCP multiaddrs for now - const port = multiaddrs[0].toString().split('/')[4] - - answers.push({ - name: peerServiceTagLocal, - type: 'SRV', - class: 'IN', - ttl: 120, - data: { - priority: 10, - weight: 1, - port, - target: OS.hostname() - } - }) - - answers.push({ - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: [Buffer.from(this._peerIdStr)] - }) - - multiaddrs.forEach((ma) => { - const proto = ma.protoNames()[0] - if (proto === 'ip4' || proto === 'ip6') { - answers.push({ - name: OS.hostname(), - type: proto === 'ip4' ? 'A' : 'AAAA', - class: 'IN', - ttl: 120, - data: ma.toString().split('/')[2] - }) - } - }) - - log('responding to query', answers) - this._mdns.respond(answers, info) - } - - stop (callback) { - this._mdns.removeListener('query', this._onQuery) - this._mdns.destroy(callback) - } -} - -module.exports = Responder diff --git a/src/index.js b/src/index.js index e600703..d4d7eba 100644 --- a/src/index.js +++ b/src/index.js @@ -25,7 +25,6 @@ class MulticastDNS extends EventEmitter { if (options.compat !== false) { this._goMdns = new GoMulticastDNS(options.peerInfo) - this._goMdns.on('peer', this._onPeer) } } @@ -70,7 +69,6 @@ class MulticastDNS extends EventEmitter { this._queryInterval = null if (this._goMdns) { - this._goMdns.removeListener('peer', this._onPeer) parallel([ cb => this._goMdns.stop(cb), cb => this.mdns.destroy(cb) From 5717bdea0a303aae3a8097f1ac799f856f5be941 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 9 Apr 2019 15:15:13 +0100 Subject: [PATCH 09/11] fix: default options License: MIT Signed-off-by: Alan Shaw --- src/compat/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compat/index.js b/src/compat/index.js index 1596185..5f2053e 100644 --- a/src/compat/index.js +++ b/src/compat/index.js @@ -17,7 +17,7 @@ class GoMulticastDNS { assert(peerInfo, 'missing peerInfo parameter') this._peerInfo = peerInfo this._peerIdStr = peerInfo.id.toB58String() - this._options = options + this._options = options || {} this._onTick = this._onTick.bind(this) } From 35e92332f550cfe55a561e1ede0d8192d965f561 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 9 Apr 2019 15:21:02 +0100 Subject: [PATCH 10/11] refactor: revert unneeded changes License: MIT Signed-off-by: Alan Shaw --- src/index.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index d4d7eba..b0d0cff 100644 --- a/src/index.js +++ b/src/index.js @@ -21,7 +21,6 @@ class MulticastDNS extends EventEmitter { this.port = options.port || 5353 this.peerInfo = options.peerInfo this._queryInterval = null - this._onPeer = this._onPeer.bind(this) if (options.compat !== false) { this._goMdns = new GoMulticastDNS(options.peerInfo) @@ -29,6 +28,7 @@ class MulticastDNS extends EventEmitter { } start (callback) { + const self = this const mdns = multicastDNS({ port: this.port }) this.mdns = mdns @@ -41,7 +41,7 @@ class MulticastDNS extends EventEmitter { return log('Error processing peer response', err) } - this._onPeer(foundPeer) + self.emit('peer', foundPeer) }) }) @@ -56,10 +56,6 @@ class MulticastDNS extends EventEmitter { } } - _onPeer (peerInfo) { - this.emit('peer', peerInfo) - } - stop (callback) { if (!this.mdns) { return callback(new Error('MulticastDNS service had not started yet')) From 21795de3d226d394c2ca86fbb689adbdf6392d8e Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 9 Apr 2019 15:28:49 +0100 Subject: [PATCH 11/11] refactor: add tag to allow this to be used standalone License: MIT Signed-off-by: Alan Shaw --- src/compat/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compat/index.js b/src/compat/index.js index 5f2053e..3d70e4c 100644 --- a/src/compat/index.js +++ b/src/compat/index.js @@ -91,3 +91,4 @@ class GoMulticastDNS { } module.exports = GoMulticastDNS +module.exports.tag = 'mdns'