Skip to content

Commit 4f19234

Browse files
authoredSep 1, 2023
fix(@libp2p/mdns): do not send TXT records that are too long (#2014)
The data field of mDNS TXT records has a [hard limit of 255 characters](https://agnat.github.io/node_mdns/user_guide.html#txt_records) - some multiaddrs can be longer than this so filter them out before sending, otherwise remote peers can fail to parse our mDNS response. We should also only be sending [link-local addresses](https://github.com/libp2p/specs/blob/master/discovery/mdns.md#issues) in mDNS responses so filter any non-link-local addresses out too, though go-libp2p includes loopback addresses even though it probably shouldn't (according to the mDNS spec which conflicts with our mDNS Peer Discovery spec) so we continue to do so as well. Fixes #2012
1 parent 3282563 commit 4f19234

File tree

5 files changed

+159
-44
lines changed

5 files changed

+159
-44
lines changed
 

‎packages/peer-discovery-mdns/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@libp2p/interface": "^0.1.2",
4848
"@libp2p/logger": "^3.0.2",
4949
"@libp2p/peer-id": "^3.0.2",
50+
"@libp2p/utils": "^4.0.2",
5051
"@multiformats/multiaddr": "^12.1.5",
5152
"@types/multicast-dns": "^7.2.1",
5253
"dns-packet": "^5.4.0",

‎packages/peer-discovery-mdns/src/index.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as query from './query.js'
66
import { stringGen } from './utils.js'
77
import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface/peer-discovery'
88
import type { PeerInfo } from '@libp2p/interface/peer-info'
9+
import type { Startable } from '@libp2p/interface/src/startable.js'
910
import type { AddressManager } from '@libp2p/interface-internal/address-manager'
1011

1112
const log = logger('libp2p:mdns')
@@ -23,7 +24,7 @@ export interface MulticastDNSComponents {
2324
addressManager: AddressManager
2425
}
2526

26-
class MulticastDNS extends EventEmitter<PeerDiscoveryEvents> implements PeerDiscovery {
27+
class MulticastDNS extends EventEmitter<PeerDiscoveryEvents> implements PeerDiscovery, Startable {
2728
public mdns?: multicastDNS.MulticastDNS
2829

2930
private readonly broadcast: boolean
@@ -50,9 +51,10 @@ class MulticastDNS extends EventEmitter<PeerDiscoveryEvents> implements PeerDisc
5051
this.port = init.port ?? 5353
5152
this.components = components
5253
this._queryInterval = null
53-
this._onPeer = this._onPeer.bind(this)
5454
this._onMdnsQuery = this._onMdnsQuery.bind(this)
5555
this._onMdnsResponse = this._onMdnsResponse.bind(this)
56+
this._onMdnsWarning = this._onMdnsWarning.bind(this)
57+
this._onMdnsError = this._onMdnsError.bind(this)
5658
}
5759

5860
readonly [peerDiscovery] = this
@@ -76,6 +78,8 @@ class MulticastDNS extends EventEmitter<PeerDiscoveryEvents> implements PeerDisc
7678
this.mdns = multicastDNS({ port: this.port, ip: this.ip })
7779
this.mdns.on('query', this._onMdnsQuery)
7880
this.mdns.on('response', this._onMdnsResponse)
81+
this.mdns.on('warning', this._onMdnsWarning)
82+
this.mdns.on('error', this._onMdnsError)
7983

8084
this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval)
8185
}
@@ -113,14 +117,12 @@ class MulticastDNS extends EventEmitter<PeerDiscoveryEvents> implements PeerDisc
113117
}
114118
}
115119

116-
_onPeer (evt: CustomEvent<PeerInfo>): void {
117-
if (this.mdns == null) {
118-
return
119-
}
120+
_onMdnsWarning (err: Error): void {
121+
log.error('mdns warning', err)
122+
}
120123

121-
this.dispatchEvent(new CustomEvent<PeerInfo>('peer', {
122-
detail: evt.detail
123-
}))
124+
_onMdnsError (err: Error): void {
125+
log.error('mdns error', err)
124126
}
125127

126128
/**
@@ -135,6 +137,8 @@ class MulticastDNS extends EventEmitter<PeerDiscoveryEvents> implements PeerDisc
135137

136138
this.mdns.removeListener('query', this._onMdnsQuery)
137139
this.mdns.removeListener('response', this._onMdnsResponse)
140+
this.mdns.removeListener('warning', this._onMdnsWarning)
141+
this.mdns.removeListener('error', this._onMdnsError)
138142

139143
if (this._queryInterval != null) {
140144
clearInterval(this._queryInterval)

‎packages/peer-discovery-mdns/src/query.ts

+46-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { logger } from '@libp2p/logger'
22
import { peerIdFromString } from '@libp2p/peer-id'
3-
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'
3+
import { isPrivate } from '@libp2p/utils/multiaddr/is-private'
4+
import { multiaddr, type Multiaddr, protocols } from '@multiformats/multiaddr'
45
import type { PeerInfo } from '@libp2p/interface/peer-info'
56
import type { Answer, StringAnswer, TxtAnswer } from 'dns-packet'
67
import type { MulticastDNS, QueryPacket, ResponsePacket } from 'multicast-dns'
@@ -9,7 +10,7 @@ const log = logger('libp2p:mdns:query')
910

1011
export function queryLAN (mdns: MulticastDNS, serviceTag: string, interval: number): ReturnType<typeof setInterval> {
1112
const query = (): void => {
12-
log('query', serviceTag)
13+
log.trace('query', serviceTag)
1314

1415
mdns.query({
1516
questions: [{
@@ -40,6 +41,16 @@ export function gotResponse (rsp: ResponsePacket, localPeerName: string, service
4041
}
4142
})
4243

44+
// according to the spec, peer details should be in the additional records,
45+
// not the answers though it seems go-libp2p at least ignores this?
46+
// https://github.com/libp2p/specs/blob/master/discovery/mdns.md#response
47+
rsp.additionals.forEach((answer) => {
48+
switch (answer.type) {
49+
case 'TXT': txtAnswers.push(answer); break
50+
default: break
51+
}
52+
})
53+
4354
if (answerPTR == null ||
4455
answerPTR?.name !== serviceTag ||
4556
txtAnswers.length === 0 ||
@@ -63,7 +74,7 @@ export function gotResponse (rsp: ResponsePacket, localPeerName: string, service
6374

6475
return {
6576
id: peerIdFromString(peerId),
66-
multiaddrs,
77+
multiaddrs: multiaddrs.map(addr => addr.decapsulateCode(protocols('p2p').code)),
6778
protocols: []
6879
}
6980
} catch (e) {
@@ -92,20 +103,45 @@ export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerName: string
92103
data: peerName + '.' + serviceTag
93104
})
94105

95-
multiaddrs.forEach((addr) => {
96-
// spec mandates multiaddr contains peer id
97-
if (addr.getPeerId() != null) {
106+
multiaddrs
107+
// mDNS requires link-local addresses only
108+
// https://github.com/libp2p/specs/blob/master/discovery/mdns.md#issues
109+
.filter(isLinkLocal)
110+
.forEach((addr) => {
111+
const data = 'dnsaddr=' + addr.toString()
112+
113+
// TXT record fields have a max data length of 255 bytes
114+
// see 6.1 - https://www.ietf.org/rfc/rfc6763.txt
115+
if (data.length > 255) {
116+
log('multiaddr %a is too long to use in mDNS query response', addr)
117+
return
118+
}
119+
120+
// spec mandates multiaddr contains peer id
121+
if (addr.getPeerId() == null) {
122+
log('multiaddr %a did not have a peer ID so cannot be used in mDNS query response', addr)
123+
return
124+
}
125+
98126
answers.push({
99127
name: peerName + '.' + serviceTag,
100128
type: 'TXT',
101129
class: 'IN',
102130
ttl: 120,
103-
data: 'dnsaddr=' + addr.toString()
131+
data
104132
})
105-
}
106-
})
133+
})
107134

108-
log('responding to query')
135+
log.trace('responding to query')
109136
mdns.respond(answers)
110137
}
111138
}
139+
140+
function isLinkLocal (ma: Multiaddr): boolean {
141+
// match private ip4/ip6 & loopback addresses
142+
if (isPrivate(ma)) {
143+
return true
144+
}
145+
146+
return false
147+
}

‎packages/peer-discovery-mdns/test/compliance.spec.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-env mocha */
22

33
import { CustomEvent } from '@libp2p/interface/events'
4+
import { isStartable } from '@libp2p/interface/startable'
45
import tests from '@libp2p/interface-compliance-tests/peer-discovery'
56
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
67
import { multiaddr } from '@multiformats/multiaddr'
@@ -32,16 +33,21 @@ describe('compliance tests', () => {
3233
})
3334

3435
// Trigger discovery
35-
const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2d'
36-
37-
// @ts-expect-error not a PeerDiscovery field
38-
intervalId = setInterval(() => discovery._onPeer(new CustomEvent('peer', {
39-
detail: {
40-
id: peerId2,
41-
multiaddrs: [multiaddr(maStr)],
42-
protocols: []
36+
const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star'
37+
38+
intervalId = setInterval(() => {
39+
if (isStartable(discovery) && !discovery.isStarted()) {
40+
return
4341
}
44-
})), 1000)
42+
43+
discovery.dispatchEvent(new CustomEvent('peer', {
44+
detail: {
45+
id: peerId2,
46+
multiaddrs: [multiaddr(maStr)],
47+
protocols: []
48+
}
49+
}))
50+
}, 1000)
4551

4652
return discovery
4753
},

‎packages/peer-discovery-mdns/test/multicast-dns.spec.ts

+84-16
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,25 @@ describe('MulticastDNS', () => {
3838
])
3939

4040
aMultiaddrs = [
41-
multiaddr('/ip4/127.0.0.1/tcp/20001'),
41+
multiaddr('/ip4/192.168.1.142/tcp/20001'),
4242
multiaddr('/dns4/webrtc-star.discovery.libp2p.io/tcp/443/wss/p2p-webrtc-star'),
4343
multiaddr('/dns4/discovery.libp2p.io/tcp/8443')
4444
]
4545

4646
bMultiaddrs = [
47-
multiaddr('/ip4/127.0.0.1/tcp/20002'),
48-
multiaddr('/ip6/::1/tcp/20002'),
47+
multiaddr('/ip4/192.168.1.143/tcp/20002'),
48+
multiaddr('/ip6/2604:1380:4602:5c00::3/tcp/20002'),
4949
multiaddr('/dnsaddr/discovery.libp2p.io')
5050
]
5151

5252
cMultiaddrs = [
53-
multiaddr('/ip4/127.0.0.1/tcp/20003'),
54-
multiaddr('/ip4/127.0.0.1/tcp/30003/ws'),
53+
multiaddr('/ip4/192.168.1.144/tcp/20003'),
54+
multiaddr('/ip4/192.168.1.144/tcp/30003/ws'),
5555
multiaddr('/dns4/discovery.libp2p.io')
5656
]
5757

5858
dMultiaddrs = [
59-
multiaddr('/ip4/127.0.0.1/tcp/30003/ws')
59+
multiaddr('/ip4/192.168.1.145/tcp/30003/ws')
6060
]
6161
})
6262

@@ -110,7 +110,8 @@ describe('MulticastDNS', () => {
110110
await pWaitFor(() => peers.has(expectedPeer))
111111
mdnsA.removeEventListener('peer', foundPeer)
112112

113-
expect(peers.get(expectedPeer).multiaddrs.length).to.equal(3)
113+
// everything except loopback
114+
expect(peers.get(expectedPeer).multiaddrs.length).to.equal(2)
114115

115116
await stop(mdnsA, mdnsB, mdnsD)
116117
})
@@ -141,15 +142,6 @@ describe('MulticastDNS', () => {
141142
await stop(mdnsC)
142143
})
143144

144-
it('should start and stop with go-libp2p-mdns compat', async () => {
145-
const mdnsA = mdns({
146-
port: 50004
147-
})(getComponents(pA, aMultiaddrs))
148-
149-
await start(mdnsA)
150-
await stop(mdnsA)
151-
})
152-
153145
it('should not emit undefined peer ids', async () => {
154146
const mdnsA = mdns({
155147
port: 50004
@@ -219,4 +211,80 @@ describe('MulticastDNS', () => {
219211

220212
await stop(mdnsA, mdnsB)
221213
})
214+
215+
it('only includes link-local addresses', async function () {
216+
this.timeout(40 * 1000)
217+
218+
// these are not link-local addresses
219+
const publicAddress = '/ip4/48.52.76.32/tcp/1234'
220+
const relayDnsAddress = `/dnsaddr/example.org/tcp/1234/p2p/${pD.toString()}/p2p-circuit`
221+
const dnsAddress = '/dns4/example.org/tcp/1234'
222+
223+
// this address is too long to fit in a TXT record
224+
const longAddress = `/ip4/192.168.1.142/udp/4001/quic-v1/webtransport/certhash/uEiDils3hWFJmsWOJIoMPxAcpzlyFNxTDZpklIoB8643ddw/certhash/uEiAM4BGr4OMK3O9cFGwfbNc4J7XYnsKE5wNPKKaTLa4fkw/p2p/${pD.toString()}/p2p-circuit`
225+
226+
// these are link-local addresses
227+
const relayAddress = `/ip4/192.168.1.142/tcp/1234/p2p/${pD.toString()}/p2p-circuit`
228+
const localAddress = '/ip4/192.168.1.123/tcp/1234'
229+
const localWsAddress = '/ip4/192.168.1.123/tcp/1234/ws'
230+
231+
// these are not link-local but go-libp2p advertises loopback addresses even
232+
// though you shouldn't for mDNS
233+
const loopbackAddress = '/ip4/127.0.0.1/tcp/1234'
234+
const loopbackAddress6 = '/ip6/::1/tcp/1234'
235+
236+
const mdnsA = mdns({
237+
broadcast: false, // do not talk to ourself
238+
port: 50005,
239+
ip: '224.0.0.252'
240+
})(getComponents(pA, aMultiaddrs))
241+
242+
const mdnsB = mdns({
243+
port: 50005, // port must be the same
244+
ip: '224.0.0.252' // ip must be the same
245+
})(getComponents(pB, [
246+
multiaddr(publicAddress),
247+
multiaddr(relayAddress),
248+
multiaddr(relayDnsAddress),
249+
multiaddr(localAddress),
250+
multiaddr(loopbackAddress),
251+
multiaddr(loopbackAddress6),
252+
multiaddr(dnsAddress),
253+
multiaddr(longAddress),
254+
multiaddr(localWsAddress)
255+
]))
256+
257+
await start(mdnsA, mdnsB)
258+
259+
const { detail: { id, multiaddrs } } = await new Promise<CustomEvent<PeerInfo>>((resolve) => {
260+
mdnsA.addEventListener('peer', resolve, {
261+
once: true
262+
})
263+
})
264+
265+
expect(pB.toString()).to.eql(id.toString())
266+
267+
;[
268+
publicAddress,
269+
relayDnsAddress,
270+
dnsAddress,
271+
longAddress
272+
].forEach(addr => {
273+
expect(multiaddrs.map(ma => ma.toString()))
274+
.to.not.include(addr)
275+
})
276+
277+
;[
278+
relayAddress,
279+
localAddress,
280+
localWsAddress,
281+
loopbackAddress,
282+
loopbackAddress6
283+
].forEach(addr => {
284+
expect(multiaddrs.map(ma => ma.toString()))
285+
.to.include(addr)
286+
})
287+
288+
await stop(mdnsA, mdnsB)
289+
})
222290
})

0 commit comments

Comments
 (0)
Please sign in to comment.