diff --git a/README.md b/README.md index 4354526738..b1f9ed820e 100644 --- a/README.md +++ b/README.md @@ -24,27 +24,34 @@ $ npm i @libp2p/mdns ## Usage -```JavaScript -import { MDNS } from '@libp2p/mdns' +```Typescript +import { mdns } from '@libp2p/mdns' -const mdns = new MDNS(options) +const options = { + peerDiscovery: [ + mdns() + ] +} + +async function start () { + const libp2p = await createLibp2p(options) -mdns.on('peer', (peerData) => { - console.log('Found a peer in the local network', peerData.id.toString(), peerData.multiaddrs) -}) + libp2p.on('peer:discovery', function (peerId) { + console.log('found peer: ', peerId.toB58String()) + }) + + await libp2p.start() +} -// Broadcast for 20 seconds -mdns.start() -setTimeout(() => mdns.stop(), 20 * 1000) ``` - options - - `peerId` - PeerId to announce + - `peerName` - Peer name to announce (should not be peeer id), default random string - `multiaddrs` - multiaddrs to announce - `broadcast` - (true/false) announce our presence through mDNS, default `false` - `interval` - query interval, default 10 \* 1000 (10 seconds) - `serviceTag` - name of the service announce , default 'ipfs.local\` - - `compat` - enable/disable compatibility with go-libp2p-mdns, default `true` + ## MDNS messages diff --git a/package.json b/package.json index 30be638c76..58fc6a6a4b 100644 --- a/package.json +++ b/package.json @@ -135,24 +135,22 @@ "docs": "aegir docs" }, "dependencies": { - "@libp2p/interface-peer-discovery": "^1.0.1", - "@libp2p/interface-peer-id": "^2.0.0", - "@libp2p/interface-peer-info": "^1.0.3", - "@libp2p/interfaces": "^3.0.3", - "@libp2p/logger": "^2.0.1", - "@libp2p/peer-id": "^2.0.0", - "@multiformats/multiaddr": "^11.0.0", + "@libp2p/interface-peer-discovery": "^1.0.5", + "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interface-peer-info": "^1.0.8", + "@libp2p/interfaces": "^3.3.1", + "@libp2p/logger": "^2.0.5", + "@libp2p/peer-id": "^2.0.1", + "@multiformats/multiaddr": "^11.0.7", "@types/multicast-dns": "^7.2.1", - "dns-packet": "^5.4.0", - "multicast-dns": "^7.2.0" + "multicast-dns": "^7.2.5", + "dns-packet": "^5.4.0" }, "devDependencies": { - "@libp2p/interface-address-manager": "^2.0.0", - "@libp2p/interface-peer-discovery-compliance-tests": "^2.0.0", + "@libp2p/interface-address-manager": "^2.0.1", + "@libp2p/interface-peer-discovery-compliance-tests": "^2.0.1", "@libp2p/peer-id-factory": "^2.0.0", "aegir": "^38.1.2", - "delay": "^5.0.0", - "p-defer": "^4.0.0", "p-wait-for": "^5.0.0", "ts-sinon": "^2.0.2" } diff --git a/src/compat/constants.ts b/src/compat/constants.ts deleted file mode 100644 index e3cc76d9d9..0000000000 --- a/src/compat/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const SERVICE_TAG = '_ipfs-discovery._udp' -export const SERVICE_TAG_LOCAL = `${SERVICE_TAG}.local` -export const MULTICAST_IP = '224.0.0.251' -export const MULTICAST_PORT = 5353 diff --git a/src/compat/index.ts b/src/compat/index.ts deleted file mode 100644 index 2b896e3dcf..0000000000 --- a/src/compat/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Compatibility with Go libp2p MDNS -import { EventEmitter, CustomEvent } from '@libp2p/interfaces/events' -import { Responder } from './responder.js' -import { Querier } from './querier.js' -import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' -import { symbol } from '@libp2p/interface-peer-discovery' -import type { MulticastDNSComponents } from '../index.js' - -export interface GoMulticastDNSInit { - queryPeriod?: number - queryInterval?: number -} - -export class GoMulticastDNS extends EventEmitter implements PeerDiscovery { - private _started: boolean - private readonly _responder: Responder - private readonly _querier: Querier - - constructor (components: MulticastDNSComponents, options: GoMulticastDNSInit = {}) { - super() - const { queryPeriod, queryInterval } = options - - this._started = false - - this._responder = new Responder(components) - this._querier = new Querier(components, { - queryInterval, - queryPeriod - }) - - this._querier.addEventListener('peer', (evt) => { - this.dispatchEvent(new CustomEvent('peer', { detail: evt.detail })) - }) - } - - get [symbol] (): true { - return true - } - - get [Symbol.toStringTag] (): '@libp2p/go-mdns' { - return '@libp2p/go-mdns' - } - - isStarted (): boolean { - return this._started - } - - async start (): Promise { - if (this.isStarted()) { - return - } - - this._started = true - - await this._responder.start() - await this._querier.start() - } - - async stop (): Promise { - if (!this.isStarted()) { - return - } - - this._started = false - - await Promise.all([ - this._responder.stop(), - this._querier.stop() - ]) - } -} diff --git a/src/compat/querier.ts b/src/compat/querier.ts deleted file mode 100644 index 9b8aebf25b..0000000000 --- a/src/compat/querier.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { CustomEvent, EventEmitter } from '@libp2p/interfaces/events' -import MDNS from 'multicast-dns' -import { logger } from '@libp2p/logger' -import { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } from './constants.js' -import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' -import type { ResponsePacket } from 'multicast-dns' -import type { RemoteInfo } from 'dgram' -import { findPeerInfoInAnswers } from './utils.js' -import { symbol } from '@libp2p/interface-peer-discovery' -import type { Startable } from '@libp2p/interfaces/dist/src/startable.js' -import type { PeerId } from '@libp2p/interface-peer-id' - -const log = logger('libp2p:mdns:compat:querier') - -export interface QuerierInit { - queryInterval?: number - queryPeriod?: number -} - -export interface QuerierComponents { - peerId: PeerId -} - -export interface Handle { - stop: () => Promise -} - -export class Querier extends EventEmitter implements PeerDiscovery, Startable { - private readonly _init: Required - private _handle?: Handle - private readonly components: QuerierComponents - - constructor (components: QuerierComponents, init: QuerierInit = {}) { - super() - - const { queryInterval, queryPeriod } = init - - this.components = components - this._init = { - // Re-query in leu of network change detection (every 60s by default) - queryInterval: queryInterval ?? 60000, - // Time for which the MDNS server will stay alive waiting for responses - // Must be less than options.queryInterval! - queryPeriod: Math.min( - queryInterval ?? 60000, - queryPeriod ?? 5000 - ) - } - this._onResponse = this._onResponse.bind(this) - } - - get [symbol] (): true { - return true - } - - get [Symbol.toStringTag] (): '@libp2p/go-mdns-querier' { - return '@libp2p/go-mdns-querier' - } - - isStarted (): boolean { - return Boolean(this._handle) - } - - async start (): Promise { - 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) - - // @ts-expect-error @types/multicast-dns are wrong - 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: async () => { - mdns.removeListener('response', this._onResponse) - await new Promise(resolve => { mdns.destroy(resolve as () => void) }) - } - } - }, { - period: this._init.queryPeriod, - interval: this._init.queryInterval - }) - } - - _onResponse (event: ResponsePacket, info: RemoteInfo): void { - log.trace('received mDNS query response') - const answers = event.answers ?? [] - - const peerInfo = findPeerInfoInAnswers(answers, this.components.peerId) - - if (peerInfo == null) { - log('could not read peer data from query response') - return - } - - if (peerInfo.multiaddrs.length === 0) { - log('could not parse multiaddrs from mDNS response') - return - } - - log('discovered peer in mDNS qeury response %p', peerInfo.id) - - this.dispatchEvent(new CustomEvent('peer', { - detail: peerInfo - })) - } - - async stop (): Promise { - if (this._handle != null) { - await this._handle.stop() - } - } -} - -/** - * Run `fn` for a certain period of time, and then wait for an interval before - * running it again. `fn` must return an object with a stop function, which is - * called when the period expires. - */ -function periodically (fn: () => Handle, options: { period: number, interval: number }): { stop: () => Promise } { - let handle: Handle | null - let timeoutId: NodeJS.Timer - let stopped = false - - const reRun = (): void => { - handle = fn() - timeoutId = setTimeout(() => { - if (handle != null) { - handle.stop().catch(log) - } - - if (!stopped) { - timeoutId = setTimeout(reRun, options.interval) - } - - handle = null - }, options.period) - } - - reRun() - - return { - async stop () { - stopped = true - clearTimeout(timeoutId) - if (handle != null) { - await handle.stop() - } - } - } -} - -const nextId = (() => { - let id = 0 - return () => { - id++ - if (id === Number.MAX_SAFE_INTEGER) id = 1 - return id - } -})() diff --git a/src/compat/responder.ts b/src/compat/responder.ts deleted file mode 100644 index 5cd71e968d..0000000000 --- a/src/compat/responder.ts +++ /dev/null @@ -1,116 +0,0 @@ -import OS from 'os' -import MDNS, { QueryPacket } from 'multicast-dns' -import { logger } from '@libp2p/logger' -import { SERVICE_TAG_LOCAL } from './constants.js' -import { MultiaddrObject, protocols } from '@multiformats/multiaddr' -import type { RemoteInfo } from 'dgram' -import type { Answer } from 'dns-packet' -import type { MulticastDNSComponents } from '../index.js' - -const log = logger('libp2p:mdns:compat:responder') - -export class Responder { - private readonly components: MulticastDNSComponents - private _mdns?: MDNS.MulticastDNS - - constructor (components: MulticastDNSComponents) { - this.components = components - this._onQuery = this._onQuery.bind(this) - } - - async start (): Promise { - this._mdns = MDNS() - this._mdns.on('query', this._onQuery) - } - - _onQuery (event: QueryPacket, info: RemoteInfo): void { - const addresses = this.components.addressManager.getAddresses().reduce((acc, addr) => { - addr = addr.decapsulateCode(protocols('p2p').code) - - if (addr.isThinWaistAddress()) { - acc.push(addr.toOptions()) - } - - return acc - }, []) - - // Only announce TCP for now - if (addresses.length === 0) { - log('no tcp addresses configured so cannot respond to mDNS query') - return - } - - const questions = event.questions ?? [] - - // Only respond to queries for our service tag - if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return - - log.trace('got query', event, info) - - const answers: Answer[] = [] - const peerServiceTagLocal = `${this.components.peerId.toString()}.${SERVICE_TAG_LOCAL}` - - answers.push({ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }) - - answers.push({ - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: [Buffer.from(this.components.peerId.toString())] - }) - - addresses.forEach(ma => { - if (![4, 6].includes(ma.family)) { - return - } - - answers.push({ - name: peerServiceTagLocal, - type: 'SRV', - class: 'IN', - ttl: 120, - data: { - priority: 10, - weight: 1, - port: ma.port, - target: OS.hostname() - } - }) - - answers.push({ - name: OS.hostname(), - type: ma.family === 4 ? 'A' : 'AAAA', - class: 'IN', - ttl: 120, - data: ma.host - }) - }) - - if (this._mdns != null) { - log.trace('responding to query') - log.trace('query answers', answers) - - this._mdns.respond(answers, info) - } - } - - async stop (): Promise { - if (this._mdns != null) { - this._mdns.removeListener('query', this._onQuery) - await new Promise(resolve => { - if (this._mdns != null) { - this._mdns.destroy(resolve) - } else { - resolve() - } - }) - } - } -} diff --git a/src/compat/utils.ts b/src/compat/utils.ts deleted file mode 100644 index 324a79c1f2..0000000000 --- a/src/compat/utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { PeerInfo } from '@libp2p/interface-peer-info' -import type { PeerId } from '@libp2p/interface-peer-id' -import { logger } from '@libp2p/logger' -import { peerIdFromString } from '@libp2p/peer-id' -import { multiaddr } from '@multiformats/multiaddr' -import type { Multiaddr } from '@multiformats/multiaddr' -import type { Answer } from 'dns-packet' -import { SERVICE_TAG_LOCAL } from './constants.js' - -const log = logger('libp2p:mdns:compat:utils') - -export function findPeerInfoInAnswers (answers: Answer[], ourPeerId: PeerId): PeerInfo | undefined { - const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL) - - // Only deal with responses for our service tag - if (ptrRecord == null) { - return - } - - log.trace('got response', SERVICE_TAG_LOCAL) - - const txtRecord = answers.find(a => a.type === 'TXT') - if (txtRecord == null || txtRecord.type !== 'TXT') { - log('missing TXT record in response') - return - } - - let peerIdStr: string - try { - peerIdStr = txtRecord.data[0].toString() - } catch (err) { - log('failed to extract peer ID from TXT record data', txtRecord, err) - return - } - - let peerId: PeerId - try { - peerId = peerIdFromString(peerIdStr) - } catch (err) { - log('failed to create peer ID from TXT record data', peerIdStr, err) - return - } - - if (ourPeerId.equals(peerId)) { - log('ignoring reply to myself') - return - } - - const multiaddrs: Multiaddr[] = [] - const hosts: { A: Record, AAAA: Record } = { - A: {}, - AAAA: {} - } - - answers.forEach(answer => { - if (answer.type === 'A') { - hosts.A[answer.name] = answer.data - } - - if (answer.type === 'AAAA') { - hosts.AAAA[answer.name] = answer.data - } - }) - - answers.forEach(answer => { - if (answer.type === 'SRV') { - if (hosts.A[answer.data.target] != null) { - multiaddrs.push(multiaddr(`/ip4/${hosts.A[answer.data.target]}/tcp/${answer.data.port}/p2p/${peerId.toString()}`)) - } else if (hosts.AAAA[answer.data.target] != null) { - multiaddrs.push(multiaddr(`/ip6/${hosts.AAAA[answer.data.target]}/tcp/${answer.data.port}/p2p/${peerId.toString()}`)) - } else { - multiaddrs.push(multiaddr(`/dnsaddr/${answer.data.target}/tcp/${answer.data.port}/p2p/${peerId.toString()}`)) - } - } - }) - - return { - id: peerId, - multiaddrs, - protocols: [] - } -} diff --git a/src/index.ts b/src/index.ts index 09dadd01cd..70235b1a7e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,10 @@ import multicastDNS from 'multicast-dns' import { CustomEvent, EventEmitter } from '@libp2p/interfaces/events' import { logger } from '@libp2p/logger' import * as query from './query.js' -import { GoMulticastDNS } from './compat/index.js' import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-discovery' import type { PeerInfo } from '@libp2p/interface-peer-info' import { symbol } from '@libp2p/interface-peer-discovery' +import { stringGen } from './utils.js' import type { PeerId } from '@libp2p/interface-peer-id' import type { AddressManager } from '@libp2p/interface-address-manager' @@ -15,11 +15,9 @@ export interface MulticastDNSInit { broadcast?: boolean interval?: number serviceTag?: string + peerName?: string port?: number ip?: string - compat?: boolean - compatQueryPeriod?: number - compatQueryInterval?: number } export interface MulticastDNSComponents { @@ -33,33 +31,30 @@ class MulticastDNS extends EventEmitter implements PeerDisc private readonly broadcast: boolean private readonly interval: number private readonly serviceTag: string + private readonly peerName: string private readonly port: number private readonly ip: string private _queryInterval: ReturnType | null - private readonly _goMdns?: GoMulticastDNS private readonly components: MulticastDNSComponents constructor (components: MulticastDNSComponents, init: MulticastDNSInit = {}) { super() - this.components = components this.broadcast = init.broadcast !== false this.interval = init.interval ?? (1e3 * 10) - this.serviceTag = init.serviceTag ?? 'ipfs.local' + this.serviceTag = init.serviceTag ?? '_p2p._udp.local' this.ip = init.ip ?? '224.0.0.251' + this.peerName = init.peerName ?? stringGen(63) + // 63 is dns label limit + if (this.peerName.length >= 64) { + throw new Error('Peer name should be less than 64 chars long') + } this.port = init.port ?? 5353 + this.components = components this._queryInterval = null this._onPeer = this._onPeer.bind(this) this._onMdnsQuery = this._onMdnsQuery.bind(this) this._onMdnsResponse = this._onMdnsResponse.bind(this) - - if (init.compat !== false) { - this._goMdns = new GoMulticastDNS(components, { - queryPeriod: init.compatQueryPeriod, - queryInterval: init.compatQueryInterval - }) - this._goMdns.addEventListener('peer', this._onPeer) - } } get [symbol] (): true { @@ -89,10 +84,6 @@ class MulticastDNS extends EventEmitter implements PeerDisc this.mdns.on('response', this._onMdnsResponse) this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) - - if (this._goMdns != null) { - await this._goMdns.start() - } } _onMdnsQuery (event: multicastDNS.QueryPacket): void { @@ -101,14 +92,21 @@ class MulticastDNS extends EventEmitter implements PeerDisc } log.trace('received incoming mDNS query') - query.gotQuery(event, this.mdns, this.components.peerId, this.components.addressManager.getAddresses(), this.serviceTag, this.broadcast) + const localPeerId = this.components.peerId + query.gotQuery( + event, + this.mdns, + this.peerName, + this.components.addressManager.getAddresses().map((ma) => ma.encapsulate('/p2p/' + localPeerId.toString())), + this.serviceTag, + this.broadcast) } _onMdnsResponse (event: multicastDNS.ResponsePacket): void { log.trace('received mDNS query response') try { - const foundPeer = query.gotResponse(event, this.components.peerId, this.serviceTag) + const foundPeer = query.gotResponse(event, this.peerName, this.serviceTag) if (foundPeer != null) { log('discovered peer in mDNS qeury response %p', foundPeer.id) @@ -144,23 +142,19 @@ class MulticastDNS extends EventEmitter implements PeerDisc this.mdns.removeListener('query', this._onMdnsQuery) this.mdns.removeListener('response', this._onMdnsResponse) - this._goMdns?.removeEventListener('peer', this._onPeer) if (this._queryInterval != null) { clearInterval(this._queryInterval) this._queryInterval = null } - await Promise.all([ - this._goMdns?.stop(), - new Promise((resolve) => { - if (this.mdns != null) { - this.mdns.destroy(resolve) - } else { - resolve() - } - }) - ]) + await new Promise((resolve) => { + if (this.mdns != null) { + this.mdns.destroy(resolve) + } else { + resolve() + } + }) this.mdns = undefined } @@ -172,40 +166,17 @@ export function mdns (init: MulticastDNSInit = {}): (components: MulticastDNSCom /* for reference - [ { name: 'discovery.ipfs.io.local', + [ { name: '_p2p._udp.local', type: 'PTR', class: 1, ttl: 120, - data: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local' }, - - { name: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local', - type: 'SRV', - class: 1, - ttl: 120, - data: { priority: 10, weight: 1, port: 4001, target: 'lorien.local' } }, - - { name: 'lorien.local', - type: 'A', - class: 1, - ttl: 120, - data: '127.0.0.1' }, - - { name: 'lorien.local', - type: 'A', - class: 1, - ttl: 120, - data: '127.94.0.1' }, - - { name: 'lorien.local', - type: 'A', - class: 1, - ttl: 120, - data: '172.16.38.224' }, + data: 'XQxZeAH6MX2n4255fzYmyUCUdhQ0DAWv.p2p._udp.local' }, - { name: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC.discovery.ipfs.io.local', + { name: 'XQxZeAH6MX2n4255fzYmyUCUdhQ0DAWv.p2p._udp.local', type: 'TXT', class: 1, ttl: 120, - data: 'QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC' } ], + data: 'dnsaddr=/ip4/127.0.0.1/tcp/80/p2p/QmbBHw1Xx9pUpAbrVZUKTPL5Rsph5Q9GQhRvcWVBPFgGtC' }, +] */ diff --git a/src/query.ts b/src/query.ts index 44dacc772c..f2b00a631c 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,12 +1,9 @@ -import os from 'os' +import type { PeerInfo } from '@libp2p/interface-peer-info' import { logger } from '@libp2p/logger' -import { protocols, multiaddr } from '@multiformats/multiaddr' -import type { Multiaddr, MultiaddrObject } from '@multiformats/multiaddr' import { peerIdFromString } from '@libp2p/peer-id' -import type { PeerId } from '@libp2p/interface-peer-id' -import type { PeerInfo } from '@libp2p/interface-peer-info' -import type { MulticastDNS, ResponsePacket, QueryPacket } from 'multicast-dns' -import type { SrvAnswer, StringAnswer, TxtAnswer, Answer } from 'dns-packet' +import { multiaddr, Multiaddr } from '@multiformats/multiaddr' +import type { Answer, StringAnswer, TxtAnswer } from 'dns-packet' +import type { MulticastDNS, QueryPacket, ResponsePacket } from 'multicast-dns' const log = logger('libp2p:mdns:query') @@ -27,93 +24,60 @@ export function queryLAN (mdns: MulticastDNS, serviceTag: string, interval: numb return setInterval(query, interval) } -interface Answers { - ptr?: StringAnswer - srv?: SrvAnswer - txt?: TxtAnswer - a: StringAnswer[] - aaaa: StringAnswer[] -} - -export function gotResponse (rsp: ResponsePacket, localPeerId: PeerId, serviceTag: string): PeerInfo | undefined { +export function gotResponse (rsp: ResponsePacket, localPeerName: string, serviceTag: string): PeerInfo | undefined { if (rsp.answers == null) { return } - const answers: Answers = { - a: [], - aaaa: [] - } + let answerPTR: StringAnswer | undefined + const txtAnswers: TxtAnswer[] = [] rsp.answers.forEach((answer) => { switch (answer.type) { - case 'PTR': answers.ptr = answer; break - case 'SRV': answers.srv = answer; break - case 'TXT': answers.txt = answer; break - case 'A': answers.a.push(answer); break - case 'AAAA': answers.aaaa.push(answer); break + case 'PTR': answerPTR = answer; break + case 'TXT': txtAnswers.push(answer); break default: break } }) - if (answers.ptr == null || - answers.ptr.name !== serviceTag || - answers.txt == null || - answers.srv == null) { + if (answerPTR == null || + answerPTR?.name !== serviceTag || + txtAnswers.length === 0 || + answerPTR.data.startsWith(localPeerName)) { return } - const b58Id = answers.txt.data[0].toString() - const port = answers.srv.data.port - const multiaddrs: Multiaddr[] = [] - - answers.a.forEach((a) => { - const ma = multiaddr(`/ip4/${a.data}/tcp/${port}`) - - if (!multiaddrs.some((m) => m.equals(ma))) { - multiaddrs.push(ma) + try { + const multiaddrs: Multiaddr[] = txtAnswers + .flatMap((a) => a.data) + .filter(answerData => answerData.toString().startsWith('dnsaddr=')) + .map((answerData) => { + return multiaddr(answerData.toString().substring('dnsaddr='.length)) + }) + + const peerId = multiaddrs[0].getPeerId() + if (peerId == null) { + throw new Error("Multiaddr doesn't contain PeerId") } - }) + log('peer found %p', peerId) - answers.aaaa.forEach((a) => { - const ma = multiaddr(`/ip6/${a.data}/tcp/${port}`) - - if (!multiaddrs.some((m) => m.equals(ma))) { - multiaddrs.push(ma) + return { + id: peerIdFromString(peerId), + multiaddrs, + protocols: [] } - }) - - if (localPeerId.toString() === b58Id) { - return // replied to myself, ignore - } - - const id = peerIdFromString(b58Id) - - log('peer found %p', id) - - return { - id, - multiaddrs, - protocols: [] + } catch (e) { + log.error('failed to parse mdns response', e) } } -export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerId: PeerId, multiaddrs: Multiaddr[], serviceTag: string, broadcast: boolean): void { +export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerName: string, multiaddrs: Multiaddr[], serviceTag: string, broadcast: boolean): void { if (!broadcast) { log('not responding to mDNS query as broadcast mode is false') return } - const addresses: MultiaddrObject[] = multiaddrs.reduce((acc, addr) => { - if (addr.decapsulateCode(protocols('p2p').code).isThinWaistAddress()) { - acc.push(addr.toOptions()) - } - return acc - }, []) - - // Only announce TCP for now - if (addresses.length === 0) { - log('no thin waist addresses present, cannot respond to query') + if (multiaddrs.length === 0) { return } @@ -125,41 +89,18 @@ export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerId: PeerId, type: 'PTR', class: 'IN', ttl: 120, - data: peerId.toString() + '.' + serviceTag - }) - - // Only announce TCP multiaddrs for now - const port = addresses[0].port - - answers.push({ - name: peerId.toString() + '.' + serviceTag, - type: 'SRV', - class: 'IN', - ttl: 120, - data: { - priority: 10, - weight: 1, - port, - target: os.hostname() - } - }) - - answers.push({ - name: peerId.toString() + '.' + serviceTag, - type: 'TXT', - class: 'IN', - ttl: 120, - data: peerId.toString() + data: peerName + '.' + serviceTag }) - addresses.forEach((addr) => { - if ([4, 6].includes(addr.family)) { + multiaddrs.forEach((addr) => { + // spec mandates multiaddr contains peer id + if (addr.getPeerId() != null) { answers.push({ - name: os.hostname(), - type: addr.family === 4 ? 'A' : 'AAAA', + name: peerName + '.' + serviceTag, + type: 'TXT', class: 'IN', ttl: 120, - data: addr.host + data: 'dnsaddr=' + addr.toString() }) } }) diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000000..c88c951102 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,9 @@ +export function stringGen (len: number): string { + let text = '' + + const charset = 'abcdefghijklmnopqrstuvwxyz0123456789' + + for (let i = 0; i < len; i++) { text += charset.charAt(Math.floor(Math.random() * charset.length)) } + + return text +} diff --git a/test/compat/go-multicast-dns.spec.ts b/test/compat/go-multicast-dns.spec.ts deleted file mode 100644 index 5de4508f5f..0000000000 --- a/test/compat/go-multicast-dns.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-env mocha */ -import { expect } from 'aegir/chai' -import { multiaddr } from '@multiformats/multiaddr' -import { createEd25519PeerId } from '@libp2p/peer-id-factory' -import pDefer from 'p-defer' -import { GoMulticastDNS } from '../../src/compat/index.js' -import { stubInterface } from 'ts-sinon' -import type { AddressManager } from '@libp2p/interface-address-manager' -import type { PeerInfo } from '@libp2p/interface-peer-info' - -let port = 20000 - -async function createGoMulticastDNS (): Promise<{ mdns: GoMulticastDNS, components: any }> { - const peerId = await createEd25519PeerId() - const addressManager = stubInterface() - addressManager.getAddresses.returns([ - multiaddr(`/ip4/127.0.0.1/tcp/${port++}/p2p/${peerId.toString()}`), - multiaddr(`/ip4/127.0.0.1/tcp/${port++}/p2p/${peerId.toString()}`) - ]) - - const components = { - peerId, - addressManager - } - - const mdns = new GoMulticastDNS(components) - - return { - mdns, - components - } -} - -describe('GoMulticastDNS', () => { - it('should start and stop', async () => { - const { mdns } = await createGoMulticastDNS() - - await mdns.start() - await mdns.stop() - }) - - it('should ignore multiple start calls', async () => { - const { mdns } = await createGoMulticastDNS() - - await mdns.start() - await mdns.start() - - await mdns.stop() - }) - - it('should ignore unnecessary stop calls', async () => { - const { mdns } = await createGoMulticastDNS() - - await mdns.stop() - }) - - it('should emit peer data when peer is discovered', async () => { - const { mdns: mdnsA } = await createGoMulticastDNS() - const { mdns: mdnsB, components: componentsB } = await createGoMulticastDNS() - const defer = pDefer() - - mdnsA.addEventListener('peer', (evt) => { - const { id } = evt.detail - - if (componentsB.peerId.equals(id) !== true) { - return - } - - defer.resolve(evt.detail) - }) - - // Start in series - await mdnsA.start() - await mdnsB.start() - - const peerData = await defer.promise - - await Promise.all([ - mdnsA.stop(), - mdnsB.stop() - ]) - - expect(peerData.id.equals(componentsB.peerId)).to.be.true() - expect(peerData.multiaddrs.map(ma => ma.toString())).includes(componentsB.addressManager.getAddresses()[1].toString()) - }) -}) diff --git a/test/compat/querier.spec.ts b/test/compat/querier.spec.ts deleted file mode 100644 index a475b1444b..0000000000 --- a/test/compat/querier.spec.ts +++ /dev/null @@ -1,317 +0,0 @@ -/* eslint-env mocha */ -import { expect } from 'aegir/chai' -import { createEd25519PeerId } from '@libp2p/peer-id-factory' -import MDNS, { QueryPacket } from 'multicast-dns' -import OS from 'os' -import delay from 'delay' -import { Querier } from '../../src/compat/querier.js' -import { SERVICE_TAG_LOCAL } from '../../src/compat/constants.js' -import type { PeerId } from '@libp2p/interface-peer-id' -import type { RemoteInfo } from 'dgram' -import type { Answer } from 'dns-packet' - -describe('Querier', () => { - let querier: Querier - let mdns: MDNS.MulticastDNS - const peerAddrs = [ - '/ip4/127.0.0.1/tcp/20001', - '/ip4/127.0.0.1/tcp/20002' - ] - let peerIds: PeerId[] - - before(async () => { - peerIds = await Promise.all([ - createEd25519PeerId(), - createEd25519PeerId() - ]) - }) - - afterEach(async () => { - mdns?.destroy() - return await Promise.all([ - querier?.stop() - ]) - }) - - it('should start and stop', async () => { - querier = new Querier({ peerId: peerIds[0] }) - - await querier.start() - await querier.stop() - }) - - it('should query on interval', async () => { - querier = new Querier({ peerId: peerIds[0] }, { queryPeriod: 0, queryInterval: 10 }) - - mdns = MDNS() - - let queryCount = 0 - - mdns.on('query', event => { - const questions = event.questions ?? [] - if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return - queryCount++ - }) - - await querier.start() - await delay(100) - // Should have queried at least twice by now! - expect(queryCount >= 2).to.be.true() - }) - - it('should not emit peer for responses with non matching service tags', async () => { - await ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toString()}.${SERVICE_TAG_LOCAL}` - const bogusServiceTagLocal = '_ifps-discovery._udp' - - return [{ - name: bogusServiceTagLocal, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }] - }) - }) - - it('should not emit peer for responses with missing TXT record', async () => { - await ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toString()}.${SERVICE_TAG_LOCAL}` - - return [{ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }] - }) - }) - - it('should not emit peer for responses with missing peer ID in TXT record', async () => { - await ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toString()}.${SERVICE_TAG_LOCAL}` - - return [{ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }, { - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: [] // undefined peer ID - }] - }) - }) - - it('should not emit peer for responses to self', async () => { - await ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toString()}.${SERVICE_TAG_LOCAL}` - - return [{ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }, { - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: peerIds[0].toString() - }] - }) - }) - - it('should not emit peer for responses with invalid peer ID in TXT record', async () => { - await ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toString()}.${SERVICE_TAG_LOCAL}` - - return [{ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }, { - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: '🤪' - }] - }) - }) - - it('should not emit peer for responses with missing SRV record', async () => { - await ensureNoPeer(event => { - const peerServiceTagLocal = `${peerIds[1].toString()}.${SERVICE_TAG_LOCAL}` - - return [{ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }, { - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: peerIds[1].toString() - }] - }) - }) - - it('should emit peer for responses even if no multiaddrs', async () => { - await ensurePeer(event => { - const peerServiceTagLocal = `${peerIds[1].toString()}.${SERVICE_TAG_LOCAL}` - - return [{ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }, { - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: peerIds[1].toString() - }, { - name: peerServiceTagLocal, - type: 'SRV', - class: 'IN', - ttl: 120, - data: { - priority: 10, - weight: 1, - port: parseInt(peerAddrs[1].split('').pop() ?? '0'), - target: OS.hostname() - } - }] - }) - }) - - it('should emit peer for responses with valid multiaddrs', async () => { - await ensurePeer(event => { - const peerServiceTagLocal = `${peerIds[1].toString()}.${SERVICE_TAG_LOCAL}` - - return [{ - name: SERVICE_TAG_LOCAL, - type: 'PTR', - class: 'IN', - ttl: 120, - data: peerServiceTagLocal - }, { - name: peerServiceTagLocal, - type: 'TXT', - class: 'IN', - ttl: 120, - data: peerIds[1].toString() - }, { - name: peerServiceTagLocal, - type: 'SRV', - class: 'IN', - ttl: 120, - data: { - priority: 10, - weight: 1, - port: parseInt(peerAddrs[1].split('').pop() ?? '0'), - target: OS.hostname() - } - }, { - name: OS.hostname(), - type: peerAddrs[1].startsWith('/ip4') ? 'A' : 'AAAA', - class: 'IN', - ttl: 120, - data: peerAddrs[1].split('/')[2] - }] - }) - }) - - /** - * Ensure peerIds[1] are emitted from `querier` - * - * @param {Function} getResponse - Given a query, construct a response to test the querier - */ - async function ensurePeer (getResponse: (event: QueryPacket, info: RemoteInfo) => Answer[]): Promise { - const querier = new Querier({ peerId: peerIds[0] }) - mdns = MDNS() - - mdns.on('query', (event, info) => { - const questions = event.questions ?? [] - if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) return - mdns.respond(getResponse(event, info), info) - }) - - let peerId - - querier.addEventListener('peer', (evt) => { - const { id } = evt.detail - - // Ignore non-test peers - if (!peerIds[1].equals(id)) { - return - } - peerId = id - }) - - await querier.start() - await delay(100) - await querier.stop() - - if (peerId == null) { - throw new Error('Missing peer') - } - } - - /** - * Ensure none of peerIds are emitted from `querier` - * - * @param {Function} getResponse - Given a query, construct a response to test the querier - */ - async function ensureNoPeer (getResponse: (event: QueryPacket, info: RemoteInfo) => Answer[]): Promise { - const querier = new Querier({ peerId: peerIds[0] }) - mdns = MDNS() - - mdns.on('query', (event, info) => { - const questions = event.questions ?? [] - - if (!questions.some(q => q.name === SERVICE_TAG_LOCAL)) { - return - } - - mdns.respond(getResponse(event, info), info) - }) - - let peerId - - querier.addEventListener('peer', (evt) => { - const { id } = evt.detail - - // Ignore non-test peers - if (!peerIds[0].equals(id) && !peerIds[1].equals(id)) { - return - } - - peerId = id - }) - - await querier.start() - await delay(100) - await querier.stop() - - if (peerId == null) { - return - } - - throw Object.assign(new Error('Unexpected peer'), { peerId }) - } -}) diff --git a/test/compat/responder.spec.ts b/test/compat/responder.spec.ts deleted file mode 100644 index 5f2b0cc841..0000000000 --- a/test/compat/responder.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import type { Multiaddr } from '@multiformats/multiaddr' -import { multiaddr } from '@multiformats/multiaddr' -import { createEd25519PeerId } from '@libp2p/peer-id-factory' -import mDNS from 'multicast-dns' -import delay from 'delay' -import pDefer from 'p-defer' -import { Responder } from '../../src/compat/responder.js' -import { SERVICE_TAG_LOCAL, MULTICAST_IP, MULTICAST_PORT } from '../../src/compat/constants.js' -import type { PeerId } from '@libp2p/interface-peer-id' -import type { ResponsePacket } from 'multicast-dns' -import { stubInterface } from 'ts-sinon' -import { findPeerInfoInAnswers } from '../../src/compat/utils.js' -import type { AddressManager } from '@libp2p/interface-address-manager' -import type { PeerInfo } from '@libp2p/interface-peer-info' -import type { MulticastDNSComponents } from '../../src/index.js' - -describe('Responder', () => { - let responder: Responder - let mdns: mDNS.MulticastDNS - let peerIds: PeerId[] - let components: MulticastDNSComponents - let multiadddrs: Multiaddr[] - - beforeEach(async () => { - peerIds = await Promise.all([ - createEd25519PeerId(), - createEd25519PeerId() - ]) - - multiadddrs = [ - multiaddr(`/ip4/127.0.0.1/tcp/20001/p2p/${peerIds[0].toString()}`), - multiaddr(`/ip4/127.0.0.1/tcp/20002/p2p/${peerIds[0].toString()}`) - ] - - const addressManager = stubInterface() - addressManager.getAddresses.returns(multiadddrs) - - components = { peerId: peerIds[0], addressManager } - }) - - afterEach(async () => { - mdns?.destroy() - - return await Promise.all([ - responder?.stop() - ]) - }) - - it('should start and stop', async () => { - responder = new Responder(components) - - await responder.start() - await responder.stop() - }) - - it('should not respond to a query if no TCP addresses', async () => { - const peerId = await createEd25519PeerId() - responder = new Responder(components) - components.addressManager.getAddresses = () => [] - mdns = mDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) - - await responder.start() - - let response - - mdns.on('response', event => { - if (isResponseFrom(event, peerId)) { - response = event - } - }) - - mdns.query({ - id: 1, // id > 0 for unicast response - questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }] - }, { - address: MULTICAST_IP, - port: MULTICAST_PORT - }) - - await delay(100) - expect(response).to.not.exist() - }) - - it('should not respond to a query with non matching service tag', async () => { - responder = new Responder(components) - mdns = mDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) - - await responder.start() - - let response - - mdns.on('response', event => { - if (isResponseFrom(event, peerIds[0])) { - response = event - } - }) - - const bogusServiceTagLocal = '_ifps-discovery._udp' - - mdns.query({ - id: 1, // id > 0 for unicast response - questions: [{ name: bogusServiceTagLocal, type: 'PTR', class: 'IN' }] - }, { - address: MULTICAST_IP, - port: MULTICAST_PORT - }) - - await delay(100) - expect(response).to.not.exist() - }) - - it('should respond correctly', async () => { - responder = new Responder(components) - await responder.start() - const defer = pDefer() - - mdns = mDNS({ multicast: false, interface: '0.0.0.0', port: 0 }) - mdns.on('response', event => { - if (!isResponseFrom(event, peerIds[0])) { - return - } - - const peerInfo = findPeerInfoInAnswers(event.answers, peerIds[1]) - - if (peerInfo == null) { - defer.reject(new Error('Could not read PeerData from mDNS query response')); return - } - - defer.resolve(peerInfo) - }) - - mdns.query({ - id: 1, // id > 0 for unicast response - questions: [{ name: SERVICE_TAG_LOCAL, type: 'PTR', class: 'IN' }] - }, { - address: MULTICAST_IP, - port: MULTICAST_PORT - }) - - const peerData = await defer.promise - - expect(peerData.multiaddrs.map(ma => ma.toString())).to.include(multiadddrs[0].toString()) - expect(peerData.multiaddrs.map(ma => ma.toString())).to.include(multiadddrs[1].toString()) - }) -}) - -function isResponseFrom (res: ResponsePacket, fromPeerId: PeerId): boolean { - const answers = res.answers ?? [] - const ptrRecord = answers.find(a => a.type === 'PTR' && a.name === SERVICE_TAG_LOCAL) - if (ptrRecord == null) return false // Ignore irrelevant - - const txtRecord = answers.find(a => a.type === 'TXT') - if ((txtRecord == null) || txtRecord.type !== 'TXT') { - return false // Ignore missing TXT record - } - - let peerIdStr - try { - peerIdStr = txtRecord.data[0].toString() - } catch (err) { - return false // Ignore invalid peer ID data - } - - // Ignore response from someone else - if (fromPeerId.toString() !== peerIdStr) { - return false - } - - return true -} diff --git a/test/compliance.spec.ts b/test/compliance.spec.ts index efe36cd047..1cb42b4fc4 100644 --- a/test/compliance.spec.ts +++ b/test/compliance.spec.ts @@ -26,8 +26,7 @@ describe('compliance tests', () => { discovery = mdns({ broadcast: false, - port: 50001, - compat: true + port: 50001 })({ peerId: peerId1, addressManager diff --git a/test/multicast-dns.spec.ts b/test/multicast-dns.spec.ts index d517b961f9..ed96b07625 100644 --- a/test/multicast-dns.spec.ts +++ b/test/multicast-dns.spec.ts @@ -24,7 +24,6 @@ describe('MulticastDNS', () => { let aMultiaddrs: Multiaddr[] let pB: PeerId let bMultiaddrs: Multiaddr[] - let pC: PeerId let cMultiaddrs: Multiaddr[] let pD: PeerId let dMultiaddrs: Multiaddr[] @@ -32,8 +31,7 @@ describe('MulticastDNS', () => { before(async function () { this.timeout(80 * 1000) - ;[pA, pB, pC, pD] = await Promise.all([ - createEd25519PeerId(), + ;[pA, pB, pD] = await Promise.all([ createEd25519PeerId(), createEd25519PeerId(), createEd25519PeerId() @@ -67,13 +65,11 @@ describe('MulticastDNS', () => { const mdnsA = mdns({ broadcast: false, // do not talk to ourself - port: 50001, - compat: false + port: 50001 })(getComponents(pA, aMultiaddrs)) const mdnsB = mdns({ - port: 50001, // port must be the same - compat: false + port: 50001 // port must be the same })(getComponents(pB, bMultiaddrs)) await start(mdnsA, mdnsB) @@ -89,27 +85,24 @@ describe('MulticastDNS', () => { await stop(mdnsA, mdnsB) }) - it('only announce TCP multiaddrs', async function () { + it('announces all multiaddresses', async function () { this.timeout(40 * 1000) const mdnsA = mdns({ broadcast: false, // do not talk to ourself - port: 50003, - compat: false + port: 50003 })(getComponents(pA, aMultiaddrs)) - const mdnsC = mdns({ - port: 50003, // port must be the same - compat: false - })(getComponents(pC, cMultiaddrs)) + const mdnsB = mdns({ + port: 50003 // port must be the same + })(getComponents(pB, cMultiaddrs)) const mdnsD = mdns({ - port: 50003, // port must be the same - compat: false + port: 50003 // port must be the same })(getComponents(pD, dMultiaddrs)) - await start(mdnsA, mdnsC, mdnsD) + await start(mdnsA, mdnsB, mdnsD) const peers = new Map() - const expectedPeer = pC.toString() + const expectedPeer = pB.toString() const foundPeer = (evt: CustomEvent): Map => peers.set(evt.detail.id.toString(), evt.detail) mdnsA.addEventListener('peer', foundPeer) @@ -117,50 +110,20 @@ describe('MulticastDNS', () => { await pWaitFor(() => peers.has(expectedPeer)) mdnsA.removeEventListener('peer', foundPeer) - expect(peers.get(expectedPeer).multiaddrs.length).to.equal(1) + expect(peers.get(expectedPeer).multiaddrs.length).to.equal(3) - await stop(mdnsA, mdnsC, mdnsD) - }) - - it('announces IP6 addresses', async function () { - this.timeout(40 * 1000) - - const mdnsA = mdns({ - broadcast: false, // do not talk to ourself - port: 50001, - compat: false - })(getComponents(pA, aMultiaddrs)) - - const mdnsB = mdns({ - port: 50001, - compat: false - })(getComponents(pB, bMultiaddrs)) - - await start(mdnsA, mdnsB) - - const { detail: { id, multiaddrs } } = await new Promise>((resolve) => { - mdnsA.addEventListener('peer', resolve, { - once: true - }) - }) - - expect(pB.toString()).to.eql(id.toString()) - expect(multiaddrs.length).to.equal(2) - - await stop(mdnsA, mdnsB) + await stop(mdnsA, mdnsB, mdnsD) }) it('doesn\'t emit peers after stop', async function () { this.timeout(40 * 1000) const mdnsA = mdns({ - port: 50004, // port must be the same - compat: false + port: 50004 // port must be the same })(getComponents(pA, aMultiaddrs)) const mdnsC = mdns({ - port: 50004, - compat: false + port: 50004 })(getComponents(pD, dMultiaddrs)) await start(mdnsA) @@ -236,14 +199,12 @@ describe('MulticastDNS', () => { const mdnsA = mdns({ broadcast: false, // do not talk to ourself port: 50005, - ip: '224.0.0.252', - compat: false + ip: '224.0.0.252' })(getComponents(pA, aMultiaddrs)) const mdnsB = mdns({ port: 50005, // port must be the same - ip: '224.0.0.252', // ip must be the same - compat: false + ip: '224.0.0.252' // ip must be the same })(getComponents(pB, bMultiaddrs)) await start(mdnsA, mdnsB)