@libp2p/mdns 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ import { EventEmitter } from 'events'
2
+ import MDNS from 'multicast-dns'
3
+ import { Multiaddr } from '@multiformats/multiaddr'
4
+ import { PeerId } from '@libp2p/peer-id'
5
+ import debug from 'debug'
6
+ import { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } from './constants.js'
7
+ import { base58btc } from 'multiformats/bases/base58'
8
+ import type { PeerDiscovery } from '@libp2p/interfaces/peer-discovery'
9
+ import type { ResponsePacket } from 'multicast-dns'
10
+ import type { RemoteInfo } from 'dgram'
11
+
12
+ const log = Object.assign(debug('libp2p:mdns:compat:querier'), {
13
+ error: debug('libp2p:mdns:compat:querier:error')
14
+ })
15
+
16
+ export interface QuerierOptions {
17
+ peerId: PeerId
18
+ queryInterval?: number
19
+ queryPeriod?: number
20
+ }
21
+
22
+ export interface Handle {
23
+ stop: () => Promise<void>
24
+ }
25
+
26
+ export class Querier extends EventEmitter implements PeerDiscovery {
27
+ private readonly _peerIdStr: string
28
+ private readonly _options: Required<QuerierOptions>
29
+ private _handle?: Handle
30
+
31
+ constructor (options: QuerierOptions) {
32
+ super()
33
+
34
+ const { peerId, queryInterval, queryPeriod } = options
35
+
36
+ if (peerId == null) {
37
+ throw new Error('missing peerId parameter')
38
+ }
39
+
40
+ this._peerIdStr = peerId.toString(base58btc)
41
+ this._options = {
42
+ peerId,
43
+
44
+ // Re-query in leu of network change detection (every 60s by default)
45
+ queryInterval: queryInterval ?? 60000,
46
+ // Time for which the MDNS server will stay alive waiting for responses
47
+ // Must be less than options.queryInterval!
48
+ queryPeriod: Math.min(
49
+ queryInterval ?? 60000,
50
+ queryPeriod ?? 5000
51
+ )
52
+ }
53
+ this._onResponse = this._onResponse.bind(this)
54
+ }
55
+
56
+ isStarted () {
57
+ return Boolean(this._handle)
58
+ }
59
+
60
+ start () {
61
+ this._handle = periodically(() => {
62
+ // Create a querier that queries multicast but gets responses unicast
63
+ const mdns = MDNS({ multicast: false, interface: '0.0.0.0', port: 0 })
64
+
65
+ mdns.on('response', this._onResponse)
66
+
67
+ // @ts-expect-error @types/multicast-dns are wrong
68
+ mdns.query({
69
+ id: nextId(), // id > 0 for unicast response
70
+ questions: [{
71
+ name: SERVICE_TAG_LOCAL,
72
+ type: 'PTR',
73
+ class: 'IN'
74
+ }]
75
+ }, null, {
76
+ address: MULTICAST_IP,
77
+ port: MULTICAST_PORT
78
+ })
79
+
80
+ return {
81
+ stop: async () => {
82
+ mdns.removeListener('response', this._onResponse)
83
+ return await new Promise(resolve => mdns.destroy(resolve))
84
+ }
85
+ }
86
+ }, {
87
+ period: this._options.queryPeriod,
88
+ interval: this._options.queryInterval
89
+ })
90
+ }
91
+
92
+ _onResponse (event: ResponsePacket, info: RemoteInfo) {
93
+ const answers = event.answers ?? []
94
+ const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL)
95
+
96
+ // Only deal with responses for our service tag
97
+ if (ptrRecord == null) return
98
+
99
+ log('got response', event, info)
100
+
101
+ const txtRecord = answers.find(a => a.type === 'TXT')
102
+ if (txtRecord == null || txtRecord.type !== 'TXT') {
103
+ return log('missing TXT record in response')
104
+ }
105
+
106
+ let peerIdStr
107
+ try {
108
+ peerIdStr = txtRecord.data[0].toString()
109
+ } catch (err) {
110
+ return log('failed to extract peer ID from TXT record data', txtRecord, err)
111
+ }
112
+
113
+ if (this._peerIdStr === peerIdStr) {
114
+ return log('ignoring reply to myself')
115
+ }
116
+
117
+ let peerId
118
+ try {
119
+ peerId = PeerId.fromString(peerIdStr)
120
+ } catch (err) {
121
+ return log('failed to create peer ID from TXT record data', peerIdStr, err)
122
+ }
123
+
124
+ const srvRecord = answers.find(a => a.type === 'SRV')
125
+ if (srvRecord == null || srvRecord.type !== 'SRV') {
126
+ return log('missing SRV record in response')
127
+ }
128
+
129
+ log('peer found', peerIdStr)
130
+
131
+ const { port } = srvRecord.data ?? {}
132
+ const protos = { A: 'ip4', AAAA: 'ip6' }
133
+
134
+ const multiaddrs = answers
135
+ .filter(a => ['A', 'AAAA'].includes(a.type))
136
+ .reduce<Multiaddr[]>((addrs, a) => {
137
+ if (a.type !== 'A' && a.type !== 'AAAA') {
138
+ return addrs
139
+ }
140
+
141
+ const maStr = `/${protos[a.type]}/${a.data}/tcp/${port}`
142
+ try {
143
+ addrs.push(new Multiaddr(maStr))
144
+ log(maStr)
145
+ } catch (err) {
146
+ log(`failed to create multiaddr from ${a.type} record data`, maStr, port, err)
147
+ }
148
+ return addrs
149
+ }, [])
150
+
151
+ this.emit('peer', {
152
+ id: peerId,
153
+ multiaddrs
154
+ })
155
+ }
156
+
157
+ async stop () {
158
+ if (this._handle != null) {
159
+ await this._handle.stop()
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Run `fn` for a certain period of time, and then wait for an interval before
166
+ * running it again. `fn` must return an object with a stop function, which is
167
+ * called when the period expires.
168
+ */
169
+ function periodically (fn: () => Handle, options: { period: number, interval: number }) {
170
+ let handle: Handle | null
171
+ let timeoutId: NodeJS.Timer
172
+ let stopped = false
173
+
174
+ const reRun = () => {
175
+ handle = fn()
176
+ timeoutId = setTimeout(() => {
177
+ if (handle != null) {
178
+ handle.stop().catch(log)
179
+ }
180
+
181
+ if (!stopped) {
182
+ timeoutId = setTimeout(reRun, options.interval)
183
+ }
184
+
185
+ handle = null
186
+ }, options.period)
187
+ }
188
+
189
+ reRun()
190
+
191
+ return {
192
+ async stop () {
193
+ stopped = true
194
+ clearTimeout(timeoutId)
195
+ if (handle != null) {
196
+ await handle.stop()
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ const nextId = (() => {
203
+ let id = 0
204
+ return () => {
205
+ id++
206
+ if (id === Number.MAX_SAFE_INTEGER) id = 1
207
+ return id
208
+ }
209
+ })()
@@ -0,0 +1,127 @@
1
+ import OS from 'os'
2
+ import MDNS, { QueryPacket } from 'multicast-dns'
3
+ import debug from 'debug'
4
+ import { SERVICE_TAG_LOCAL } from './constants.js'
5
+ import type { PeerId } from '@libp2p/interfaces/peer-id'
6
+ import type { Multiaddr, MultiaddrObject } from '@multiformats/multiaddr'
7
+ import { base58btc } from 'multiformats/bases/base58'
8
+ import type { RemoteInfo } from 'dgram'
9
+ import type { Answer } from 'dns-packet'
10
+
11
+ const log = Object.assign(debug('libp2p:mdns:compat:responder'), {
12
+ error: debug('libp2p:mdns:compat:responder:error')
13
+ })
14
+
15
+ export interface ResponderOptions {
16
+ peerId: PeerId
17
+ multiaddrs: Multiaddr[]
18
+ }
19
+
20
+ export class Responder {
21
+ private readonly _peerIdStr: string
22
+ private readonly _multiaddrs: Multiaddr[]
23
+ private _mdns?: MDNS.MulticastDNS
24
+
25
+ constructor (options: ResponderOptions) {
26
+ const { peerId, multiaddrs } = options
27
+
28
+ if (peerId == null) {
29
+ throw new Error('missing peerId parameter')
30
+ }
31
+
32
+ this._peerIdStr = peerId.toString(base58btc)
33
+ this._multiaddrs = multiaddrs
34
+ this._onQuery = this._onQuery.bind(this)
35
+ }
36
+
37
+ start () {
38
+ this._mdns = MDNS()
39
+ this._mdns.on('query', this._onQuery)
40
+ }
41
+
42
+ _onQuery (event: QueryPacket, info: RemoteInfo) {
43
+ const addresses = this._multiaddrs.reduce<MultiaddrObject[]>((acc, addr) => {
44
+ if (addr.isThinWaistAddress()) {
45
+ acc.push(addr.toOptions())
46
+ }
47
+ return acc
48
+ }, [])
49
+
50
+ // Only announce TCP for now
51
+ if (addresses.length === 0) {
52
+ return
53
+ }
54
+
55
+ const questions = event.questions ?? []
56
+
57
+ // Only respond to queries for our service tag
58
+ if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return
59
+
60
+ log('got query', event, info)
61
+
62
+ const answers: Answer[] = []
63
+ const peerServiceTagLocal = `${this._peerIdStr}.${SERVICE_TAG_LOCAL}`
64
+
65
+ answers.push({
66
+ name: SERVICE_TAG_LOCAL,
67
+ type: 'PTR',
68
+ class: 'IN',
69
+ ttl: 120,
70
+ data: peerServiceTagLocal
71
+ })
72
+
73
+ // Only announce TCP multiaddrs for now
74
+ const port = addresses[0].port
75
+
76
+ answers.push({
77
+ name: peerServiceTagLocal,
78
+ type: 'SRV',
79
+ class: 'IN',
80
+ ttl: 120,
81
+ data: {
82
+ priority: 10,
83
+ weight: 1,
84
+ port,
85
+ target: OS.hostname()
86
+ }
87
+ })
88
+
89
+ answers.push({
90
+ name: peerServiceTagLocal,
91
+ type: 'TXT',
92
+ class: 'IN',
93
+ ttl: 120,
94
+ data: [Buffer.from(this._peerIdStr)]
95
+ })
96
+
97
+ addresses.forEach((ma) => {
98
+ if ([4, 6].includes(ma.family)) {
99
+ answers.push({
100
+ name: OS.hostname(),
101
+ type: ma.family === 4 ? 'A' : 'AAAA',
102
+ class: 'IN',
103
+ ttl: 120,
104
+ data: ma.host
105
+ })
106
+ }
107
+ })
108
+
109
+ if (this._mdns != null) {
110
+ log('responding to query', answers)
111
+ this._mdns.respond(answers, info)
112
+ }
113
+ }
114
+
115
+ stop () {
116
+ if (this._mdns != null) {
117
+ this._mdns.removeListener('query', this._onQuery)
118
+ return new Promise<void>(resolve => {
119
+ if (this._mdns != null) {
120
+ this._mdns.destroy(resolve)
121
+ } else {
122
+ resolve()
123
+ }
124
+ })
125
+ }
126
+ }
127
+ }
package/src/index.ts ADDED
@@ -0,0 +1,191 @@
1
+ import multicastDNS from 'multicast-dns'
2
+ import { EventEmitter } from 'events'
3
+ import debug from 'debug'
4
+ import * as query from './query.js'
5
+ import { GoMulticastDNS } from './compat/index.js'
6
+ import type { PeerId } from '@libp2p/interfaces/peer-id'
7
+ import type PeerDiscovery from '@libp2p/interfaces/peer-discovery'
8
+ import type { Multiaddr } from '@multiformats/multiaddr'
9
+ import type { PeerData } from '@libp2p/interfaces/peer-data'
10
+
11
+ const log = Object.assign(debug('libp2p:mdns'), {
12
+ error: debug('libp2p:mdns:error')
13
+ })
14
+
15
+ export interface MulticastDNSOptions {
16
+ peerId: PeerId
17
+ broadcast?: boolean
18
+ interval?: number
19
+ serviceTag?: string
20
+ port?: number
21
+ multiaddrs?: Multiaddr[]
22
+ compat?: boolean
23
+ compatQueryPeriod?: number
24
+ compatQueryInterval?: number
25
+ }
26
+
27
+ export class MulticastDNS extends EventEmitter implements PeerDiscovery {
28
+ static tag = 'mdns'
29
+
30
+ public mdns?: multicastDNS.MulticastDNS
31
+
32
+ private readonly broadcast: boolean
33
+ private readonly interval: number
34
+ private readonly serviceTag: string
35
+ private readonly port: number
36
+ private readonly peerId: PeerId
37
+ private readonly peerMultiaddrs: Multiaddr[] // TODO: update this when multiaddrs change?
38
+ private _queryInterval: NodeJS.Timer | null
39
+ private readonly _goMdns?: GoMulticastDNS
40
+
41
+ constructor (options: MulticastDNSOptions) {
42
+ super()
43
+
44
+ if (options.peerId == null) {
45
+ throw new Error('needs own PeerId to work')
46
+ }
47
+
48
+ this.broadcast = options.broadcast !== false
49
+ this.interval = options.interval ?? (1e3 * 10)
50
+ this.serviceTag = options.serviceTag ?? 'ipfs.local'
51
+ this.port = options.port ?? 5353
52
+ this.peerId = options.peerId
53
+ this.peerMultiaddrs = options.multiaddrs ?? []
54
+ this._queryInterval = null
55
+ this._onPeer = this._onPeer.bind(this)
56
+ this._onMdnsQuery = this._onMdnsQuery.bind(this)
57
+ this._onMdnsResponse = this._onMdnsResponse.bind(this)
58
+
59
+ if (options.compat !== false) {
60
+ this._goMdns = new GoMulticastDNS({
61
+ multiaddrs: this.peerMultiaddrs,
62
+ peerId: options.peerId,
63
+ queryPeriod: options.compatQueryPeriod,
64
+ queryInterval: options.compatQueryInterval
65
+ })
66
+ this._goMdns.on('peer', this._onPeer)
67
+ }
68
+ }
69
+
70
+ isStarted () {
71
+ return Boolean(this.mdns)
72
+ }
73
+
74
+ /**
75
+ * Start sending queries to the LAN.
76
+ *
77
+ * @returns {void}
78
+ */
79
+ async start () {
80
+ if (this.mdns != null) {
81
+ return
82
+ }
83
+
84
+ this.mdns = multicastDNS({ port: this.port })
85
+ this.mdns.on('query', this._onMdnsQuery)
86
+ this.mdns.on('response', this._onMdnsResponse)
87
+
88
+ this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval)
89
+
90
+ if (this._goMdns != null) {
91
+ await this._goMdns.start()
92
+ }
93
+ }
94
+
95
+ _onMdnsQuery (event: multicastDNS.QueryPacket) {
96
+ if (this.mdns == null) {
97
+ return
98
+ }
99
+
100
+ query.gotQuery(event, this.mdns, this.peerId, this.peerMultiaddrs, this.serviceTag, this.broadcast)
101
+ }
102
+
103
+ _onMdnsResponse (event: multicastDNS.ResponsePacket) {
104
+ try {
105
+ const foundPeer = query.gotResponse(event, this.peerId, this.serviceTag)
106
+
107
+ if (foundPeer != null) {
108
+ this.emit('peer', foundPeer)
109
+ }
110
+ } catch (err) {
111
+ log('Error processing peer response', err)
112
+ }
113
+ }
114
+
115
+ _onPeer (peerData: PeerData) {
116
+ (this.mdns != null) && this.emit('peer', peerData)
117
+ }
118
+
119
+ /**
120
+ * Stop sending queries to the LAN.
121
+ *
122
+ * @returns {Promise}
123
+ */
124
+ async stop () {
125
+ if (this.mdns == null) {
126
+ return
127
+ }
128
+
129
+ this.mdns.removeListener('query', this._onMdnsQuery)
130
+ this.mdns.removeListener('response', this._onMdnsResponse)
131
+ this._goMdns?.removeListener('peer', this._onPeer)
132
+
133
+ if (this._queryInterval != null) {
134
+ clearInterval(this._queryInterval)
135
+ this._queryInterval = null
136
+ }
137
+
138
+ await Promise.all([
139
+ this._goMdns?.stop(),
140
+ new Promise<void>((resolve) => {
141
+ if (this.mdns != null) {
142
+ this.mdns.destroy(resolve)
143
+ } else {
144
+ resolve()
145
+ }
146
+ })
147
+ ])
148
+
149
+ this.mdns = undefined
150
+ }
151
+ }
152
+
153
+ /* for reference
154
+
155
+ [ { name: 'discovery.ipfs.io.local',
156
+ type: 'PTR',
157
+ class: 1,
158
+ ttl: 120,
159
+ data: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local' },
160
+
161
+ { name: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local',
162
+ type: 'SRV',
163
+ class: 1,
164
+ ttl: 120,
165
+ data: { priority: 10, weight: 1, port: 4001, target: 'lorien.local' } },
166
+
167
+ { name: 'lorien.local',
168
+ type: 'A',
169
+ class: 1,
170
+ ttl: 120,
171
+ data: '127.0.0.1' },
172
+
173
+ { name: 'lorien.local',
174
+ type: 'A',
175
+ class: 1,
176
+ ttl: 120,
177
+ data: '127.94.0.1' },
178
+
179
+ { name: 'lorien.local',
180
+ type: 'A',
181
+ class: 1,
182
+ ttl: 120,
183
+ data: '172.16.38.224' },
184
+
185
+ { name: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local',
186
+ type: 'TXT',
187
+ class: 1,
188
+ ttl: 120,
189
+ data: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC' } ],
190
+
191
+ */
package/src/query.ts ADDED
@@ -0,0 +1,166 @@
1
+ import os from 'os'
2
+ import debug from 'debug'
3
+ import { Multiaddr, MultiaddrObject } from '@multiformats/multiaddr'
4
+ import { base58btc } from 'multiformats/bases/base58'
5
+ import { PeerId } from '@libp2p/peer-id'
6
+ import type { PeerData } from '@libp2p/interfaces/peer-data'
7
+ import type { MulticastDNS, ResponsePacket, QueryPacket } from 'multicast-dns'
8
+ import type { SrvAnswer, StringAnswer, TxtAnswer, Answer } from 'dns-packet'
9
+
10
+ const log = Object.assign(debug('libp2p:mdns'), {
11
+ error: debug('libp2p:mdns:error')
12
+ })
13
+
14
+ export function queryLAN (mdns: MulticastDNS, serviceTag: string, interval: number) {
15
+ const query = () => {
16
+ log('query', serviceTag)
17
+ mdns.query({
18
+ questions: [{
19
+ name: serviceTag,
20
+ type: 'PTR'
21
+ }]
22
+ })
23
+ }
24
+
25
+ // Immediately start a query, then do it every interval.
26
+ query()
27
+ return setInterval(query, interval)
28
+ }
29
+
30
+ interface Answers {
31
+ ptr?: StringAnswer
32
+ srv?: SrvAnswer
33
+ txt?: TxtAnswer
34
+ a: StringAnswer[]
35
+ aaaa: StringAnswer[]
36
+ }
37
+
38
+ export function gotResponse (rsp: ResponsePacket, localPeerId: PeerId, serviceTag: string): PeerData | undefined {
39
+ if (rsp.answers == null) {
40
+ return
41
+ }
42
+
43
+ const answers: Answers = {
44
+ a: [],
45
+ aaaa: []
46
+ }
47
+
48
+ rsp.answers.forEach((answer) => {
49
+ switch (answer.type) {
50
+ case 'PTR': answers.ptr = answer; break
51
+ case 'SRV': answers.srv = answer; break
52
+ case 'TXT': answers.txt = answer; break
53
+ case 'A': answers.a.push(answer); break
54
+ case 'AAAA': answers.aaaa.push(answer); break
55
+ default: break
56
+ }
57
+ })
58
+
59
+ if (answers.ptr == null ||
60
+ answers.ptr.name !== serviceTag ||
61
+ answers.txt == null ||
62
+ answers.srv == null) {
63
+ return
64
+ }
65
+
66
+ const b58Id = answers.txt.data[0].toString()
67
+ const port = answers.srv.data.port
68
+ const multiaddrs: Multiaddr[] = []
69
+
70
+ answers.a.forEach((a) => {
71
+ const ma = new Multiaddr(`/ip4/${a.data}/tcp/${port}`)
72
+
73
+ if (!multiaddrs.some((m) => m.equals(ma))) {
74
+ multiaddrs.push(ma)
75
+ }
76
+ })
77
+
78
+ answers.aaaa.forEach((a) => {
79
+ const ma = new Multiaddr(`/ip6/${a.data}/tcp/${port}`)
80
+
81
+ if (!multiaddrs.some((m) => m.equals(ma))) {
82
+ multiaddrs.push(ma)
83
+ }
84
+ })
85
+
86
+ if (localPeerId.toString(base58btc) === b58Id) {
87
+ return // replied to myself, ignore
88
+ }
89
+
90
+ log('peer found -', b58Id)
91
+
92
+ return {
93
+ id: PeerId.fromString(b58Id),
94
+ multiaddrs,
95
+ protocols: []
96
+ }
97
+ }
98
+
99
+ export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerId: PeerId, multiaddrs: Multiaddr[], serviceTag: string, broadcast: boolean) {
100
+ if (!broadcast) {
101
+ return
102
+ }
103
+
104
+ const addresses: MultiaddrObject[] = multiaddrs.reduce<MultiaddrObject[]>((acc, addr) => {
105
+ if (addr.isThinWaistAddress()) {
106
+ acc.push(addr.toOptions())
107
+ }
108
+ return acc
109
+ }, [])
110
+
111
+ // Only announce TCP for now
112
+ if (addresses.length === 0) {
113
+ return
114
+ }
115
+
116
+ if (qry.questions[0] != null && qry.questions[0].name === serviceTag) {
117
+ const answers: Answer[] = []
118
+
119
+ answers.push({
120
+ name: serviceTag,
121
+ type: 'PTR',
122
+ class: 'IN',
123
+ ttl: 120,
124
+ data: peerId.toString(base58btc) + '.' + serviceTag
125
+ })
126
+
127
+ // Only announce TCP multiaddrs for now
128
+ const port = addresses[0].port
129
+
130
+ answers.push({
131
+ name: peerId.toString(base58btc) + '.' + serviceTag,
132
+ type: 'SRV',
133
+ class: 'IN',
134
+ ttl: 120,
135
+ data: {
136
+ priority: 10,
137
+ weight: 1,
138
+ port: port,
139
+ target: os.hostname()
140
+ }
141
+ })
142
+
143
+ answers.push({
144
+ name: peerId.toString(base58btc) + '.' + serviceTag,
145
+ type: 'TXT',
146
+ class: 'IN',
147
+ ttl: 120,
148
+ data: peerId.toString(base58btc)
149
+ })
150
+
151
+ addresses.forEach((addr) => {
152
+ if ([4, 6].includes(addr.family)) {
153
+ answers.push({
154
+ name: os.hostname(),
155
+ type: addr.family === 4 ? 'A' : 'AAAA',
156
+ class: 'IN',
157
+ ttl: 120,
158
+ data: addr.host
159
+ })
160
+ }
161
+ })
162
+
163
+ log('responding to query')
164
+ mdns.respond(answers)
165
+ }
166
+ }