Skip to content
This repository was archived by the owner on Jul 21, 2023. It is now read-only.

Commit c6d1d49

Browse files
Alan Shawjacobheun
Alan Shaw
authored andcommittedMay 9, 2019
feat: compatibility with go-libp2p-mdns (#80)
Adds an additional mdns poller to interop with go-libp2p until both implementations comply with the new spec, https://github.com/libp2p/specs/blob/4c5a459ae8fb9a250e5f87f0c64fadaa7997266a/discovery/mdns.md.
1 parent 92cfb26 commit c6d1d49

12 files changed

+995
-18
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ docs
33
**/*.log
44
test/repo-tests*
55
**/bundle.js
6+
.nyc_output
67

78
# Logs
89
logs

‎README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ mdns.start(() => setTimeout(() => mdns.stop(() => {}), 20 * 1000))
3333

3434
- options
3535
- `peerInfo` - PeerInfo to announce
36-
- `broadcast` - (true/false) announce our presence through mDNS, default false
36+
- `broadcast` - (true/false) announce our presence through mDNS, default `false`
3737
- `interval` - query interval, default 10 * 1000 (10 seconds)
3838
- `serviceTag` - name of the service announce , default 'ipfs.local`
39+
- `compat` - enable/disable compatibility with go-libp2p-mdns, default `true`
3940

4041
## MDNS messages
4142

‎package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
],
1111
"scripts": {
1212
"lint": "aegir lint",
13-
"coverage": "aegir coverage",
13+
"coverage": "nyc --reporter=lcov --reporter=text npm run test:node",
1414
"test": "aegir test -t node",
1515
"test:node": "aegir test -t node",
1616
"release": "aegir release -t node --no-build",
@@ -35,11 +35,11 @@
3535
"homepage": "https://github.com/libp2p/js-libp2p-mdns",
3636
"devDependencies": {
3737
"aegir": "^18.2.2",
38-
"async": "^2.6.2",
3938
"chai": "^4.2.0",
4039
"dirty-chai": "^2.0.1"
4140
},
4241
"dependencies": {
42+
"async": "^2.6.2",
4343
"debug": "^4.1.1",
4444
"libp2p-tcp": "~0.13.0",
4545
"multiaddr": "^6.0.6",

‎src/compat/constants.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'use strict'
2+
3+
exports.SERVICE_TAG = '_ipfs-discovery._udp'
4+
exports.SERVICE_TAG_LOCAL = `${exports.SERVICE_TAG}.local`
5+
exports.MULTICAST_IP = '224.0.0.251'
6+
exports.MULTICAST_PORT = 5353

‎src/compat/index.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict'
2+
3+
// Compatibility with Go libp2p MDNS
4+
5+
const EE = require('events')
6+
const parallel = require('async/parallel')
7+
const Responder = require('./responder')
8+
const Querier = require('./querier')
9+
10+
class GoMulticastDNS extends EE {
11+
constructor (peerInfo) {
12+
super()
13+
this._started = false
14+
this._peerInfo = peerInfo
15+
this._onPeer = this._onPeer.bind(this)
16+
}
17+
18+
start (callback) {
19+
if (this._started) {
20+
return callback(new Error('MulticastDNS service is already started'))
21+
}
22+
23+
this._started = true
24+
this._responder = new Responder(this._peerInfo)
25+
this._querier = new Querier(this._peerInfo.id)
26+
27+
this._querier.on('peer', this._onPeer)
28+
29+
parallel([
30+
cb => this._responder.start(cb),
31+
cb => this._querier.start(cb)
32+
], callback)
33+
}
34+
35+
_onPeer (peerInfo) {
36+
this.emit('peer', peerInfo)
37+
}
38+
39+
stop (callback) {
40+
if (!this._started) {
41+
return callback(new Error('MulticastDNS service is not started'))
42+
}
43+
44+
const responder = this._responder
45+
const querier = this._querier
46+
47+
this._started = false
48+
this._responder = null
49+
this._querier = null
50+
51+
querier.removeListener('peer', this._onPeer)
52+
53+
parallel([
54+
cb => responder.stop(cb),
55+
cb => querier.stop(cb)
56+
], callback)
57+
}
58+
}
59+
60+
module.exports = GoMulticastDNS

‎src/compat/querier.js

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
'use strict'
2+
3+
const assert = require('assert')
4+
const EE = require('events')
5+
const MDNS = require('multicast-dns')
6+
const Multiaddr = require('multiaddr')
7+
const PeerInfo = require('peer-info')
8+
const PeerId = require('peer-id')
9+
const nextTick = require('async/nextTick')
10+
const log = require('debug')('libp2p:mdns:compat:querier')
11+
const { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } = require('./constants')
12+
13+
class Querier extends EE {
14+
constructor (peerId, options) {
15+
super()
16+
assert(peerId, 'missing peerId parameter')
17+
options = options || {}
18+
this._peerIdStr = peerId.toB58String()
19+
// Re-query every 60s, in leu of network change detection
20+
options.queryInterval = options.queryInterval || 60000
21+
// Time for which the MDNS server will stay alive waiting for responses
22+
// Must be less than options.queryInterval!
23+
options.queryPeriod = Math.min(
24+
options.queryInterval,
25+
options.queryPeriod == null ? 5000 : options.queryPeriod
26+
)
27+
this._options = options
28+
this._onResponse = this._onResponse.bind(this)
29+
}
30+
31+
start (callback) {
32+
this._handle = periodically(() => {
33+
// Create a querier that queries multicast but gets responses unicast
34+
const mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 })
35+
36+
mdns.on('response', this._onResponse)
37+
38+
mdns.query({
39+
id: nextId(), // id > 0 for unicast response
40+
questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }]
41+
}, null, {
42+
address: MULTICAST_IP,
43+
port: MULTICAST_PORT
44+
})
45+
46+
return {
47+
stop: callback => {
48+
mdns.removeListener('response', this._onResponse)
49+
mdns.destroy(callback)
50+
}
51+
}
52+
}, {
53+
period: this._options.queryPeriod,
54+
interval: this._options.queryInterval
55+
})
56+
57+
nextTick(() => callback())
58+
}
59+
60+
_onResponse (event, info) {
61+
const answers = event.answers || []
62+
const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL)
63+
64+
// Only deal with responses for our service tag
65+
if (!ptrRecord) return
66+
67+
log('got response', event, info)
68+
69+
const txtRecord = answers.find(a => a.type === 'TXT')
70+
if (!txtRecord) return log('missing TXT record in response')
71+
72+
let peerIdStr
73+
try {
74+
peerIdStr = txtRecord.data[0].toString()
75+
} catch (err) {
76+
return log('failed to extract peer ID from TXT record data', txtRecord, err)
77+
}
78+
79+
if (this._peerIdStr === peerIdStr) {
80+
return log('ignoring reply to myself')
81+
}
82+
83+
let peerId
84+
try {
85+
peerId = PeerId.createFromB58String(peerIdStr)
86+
} catch (err) {
87+
return log('failed to create peer ID from TXT record data', peerIdStr, err)
88+
}
89+
90+
PeerInfo.create(peerId, (err, info) => {
91+
if (err) return log('failed to create peer info from peer ID', peerId, err)
92+
93+
const srvRecord = answers.find(a => a.type === 'SRV')
94+
if (!srvRecord) return log('missing SRV record in response')
95+
96+
log('peer found', peerIdStr)
97+
98+
const { port } = srvRecord.data || {}
99+
const protos = { A: 'ip4', AAAA: 'ip6' }
100+
101+
const multiaddrs = answers
102+
.filter(a => ['A', 'AAAA'].includes(a.type))
103+
.reduce((addrs, a) => {
104+
const maStr = `/${protos[a.type]}/${a.data}/tcp/${port}`
105+
try {
106+
addrs.push(new Multiaddr(maStr))
107+
log(maStr)
108+
} catch (err) {
109+
log(`failed to create multiaddr from ${a.type} record data`, maStr, port, err)
110+
}
111+
return addrs
112+
}, [])
113+
114+
multiaddrs.forEach(addr => info.multiaddrs.add(addr))
115+
this.emit('peer', info)
116+
})
117+
}
118+
119+
stop (callback) {
120+
this._handle.stop(callback)
121+
}
122+
}
123+
124+
module.exports = Querier
125+
126+
/**
127+
* Run `fn` for a certain period of time, and then wait for an interval before
128+
* running it again. `fn` must return an object with a stop function, which is
129+
* called when the period expires.
130+
*
131+
* @param {Function} fn function to run
132+
* @param {Object} [options]
133+
* @param {Object} [options.period] Period in ms to run the function for
134+
* @param {Object} [options.interval] Interval in ms between runs
135+
* @returns {Object} handle that can be used to stop execution
136+
*/
137+
function periodically (fn, options) {
138+
let handle, timeoutId
139+
let stopped = false
140+
141+
const reRun = () => {
142+
handle = fn()
143+
timeoutId = setTimeout(() => {
144+
handle.stop(err => {
145+
if (err) log(err)
146+
if (!stopped) {
147+
timeoutId = setTimeout(reRun, options.interval)
148+
}
149+
})
150+
handle = null
151+
}, options.period)
152+
}
153+
154+
reRun()
155+
156+
return {
157+
stop (callback) {
158+
stopped = true
159+
clearTimeout(timeoutId)
160+
if (handle) {
161+
handle.stop(callback)
162+
} else {
163+
callback()
164+
}
165+
}
166+
}
167+
}
168+
169+
const nextId = (() => {
170+
let id = 0
171+
return () => {
172+
id++
173+
if (id === Number.MAX_SAFE_INTEGER) id = 1
174+
return id
175+
}
176+
})()

‎src/compat/responder.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict'
2+
3+
const OS = require('os')
4+
const assert = require('assert')
5+
const MDNS = require('multicast-dns')
6+
const log = require('debug')('libp2p:mdns:compat:responder')
7+
const TCP = require('libp2p-tcp')
8+
const nextTick = require('async/nextTick')
9+
const { SERVICE_TAG_LOCAL } = require('./constants')
10+
11+
const tcp = new TCP()
12+
13+
class Responder {
14+
constructor (peerInfo) {
15+
assert(peerInfo, 'missing peerInfo parameter')
16+
this._peerInfo = peerInfo
17+
this._peerIdStr = peerInfo.id.toB58String()
18+
this._onQuery = this._onQuery.bind(this)
19+
}
20+
21+
start (callback) {
22+
this._mdns = MDNS()
23+
this._mdns.on('query', this._onQuery)
24+
nextTick(() => callback())
25+
}
26+
27+
_onQuery (event, info) {
28+
const multiaddrs = tcp.filter(this._peerInfo.multiaddrs.toArray())
29+
// Only announce TCP for now
30+
if (!multiaddrs.length) return
31+
32+
const questions = event.questions || []
33+
34+
// Only respond to queries for our service tag
35+
if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return
36+
37+
log('got query', event, info)
38+
39+
const answers = []
40+
const peerServiceTagLocal = `${this._peerIdStr}.${SERVICE_TAG_LOCAL}`
41+
42+
answers.push({
43+
name: SERVICE_TAG_LOCAL,
44+
type: 'PTR',
45+
class: 'IN',
46+
ttl: 120,
47+
data: peerServiceTagLocal
48+
})
49+
50+
// Only announce TCP multiaddrs for now
51+
const port = multiaddrs[0].toString().split('/')[4]
52+
53+
answers.push({
54+
name: peerServiceTagLocal,
55+
type: 'SRV',
56+
class: 'IN',
57+
ttl: 120,
58+
data: {
59+
priority: 10,
60+
weight: 1,
61+
port,
62+
target: OS.hostname()
63+
}
64+
})
65+
66+
answers.push({
67+
name: peerServiceTagLocal,
68+
type: 'TXT',
69+
class: 'IN',
70+
ttl: 120,
71+
data: [Buffer.from(this._peerIdStr)]
72+
})
73+
74+
multiaddrs.forEach((ma) => {
75+
const proto = ma.protoNames()[0]
76+
if (proto === 'ip4' || proto === 'ip6') {
77+
answers.push({
78+
name: OS.hostname(),
79+
type: proto === 'ip4' ? 'A' : 'AAAA',
80+
class: 'IN',
81+
ttl: 120,
82+
data: ma.toString().split('/')[2]
83+
})
84+
}
85+
})
86+
87+
log('responding to query', answers)
88+
this._mdns.respond(answers, info)
89+
}
90+
91+
stop (callback) {
92+
this._mdns.removeListener('query', this._onQuery)
93+
this._mdns.destroy(callback)
94+
}
95+
}
96+
97+
module.exports = Responder

‎src/index.js

+34-6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
const multicastDNS = require('multicast-dns')
44
const EventEmitter = require('events').EventEmitter
55
const assert = require('assert')
6+
const nextTick = require('async/nextTick')
7+
const parallel = require('async/parallel')
68
const debug = require('debug')
79
const log = debug('libp2p:mdns')
810
const query = require('./query')
11+
const GoMulticastDNS = require('./compat')
912

1013
class MulticastDNS extends EventEmitter {
1114
constructor (options) {
@@ -18,10 +21,18 @@ class MulticastDNS extends EventEmitter {
1821
this.port = options.port || 5353
1922
this.peerInfo = options.peerInfo
2023
this._queryInterval = null
24+
this._onPeer = this._onPeer.bind(this)
25+
26+
if (options.compat !== false) {
27+
this._goMdns = new GoMulticastDNS(options.peerInfo, {
28+
queryPeriod: options.compatQueryPeriod,
29+
queryInterval: options.compatQueryInterval
30+
})
31+
this._goMdns.on('peer', this._onPeer)
32+
}
2133
}
2234

2335
start (callback) {
24-
const self = this
2536
const mdns = multicastDNS({ port: this.port })
2637

2738
this.mdns = mdns
@@ -34,23 +45,40 @@ class MulticastDNS extends EventEmitter {
3445
return log('Error processing peer response', err)
3546
}
3647

37-
self.emit('peer', foundPeer)
48+
this._onPeer(foundPeer)
3849
})
3950
})
4051

4152
mdns.on('query', (event) => {
4253
query.gotQuery(event, this.mdns, this.peerInfo, this.serviceTag, this.broadcast)
4354
})
4455

45-
setImmediate(() => callback())
56+
if (this._goMdns) {
57+
this._goMdns.start(callback)
58+
} else {
59+
nextTick(() => callback())
60+
}
61+
}
62+
63+
_onPeer (peerInfo) {
64+
this.emit('peer', peerInfo)
4665
}
4766

4867
stop (callback) {
4968
if (!this.mdns) {
50-
callback(new Error('MulticastDNS service had not started yet'))
69+
return callback(new Error('MulticastDNS service had not started yet'))
70+
}
71+
72+
clearInterval(this._queryInterval)
73+
this._queryInterval = null
74+
75+
if (this._goMdns) {
76+
this._goMdns.removeListener('peer', this._onPeer)
77+
parallel([
78+
cb => this._goMdns.stop(cb),
79+
cb => this.mdns.destroy(cb)
80+
], callback)
5181
} else {
52-
clearInterval(this._queryInterval)
53-
this._queryInterval = null
5482
this.mdns.destroy(callback)
5583
this.mdns = undefined
5684
}

‎test/compat/go-multicast-dns.spec.js

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const chai = require('chai')
5+
const dirtyChai = require('dirty-chai')
6+
const expect = chai.expect
7+
chai.use(dirtyChai)
8+
const PeerInfo = require('peer-info')
9+
const map = require('async/map')
10+
const series = require('async/series')
11+
12+
const GoMulticastDNS = require('../../src/compat')
13+
14+
describe('GoMulticastDNS', () => {
15+
const peerAddrs = [
16+
'/ip4/127.0.0.1/tcp/20001',
17+
'/ip4/127.0.0.1/tcp/20002'
18+
]
19+
let peerInfos
20+
21+
before(done => {
22+
map(peerAddrs, (addr, cb) => {
23+
PeerInfo.create((err, info) => {
24+
expect(err).to.not.exist()
25+
info.multiaddrs.add(addr)
26+
cb(null, info)
27+
})
28+
}, (err, infos) => {
29+
expect(err).to.not.exist()
30+
peerInfos = infos
31+
done()
32+
})
33+
})
34+
35+
it('should start and stop', done => {
36+
const mdns = new GoMulticastDNS(peerInfos[0])
37+
38+
mdns.start(err => {
39+
expect(err).to.not.exist()
40+
mdns.stop(err => {
41+
expect(err).to.not.exist()
42+
done()
43+
})
44+
})
45+
})
46+
47+
it('should not start when started', done => {
48+
const mdns = new GoMulticastDNS(peerInfos[0])
49+
50+
mdns.start(err => {
51+
expect(err).to.not.exist()
52+
53+
mdns.start(err => {
54+
expect(err.message).to.equal('MulticastDNS service is already started')
55+
mdns.stop(done)
56+
})
57+
})
58+
})
59+
60+
it('should not stop when not started', done => {
61+
const mdns = new GoMulticastDNS(peerInfos[0])
62+
63+
mdns.stop(err => {
64+
expect(err.message).to.equal('MulticastDNS service is not started')
65+
done()
66+
})
67+
})
68+
69+
it('should emit peer info when peer is discovered', done => {
70+
const mdnsA = new GoMulticastDNS(peerInfos[0])
71+
const mdnsB = new GoMulticastDNS(peerInfos[1])
72+
73+
mdnsA.on('peer', info => {
74+
if (!info.id.isEqual(peerInfos[1].id)) return
75+
expect(info.multiaddrs.has(peerAddrs[1])).to.be.true()
76+
done()
77+
})
78+
79+
series([
80+
cb => mdnsA.start(cb),
81+
cb => mdnsB.start(cb)
82+
], err => expect(err).to.not.exist())
83+
})
84+
})

‎test/compat/querier.spec.js

+322
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const chai = require('chai')
5+
const dirtyChai = require('dirty-chai')
6+
const expect = chai.expect
7+
chai.use(dirtyChai)
8+
const PeerInfo = require('peer-info')
9+
const parallel = require('async/parallel')
10+
const map = require('async/map')
11+
const MDNS = require('multicast-dns')
12+
const OS = require('os')
13+
14+
const Querier = require('../../src/compat/querier')
15+
const { SERVICE_TAG_LOCAL } = require('../../src/compat/constants')
16+
17+
describe('Querier', () => {
18+
let querier, mdns
19+
const peerAddrs = [
20+
'/ip4/127.0.0.1/tcp/20001',
21+
'/ip4/127.0.0.1/tcp/20002'
22+
]
23+
let peerInfos
24+
25+
before(done => {
26+
map(peerAddrs, (addr, cb) => {
27+
PeerInfo.create((err, info) => {
28+
expect(err).to.not.exist()
29+
info.multiaddrs.add(addr)
30+
cb(null, info)
31+
})
32+
}, (err, infos) => {
33+
expect(err).to.not.exist()
34+
peerInfos = infos
35+
done()
36+
})
37+
})
38+
39+
afterEach(done => {
40+
parallel([
41+
cb => querier ? querier.stop(cb) : cb(),
42+
cb => mdns ? mdns.destroy(cb) : cb()
43+
], err => {
44+
querier = mdns = null
45+
done(err)
46+
})
47+
})
48+
49+
it('should start and stop', done => {
50+
const querier = new Querier(peerInfos[0].id)
51+
52+
querier.start(err => {
53+
expect(err).to.not.exist()
54+
querier.stop(err => {
55+
expect(err).to.not.exist()
56+
done()
57+
})
58+
})
59+
})
60+
61+
it('should query on interval', done => {
62+
querier = new Querier(peerInfos[0].id, { queryPeriod: 0, queryInterval: 10 })
63+
mdns = MDNS()
64+
65+
let queryCount = 0
66+
67+
mdns.on('query', event => {
68+
const questions = event.questions || []
69+
if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return
70+
queryCount++
71+
})
72+
73+
querier.start(err => expect(err).to.not.exist())
74+
75+
setTimeout(() => {
76+
// Should have queried at least twice by now!
77+
expect(queryCount >= 2).to.be.true()
78+
done()
79+
}, 100)
80+
})
81+
82+
it('should not emit peer for responses with non matching service tags', done => {
83+
ensureNoPeer(event => {
84+
const peerServiceTagLocal = `${peerInfos[1].id.toB58String()}.${SERVICE_TAG_LOCAL}`
85+
const bogusServiceTagLocal = '_ifps-discovery._udp'
86+
87+
return [{
88+
name: bogusServiceTagLocal,
89+
type: 'PTR',
90+
class: 'IN',
91+
ttl: 120,
92+
data: peerServiceTagLocal
93+
}]
94+
}, done)
95+
})
96+
97+
it('should not emit peer for responses with missing TXT record', done => {
98+
ensureNoPeer(event => {
99+
const peerServiceTagLocal = `${peerInfos[1].id.toB58String()}.${SERVICE_TAG_LOCAL}`
100+
101+
return [{
102+
name: SERVICE_TAG_LOCAL,
103+
type: 'PTR',
104+
class: 'IN',
105+
ttl: 120,
106+
data: peerServiceTagLocal
107+
}]
108+
}, done)
109+
})
110+
111+
it('should not emit peer for responses with missing peer ID in TXT record', done => {
112+
ensureNoPeer(event => {
113+
const peerServiceTagLocal = `${peerInfos[1].id.toB58String()}.${SERVICE_TAG_LOCAL}`
114+
115+
return [{
116+
name: SERVICE_TAG_LOCAL,
117+
type: 'PTR',
118+
class: 'IN',
119+
ttl: 120,
120+
data: peerServiceTagLocal
121+
}, {
122+
name: peerServiceTagLocal,
123+
type: 'TXT',
124+
class: 'IN',
125+
ttl: 120,
126+
data: [] // undefined peer ID
127+
}]
128+
}, done)
129+
})
130+
131+
it('should not emit peer for responses to self', done => {
132+
ensureNoPeer(event => {
133+
const peerServiceTagLocal = `${peerInfos[1].id.toB58String()}.${SERVICE_TAG_LOCAL}`
134+
135+
return [{
136+
name: SERVICE_TAG_LOCAL,
137+
type: 'PTR',
138+
class: 'IN',
139+
ttl: 120,
140+
data: peerServiceTagLocal
141+
}, {
142+
name: peerServiceTagLocal,
143+
type: 'TXT',
144+
class: 'IN',
145+
ttl: 120,
146+
data: peerInfos[0].id.toB58String()
147+
}]
148+
}, done)
149+
})
150+
151+
// TODO: unskip when https://github.com/libp2p/js-peer-id/issues/83 is resolved
152+
it.skip('should not emit peer for responses with invalid peer ID in TXT record', done => {
153+
ensureNoPeer(event => {
154+
const peerServiceTagLocal = `${peerInfos[1].id.toB58String()}.${SERVICE_TAG_LOCAL}`
155+
156+
return [{
157+
name: SERVICE_TAG_LOCAL,
158+
type: 'PTR',
159+
class: 'IN',
160+
ttl: 120,
161+
data: peerServiceTagLocal
162+
}, {
163+
name: peerServiceTagLocal,
164+
type: 'TXT',
165+
class: 'IN',
166+
ttl: 120,
167+
data: '🤪'
168+
}]
169+
}, done)
170+
})
171+
172+
it('should not emit peer for responses with missing SRV record', done => {
173+
ensureNoPeer(event => {
174+
const peerServiceTagLocal = `${peerInfos[1].id.toB58String()}.${SERVICE_TAG_LOCAL}`
175+
176+
return [{
177+
name: SERVICE_TAG_LOCAL,
178+
type: 'PTR',
179+
class: 'IN',
180+
ttl: 120,
181+
data: peerServiceTagLocal
182+
}, {
183+
name: peerServiceTagLocal,
184+
type: 'TXT',
185+
class: 'IN',
186+
ttl: 120,
187+
data: peerInfos[1].id.toB58String()
188+
}]
189+
}, done)
190+
})
191+
192+
it('should emit peer for responses even if no multiaddrs', done => {
193+
ensurePeer(event => {
194+
const peerServiceTagLocal = `${peerInfos[1].id.toB58String()}.${SERVICE_TAG_LOCAL}`
195+
196+
return [{
197+
name: SERVICE_TAG_LOCAL,
198+
type: 'PTR',
199+
class: 'IN',
200+
ttl: 120,
201+
data: peerServiceTagLocal
202+
}, {
203+
name: peerServiceTagLocal,
204+
type: 'TXT',
205+
class: 'IN',
206+
ttl: 120,
207+
data: peerInfos[1].id.toB58String()
208+
}, {
209+
name: peerServiceTagLocal,
210+
type: 'SRV',
211+
class: 'IN',
212+
ttl: 120,
213+
data: {
214+
priority: 10,
215+
weight: 1,
216+
port: parseInt(peerAddrs[1].split().pop()),
217+
target: OS.hostname()
218+
}
219+
}]
220+
}, done)
221+
})
222+
223+
it('should emit peer for responses with valid multiaddrs', done => {
224+
ensurePeer(event => {
225+
const peerServiceTagLocal = `${peerInfos[1].id.toB58String()}.${SERVICE_TAG_LOCAL}`
226+
227+
return [{
228+
name: SERVICE_TAG_LOCAL,
229+
type: 'PTR',
230+
class: 'IN',
231+
ttl: 120,
232+
data: peerServiceTagLocal
233+
}, {
234+
name: peerServiceTagLocal,
235+
type: 'TXT',
236+
class: 'IN',
237+
ttl: 120,
238+
data: peerInfos[1].id.toB58String()
239+
}, {
240+
name: peerServiceTagLocal,
241+
type: 'SRV',
242+
class: 'IN',
243+
ttl: 120,
244+
data: {
245+
priority: 10,
246+
weight: 1,
247+
port: parseInt(peerAddrs[1].split().pop()),
248+
target: OS.hostname()
249+
}
250+
}, {
251+
name: OS.hostname(),
252+
type: peerAddrs[1].startsWith('/ip4') ? 'A' : 'AAAA',
253+
class: 'IN',
254+
ttl: 120,
255+
data: peerAddrs[1].split('/')[2]
256+
}]
257+
}, done)
258+
})
259+
260+
/**
261+
* Ensure peerInfos[1] are emitted from `querier`
262+
* @param {Function} getResponse Given a query, construct a response to test the querier
263+
* @param {Function} callback Callback called when test finishes
264+
*/
265+
function ensurePeer (getResponse, callback) {
266+
querier = new Querier(peerInfos[0].id)
267+
mdns = MDNS()
268+
269+
mdns.on('query', (event, info) => {
270+
const questions = event.questions || []
271+
if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return
272+
mdns.respond(getResponse(event, info), info)
273+
})
274+
275+
let peerInfo
276+
277+
querier.on('peer', info => {
278+
// Ignore non-test peers
279+
if (!info.id.isEqual(peerInfos[1].id)) return
280+
peerInfo = info
281+
})
282+
283+
querier.start(err => {
284+
if (err) return callback(err)
285+
setTimeout(() => {
286+
callback(peerInfo ? null : new Error('Missing peer'))
287+
}, 100)
288+
})
289+
}
290+
291+
/**
292+
* Ensure none of peerInfos are emitted from `querier`
293+
* @param {Function} getResponse Given a query, construct a response to test the querier
294+
* @param {Function} callback Callback called when test finishes
295+
*/
296+
function ensureNoPeer (getResponse, callback) {
297+
querier = new Querier(peerInfos[0].id)
298+
mdns = MDNS()
299+
300+
mdns.on('query', (event, info) => {
301+
const questions = event.questions || []
302+
if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return
303+
mdns.respond(getResponse(event, info), info)
304+
})
305+
306+
let peerInfo
307+
308+
querier.on('peer', info => {
309+
// Ignore non-test peers
310+
if (!info.id.isEqual(peerInfos[0].id) && !info.id.isEqual(peerInfos[1].id)) return
311+
peerInfo = info
312+
})
313+
314+
querier.start(err => {
315+
if (err) return callback(err)
316+
setTimeout(() => {
317+
if (!peerInfo) return callback()
318+
callback(Object.assign(new Error('Unexpected peer'), { peerInfo }))
319+
}, 100)
320+
})
321+
}
322+
})

‎test/compat/responder.spec.js

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const chai = require('chai')
5+
const dirtyChai = require('dirty-chai')
6+
const expect = chai.expect
7+
chai.use(dirtyChai)
8+
const PeerInfo = require('peer-info')
9+
const parallel = require('async/parallel')
10+
const map = require('async/map')
11+
const MDNS = require('multicast-dns')
12+
13+
const Responder = require('../../src/compat/responder')
14+
const { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } = require('../../src/compat/constants')
15+
16+
describe('Responder', () => {
17+
let responder, mdns
18+
const peerAddrs = [
19+
'/ip4/127.0.0.1/tcp/20001',
20+
'/ip4/127.0.0.1/tcp/20002'
21+
]
22+
let peerInfos
23+
24+
before(done => {
25+
map(peerAddrs, (addr, cb) => {
26+
PeerInfo.create((err, info) => {
27+
expect(err).to.not.exist()
28+
info.multiaddrs.add(addr)
29+
cb(null, info)
30+
})
31+
}, (err, infos) => {
32+
expect(err).to.not.exist()
33+
peerInfos = infos
34+
done()
35+
})
36+
})
37+
38+
afterEach(done => {
39+
parallel([
40+
cb => responder ? responder.stop(cb) : cb(),
41+
cb => mdns ? mdns.destroy(cb) : cb()
42+
], err => {
43+
responder = mdns = null
44+
done(err)
45+
})
46+
})
47+
48+
it('should start and stop', done => {
49+
const responder = new Responder(peerInfos[0])
50+
51+
responder.start(err => {
52+
expect(err).to.not.exist()
53+
responder.stop(err => {
54+
expect(err).to.not.exist()
55+
done()
56+
})
57+
})
58+
})
59+
60+
it('should not respond to a query if no TCP addresses', done => {
61+
PeerInfo.create((err, peerInfo) => {
62+
expect(err).to.not.exist()
63+
64+
responder = new Responder(peerInfo)
65+
mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 })
66+
67+
responder.start(err => {
68+
expect(err).to.not.exist()
69+
70+
let response
71+
72+
mdns.on('response', event => {
73+
if (isResponseFrom(event, peerInfo)) {
74+
response = event
75+
}
76+
})
77+
78+
mdns.query({
79+
id: 1, // id > 0 for unicast response
80+
questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }]
81+
}, null, {
82+
address: MULTICAST_IP,
83+
port: MULTICAST_PORT
84+
})
85+
86+
setTimeout(() => {
87+
done(response ? new Error('Unexpected repsonse') : null)
88+
}, 100)
89+
})
90+
})
91+
})
92+
93+
it('should not respond to a query with non matching service tag', done => {
94+
responder = new Responder(peerInfos[0])
95+
mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 })
96+
97+
responder.start(err => {
98+
expect(err).to.not.exist()
99+
100+
let response
101+
102+
mdns.on('response', event => {
103+
if (isResponseFrom(event, peerInfos[0])) {
104+
response = event
105+
}
106+
})
107+
108+
const bogusServiceTagLocal = '_ifps-discovery._udp'
109+
110+
mdns.query({
111+
id: 1, // id > 0 for unicast response
112+
questions: [{ name: bogusServiceTagLocal, type: 'PTR', class: 'IN' }]
113+
}, null, {
114+
address: MULTICAST_IP,
115+
port: MULTICAST_PORT
116+
})
117+
118+
setTimeout(() => {
119+
done(response ? new Error('Unexpected repsonse') : null)
120+
}, 100)
121+
})
122+
})
123+
124+
it('should respond correctly', done => {
125+
responder = new Responder(peerInfos[0])
126+
mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 })
127+
128+
responder.start(err => {
129+
expect(err).to.not.exist()
130+
131+
mdns.on('response', event => {
132+
if (!isResponseFrom(event, peerInfos[0])) return
133+
134+
const srvRecord = event.answers.find(a => a.type === 'SRV')
135+
if (!srvRecord) return done(new Error('Missing SRV record'))
136+
137+
const { port } = srvRecord.data || {}
138+
const protos = { A: 'ip4', AAAA: 'ip6' }
139+
140+
const addrs = event.answers
141+
.filter(a => ['A', 'AAAA'].includes(a.type))
142+
.map(a => `/${protos[a.type]}/${a.data}/tcp/${port}`)
143+
144+
if (!addrs.includes(peerAddrs[0])) {
145+
return done(new Error('Missing peer address in response: ' + peerAddrs[0]))
146+
}
147+
148+
done()
149+
})
150+
151+
mdns.query({
152+
id: 1, // id > 0 for unicast response
153+
questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }]
154+
}, null, {
155+
address: MULTICAST_IP,
156+
port: MULTICAST_PORT
157+
})
158+
})
159+
})
160+
})
161+
162+
function isResponseFrom (res, fromPeerInfo) {
163+
const answers = res.answers || []
164+
const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL)
165+
if (!ptrRecord) return false // Ignore irrelevant
166+
167+
const txtRecord = answers.find(a => a.type === 'TXT')
168+
if (!txtRecord) return false // Ignore missing TXT record
169+
170+
let peerIdStr
171+
try {
172+
peerIdStr = txtRecord.data[0].toString()
173+
} catch (err) {
174+
return false // Ignore invalid peer ID data
175+
}
176+
177+
// Ignore response from someone else
178+
if (fromPeerInfo.id.toB58String() !== peerIdStr) return false
179+
180+
return true
181+
}

‎test/multicast-dns.spec.js

+30-9
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,14 @@ describe('MulticastDNS', () => {
6767
const mdnsA = new MulticastDNS({
6868
peerInfo: pA,
6969
broadcast: false, // do not talk to ourself
70-
port: 50001
70+
port: 50001,
71+
compat: false
7172
})
7273

7374
const mdnsB = new MulticastDNS({
7475
peerInfo: pB,
75-
port: 50001 // port must be the same
76+
port: 50001, // port must be the same
77+
compat: false
7678
})
7779

7880
parallel([
@@ -97,15 +99,18 @@ describe('MulticastDNS', () => {
9799
const mdnsA = new MulticastDNS({
98100
peerInfo: pA,
99101
broadcast: false, // do not talk to ourself
100-
port: 50003
102+
port: 50003,
103+
compat: false
101104
})
102105
const mdnsC = new MulticastDNS({
103106
peerInfo: pC,
104-
port: 50003 // port must be the same
107+
port: 50003, // port must be the same
108+
compat: false
105109
})
106110
const mdnsD = new MulticastDNS({
107111
peerInfo: pD,
108-
port: 50003 // port must be the same
112+
port: 50003, // port must be the same
113+
compat: false
109114
})
110115

111116
parallel([
@@ -134,12 +139,14 @@ describe('MulticastDNS', () => {
134139
const mdnsA = new MulticastDNS({
135140
peerInfo: pA,
136141
broadcast: false, // do not talk to ourself
137-
port: 50001
142+
port: 50001,
143+
compat: false
138144
})
139145

140146
const mdnsB = new MulticastDNS({
141147
peerInfo: pB,
142-
port: 50001
148+
port: 50001,
149+
compat: false
143150
})
144151

145152
series([
@@ -164,12 +171,14 @@ describe('MulticastDNS', () => {
164171

165172
const mdnsA = new MulticastDNS({
166173
peerInfo: pA,
167-
port: 50004 // port must be the same
174+
port: 50004, // port must be the same
175+
compat: false
168176
})
169177

170178
const mdnsC = new MulticastDNS({
171179
peerInfo: pC,
172-
port: 50004
180+
port: 50004,
181+
compat: false
173182
})
174183

175184
series([
@@ -184,4 +193,16 @@ describe('MulticastDNS', () => {
184193
})
185194
})
186195
})
196+
197+
it('should start and stop with go-libp2p-mdns compat', done => {
198+
const mdns = new MulticastDNS({ peerInfo: pA, port: 50004 })
199+
200+
mdns.start(err => {
201+
expect(err).to.not.exist()
202+
mdns.stop(err => {
203+
expect(err).to.not.exist()
204+
done()
205+
})
206+
})
207+
})
187208
})

0 commit comments

Comments
 (0)
This repository has been archived.