Skip to content

Commit

Permalink
fix: support multiple wildcard ports
Browse files Browse the repository at this point in the history
  • Loading branch information
achingbrain committed Feb 20, 2025
1 parent ff951f1 commit 20e8844
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 61 deletions.
87 changes: 56 additions & 31 deletions packages/transport-webrtc/src/private-to-public/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,15 @@ const UDP_PROTOCOL = protocols('udp')
const IP4_PROTOCOL = protocols('ip4')
const IP6_PROTOCOL = protocols('ip6')

interface UDPMuxServer {
server: Promise<StunServer>
isIPv4: boolean
isIPv6: boolean
port: number
}

export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> implements Listener {
private readonly servers: Record<number, Promise<StunServer>>
private readonly servers: UDPMuxServer[]
private readonly multiaddrs: Multiaddr[]
private certificate?: TransportCertificate
private readonly connections: Map<string, DirectRTCPeerConnection>
Expand All @@ -63,7 +70,7 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
this.init = init
this.components = components
this.multiaddrs = []
this.servers = {}
this.servers = []
this.connections = new Map()
this.log = components.logger.forComponent('libp2p:webrtc-direct:listener')
this.certificate = init.certificates?.[0]
Expand Down Expand Up @@ -103,11 +110,17 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
// single mux listener. This is necessary because libjuice binds to all
// interfaces for a given port so we we need to key on just the port number
// not the host + the port number
if (this.servers[port] == null) {
this.servers[port] = this.startUDPMuxServer(host, port)
let existingServer = this.servers.find(s => s.port === port)

// if the server has not been started yet, or the port is a wildcard port
// and there is already a wildcard port for this address family, start a new
// UDP mux server
if (existingServer == null || (port === 0 && existingServer.port === 0 && ((existingServer.isIPv4 && isIPv4(host)) || (existingServer.isIPv6 && isIPv6(host))))) {
existingServer = this.startUDPMuxServer(host, port)
this.servers.push(existingServer)
}

const server = await this.servers[port]
const server = await existingServer.server
const address = server.address()

getNetworkAddresses(host, address.port, ipVersion).forEach((ma) => {
Expand All @@ -117,31 +130,43 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
this.safeDispatchEvent('listening')
}

private async startUDPMuxServer (host: string, port: number): Promise<StunServer> {
if (port === 0) {
// libjuice doesn't map 0 to a random free port so we have to do it
// ourselves
port = await getPort()
}

// ensure we have a certificate
if (this.certificate == null) {
const keyPair = await crypto.subtle.generateKey({
name: 'ECDSA',
namedCurve: 'P-256'
}, true, ['sign', 'verify'])

this.certificate = await generateTransportCertificate(keyPair, {
days: 365 * 10
})
}

return stunListener(host, port, this.log, (ufrag, remoteHost, remotePort) => {
this.incomingConnection(ufrag, remoteHost, remotePort)
.catch(err => {
this.log.error('error processing incoming STUN request', err)
private startUDPMuxServer (host: string, port: number): UDPMuxServer {
return {
port,
isIPv4: isIPv4(host),
isIPv6: isIPv6(host),
server: Promise.resolve()
.then(async (): Promise<StunServer> => {
if (port === 0) {
// libjuice doesn't map 0 to a random free port so we have to do it
// ourselves
port = await getPort()
}

// ensure we have a certificate
if (this.certificate == null) {
const keyPair = await crypto.subtle.generateKey({
name: 'ECDSA',
namedCurve: 'P-256'
}, true, ['sign', 'verify'])

const certificate = await generateTransportCertificate(keyPair, {
days: 365 * 10
})

if (this.certificate == null) {
this.certificate = certificate
}
}

return stunListener(host, port, this.log, (ufrag, remoteHost, remotePort) => {
this.incomingConnection(ufrag, remoteHost, remotePort)
.catch(err => {
this.log.error('error processing incoming STUN request', err)
})
})
})
})
}
}

private async incomingConnection (ufrag: string, remoteHost: string, remotePort: number): Promise<void> {
Expand Down Expand Up @@ -203,8 +228,8 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl

// stop all UDP mux listeners
await Promise.all(
Object.values(this.servers).map(async p => {
const server = await p
this.servers.map(async p => {
const server = await p.server
await server.close()
})
)
Expand Down
68 changes: 38 additions & 30 deletions packages/transport-webrtc/test/transport.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-floating-promises */

import { generateKeyPair } from '@libp2p/crypto/keys'
import { transportSymbol, type Upgrader } from '@libp2p/interface'
import { transportSymbol, type Upgrader, type Listener, type Transport } from '@libp2p/interface'
import { defaultLogger } from '@libp2p/logger'
import { peerIdFromPrivateKey } from '@libp2p/peer-id'
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'
Expand All @@ -28,8 +28,11 @@ function assertAllMultiaddrsHaveSamePort (addrs: Multiaddr[]): void {

describe('WebRTCDirect Transport', () => {
let components: WebRTCDirectTransportComponents
let listener: Listener
let upgrader: Upgrader
let transport: Transport

before(async () => {
beforeEach(async () => {
const privateKey = await generateKeyPair('Ed25519')

components = {
Expand All @@ -38,22 +41,29 @@ describe('WebRTCDirect Transport', () => {
transportManager: stubInterface<TransportManager>(),
privateKey
}

upgrader = stubInterface<Upgrader>()
transport = new WebRTCDirectTransport(components)
listener = transport.createListener({
upgrader
})
})

afterEach(async () => {
await listener?.close()
})

it('can construct', () => {
const t = new WebRTCDirectTransport(components)
expect(t.constructor.name).to.equal('WebRTCDirectTransport')
expect(transport.constructor.name).to.equal('WebRTCDirectTransport')
})

it('toString property getter', () => {
const t = new WebRTCDirectTransport(components)
const s = t[Symbol.toStringTag]
const s = transport[Symbol.toStringTag]
expect(s).to.equal('@libp2p/webrtc-direct')
})

it('symbol property getter', () => {
const t = new WebRTCDirectTransport(components)
const s = t[transportSymbol]
const s = transport[transportSymbol]
expect(s).to.equal(true)
})

Expand All @@ -67,9 +77,7 @@ describe('WebRTCDirect Transport', () => {
multiaddr('/ip4/1.2.3.4/udp/1234/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd')
]

const t = new WebRTCDirectTransport(components)

expect(t.listenFilter([
expect(transport.listenFilter([
...valid,
...invalid
])).to.deep.equal(valid)
Expand All @@ -80,12 +88,6 @@ describe('WebRTCDirect Transport', () => {
return this.skip()
}

const upgrader = stubInterface<Upgrader>()
const transport = new WebRTCDirectTransport(components)
const listener = transport.createListener({
upgrader
})

const ipv4 = multiaddr('/ip4/127.0.0.1/udp/37287')
const ipv6 = multiaddr('/ip6/::1/udp/37287')

Expand All @@ -102,12 +104,6 @@ describe('WebRTCDirect Transport', () => {
return this.skip()
}

const upgrader = stubInterface<Upgrader>()
const transport = new WebRTCDirectTransport(components)
const listener = transport.createListener({
upgrader
})

const ipv4 = multiaddr('/ip4/127.0.0.1/udp/0')
const ipv6 = multiaddr('/ip6/::1/udp/0')

Expand All @@ -121,17 +117,11 @@ describe('WebRTCDirect Transport', () => {
await listener.close()
})

it('can listen wildcard hosts', async function () {
it('can listen on wildcard hosts', async function () {
if ((!isNode && !isElectronMain) || !supportsIpV6()) {
return this.skip()
}

const upgrader = stubInterface<Upgrader>()
const transport = new WebRTCDirectTransport(components)
const listener = transport.createListener({
upgrader
})

const ipv4 = multiaddr('/ip4/0.0.0.0/udp/0')
const ipv6 = multiaddr('/ip6/::/udp/0')

Expand Down Expand Up @@ -162,4 +152,22 @@ describe('WebRTCDirect Transport', () => {

await listener.close()
})

it('can listen on multiple wildcard ports', async function () {
if ((!isNode && !isElectronMain) || !supportsIpV6()) {
return this.skip()
}

const ipv4a = multiaddr('/ip4/127.0.0.1/udp/0')
const ipv4b = multiaddr('/ip4/127.0.0.1/udp/0')

await Promise.all([
listener.listen(ipv4a),
listener.listen(ipv4b)
])

const addrs = listener.getAddrs()
expect(addrs).to.have.lengthOf(2)
expect(addrs[0].toOptions().port).to.not.equal(addrs[1].toOptions().port, 'wildcard port listeners with the same ip family should not share ports')
})
})

0 comments on commit 20e8844

Please sign in to comment.