Skip to content

Commit 7d3c86b

Browse files
committed
fix: only check for non-wildcard ports
If we are passed a wildcard port, start a UDP mux listener on a ephemeral port, otherwise check the existing listeners before trying to start a new one. To be reused an existing listener must be owned by the current peer and be of the other IP family (e.g. you cannot listen on `[ip4-host]:1234` twice but you can listen on `[ip4-host:1234]` and `[ip6-host]:1234`. Fixes #3049
1 parent 907518b commit 7d3c86b

File tree

2 files changed

+103
-37
lines changed

2 files changed

+103
-37
lines changed

packages/transport-webrtc/src/private-to-public/listener.ts

+35-35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { isIPv4, isIPv6 } from '@chainsafe/is-ip'
2-
import { InvalidPeerIdError, TypedEventEmitter } from '@libp2p/interface'
1+
import { isIPv4 } from '@chainsafe/is-ip'
2+
import { InvalidParametersError, InvalidPeerIdError, TypedEventEmitter } from '@libp2p/interface'
33
import { getThinWaistAddresses } from '@libp2p/utils/get-thin-waist-addresses'
44
import { multiaddr, fromStringTuples } from '@multiformats/multiaddr'
55
import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
@@ -83,53 +83,53 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
8383
}
8484

8585
async listen (ma: Multiaddr): Promise<void> {
86-
const { host, port } = ma.toOptions()
87-
88-
// have to do this before any async work happens so starting two listeners
89-
// for the same port concurrently (e.g. ipv4/ipv6 both port 0) results in a
90-
// single mux listener. This is necessary because libjuice binds to all
91-
// interfaces for a given port so we we need to key on just the port number
92-
// not the host + the port number
93-
let existingServer = UDP_MUX_LISTENERS.find(s => s.port === port)
94-
95-
// if the server has not been started yet, or the port is a wildcard port
96-
// and there is already a wildcard port for this address family, start a new
97-
// UDP mux server
98-
const wildcardPorts = port === 0 && existingServer?.port === 0
99-
const sameAddressFamily = (existingServer?.isIPv4 === true && isIPv4(host)) || (existingServer?.isIPv6 === true && isIPv6(host))
100-
let createdMuxServer = false
101-
102-
if (existingServer == null || (wildcardPorts && sameAddressFamily)) {
103-
this.log('starting UDP mux server on %s:%p', host, port)
104-
existingServer = this.startUDPMuxServer(host, port)
105-
UDP_MUX_LISTENERS.push(existingServer)
106-
createdMuxServer = true
107-
}
86+
const { host, port, family } = ma.toOptions()
87+
88+
let udpMuxServer: UDPMuxServer | undefined
89+
90+
if (port !== 0) {
91+
// libjuice binds to all interfaces (IPv4/IPv6) for a given port so if we
92+
// want to listen on a specific port, and there's already a mux listener
93+
// for that port for the other family started by this node, we should
94+
// reuse it
95+
udpMuxServer = UDP_MUX_LISTENERS.find(s => s.port === port)
96+
97+
// make sure the port is free for the given family
98+
if (udpMuxServer != null && ((udpMuxServer.isIPv4 && family === 4) || (udpMuxServer.isIPv6 && family === 6))) {
99+
throw new InvalidParametersError(`There is already a listener for ${host}:${port}`)
100+
}
108101

109-
if (!existingServer.peerId.equals(this.components.peerId)) {
110-
// this would have to be another in-process peer so we are likely in a
111-
// testing environment
112-
throw new InvalidPeerIdError(`Another peer is already performing UDP mux on ${host}:${existingServer.port}`)
102+
// check that we own the mux server
103+
if (udpMuxServer != null && !udpMuxServer.peerId.equals(this.components.peerId)) {
104+
throw new InvalidPeerIdError(`Another peer is already performing UDP mux on ${host}:${port}`)
105+
}
113106
}
114107

115-
this.stunServer = await existingServer.server
116-
const address = this.stunServer.address()
108+
// start the mux server if we don't have one already
109+
if (udpMuxServer == null) {
110+
this.log('starting UDP mux server on %s:%p', host, port)
111+
udpMuxServer = this.startUDPMuxServer(host, port, family)
112+
UDP_MUX_LISTENERS.push(udpMuxServer)
113+
}
117114

118-
if (!createdMuxServer) {
119-
this.log('reused existing UDP mux server on %s:%p', host, address.port)
115+
if (family === 4) {
116+
udpMuxServer.isIPv4 = true
117+
} else if (family === 6) {
118+
udpMuxServer.isIPv6 = true
120119
}
121120

121+
this.stunServer = await udpMuxServer.server
122122
this.listeningMultiaddr = ma
123123
this.safeDispatchEvent('listening')
124124
}
125125

126-
private startUDPMuxServer (host: string, port: number): UDPMuxServer {
126+
private startUDPMuxServer (host: string, port: number, family: 4 | 6): UDPMuxServer {
127127
return {
128128
peerId: this.components.peerId,
129129
owner: this,
130130
port,
131-
isIPv4: isIPv4(host),
132-
isIPv6: isIPv6(host),
131+
isIPv4: family === 4,
132+
isIPv6: family === 6,
133133
server: Promise.resolve()
134134
.then(async (): Promise<StunServer> => {
135135
// ensure we have a certificate

packages/transport-webrtc/test/transport.spec.ts

+68-2
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ describe('WebRTCDirect Transport', () => {
114114
return this.skip()
115115
}
116116

117-
const ipv4 = multiaddr('/ip4/127.0.0.1/udp/0')
118-
const ipv6 = multiaddr('/ip6/::1/udp/0')
117+
const ipv4 = multiaddr('/ip4/127.0.0.1/udp/37287')
118+
const ipv6 = multiaddr('/ip6/::1/udp/37287')
119119

120120
await Promise.all([
121121
listener.listen(ipv4),
@@ -208,4 +208,70 @@ describe('WebRTCDirect Transport', () => {
208208
expect(ma.toString()).to.include('/udp/12346/webrtc-direct/certhash/u', 'did not add certhash to all WebRTC Direct addresses')
209209
}
210210
})
211+
212+
it('can start listeners for two nodes on wildcard socket addresses', async function () {
213+
if ((!isNode && !isElectronMain) || !supportsIpV6()) {
214+
return this.skip()
215+
}
216+
217+
const otherTransport = new WebRTCDirectTransport({
218+
...components,
219+
peerId: peerIdFromPrivateKey(await generateKeyPair('Ed25519'))
220+
})
221+
const otherTransportIp4Listener = otherTransport.createListener({
222+
upgrader
223+
})
224+
const otherTransportIp6Listener = otherTransport.createListener({
225+
upgrader
226+
})
227+
228+
const ip6Listener = transport.createListener({
229+
upgrader
230+
})
231+
232+
const ipv4 = multiaddr('/ip4/0.0.0.0/udp/0')
233+
const ipv6 = multiaddr('/ip6/::/udp/0')
234+
235+
await Promise.all([
236+
listener.listen(ipv4),
237+
otherTransportIp4Listener.listen(ipv4),
238+
ip6Listener.listen(ipv6),
239+
otherTransportIp6Listener.listen(ipv6)
240+
])
241+
242+
assertAllMultiaddrsHaveSamePort(listener.getAddrs())
243+
assertAllMultiaddrsHaveSamePort(otherTransportIp4Listener.getAddrs())
244+
assertAllMultiaddrsHaveSamePort(otherTransportIp6Listener.getAddrs())
245+
assertAllMultiaddrsHaveSamePort(ip6Listener.getAddrs())
246+
247+
await listener.close()
248+
await otherTransportIp4Listener.close()
249+
await otherTransportIp6Listener.close()
250+
await ip6Listener.close()
251+
})
252+
253+
it('can start multiple wildcard listeners', async function () {
254+
if ((!isNode && !isElectronMain) || !supportsIpV6()) {
255+
return this.skip()
256+
}
257+
258+
const otherListener = transport.createListener({
259+
upgrader
260+
})
261+
262+
const ipv4 = multiaddr('/ip4/0.0.0.0/udp/0')
263+
264+
await Promise.all([
265+
listener.listen(ipv4),
266+
otherListener.listen(ipv4)
267+
])
268+
269+
assertAllMultiaddrsHaveSamePort(listener.getAddrs())
270+
assertAllMultiaddrsHaveSamePort(otherListener.getAddrs())
271+
272+
expect(listener.getAddrs()[0].toOptions().port).to.not.equal(otherListener.getAddrs()[0].toOptions().port, 'wildcard listeners did not listen on different ports')
273+
274+
await listener.close()
275+
await otherListener.close()
276+
})
211277
})

0 commit comments

Comments
 (0)