@libp2p/webrtc 5.1.1 → 5.2.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.
@@ -1,8 +1,8 @@
1
1
  import { networkInterfaces } from 'node:os'
2
2
  import { isIPv4, isIPv6 } from '@chainsafe/is-ip'
3
3
  import { TypedEventEmitter } from '@libp2p/interface'
4
- import { multiaddr, protocols } from '@multiformats/multiaddr'
5
- import { IP4 } from '@multiformats/multiaddr-matcher'
4
+ import { multiaddr, protocols, fromStringTuples } from '@multiformats/multiaddr'
5
+ import { IP4, WebRTCDirect } from '@multiformats/multiaddr-matcher'
6
6
  import { Crypto } from '@peculiar/webcrypto'
7
7
  import getPort from 'get-port'
8
8
  import pWaitFor from 'p-wait-for'
@@ -22,6 +22,8 @@ const crypto = new Crypto()
22
22
  * The time to wait, in milliseconds, for the data channel handshake to complete
23
23
  */
24
24
  const HANDSHAKE_TIMEOUT_MS = 10_000
25
+ const CODEC_WEBRTC_DIRECT = 0x0118
26
+ const CODEC_CERTHASH = 0x01d2
25
27
 
26
28
  export interface WebRTCDirectListenerComponents {
27
29
  peerId: PeerId
@@ -47,8 +49,15 @@ const UDP_PROTOCOL = protocols('udp')
47
49
  const IP4_PROTOCOL = protocols('ip4')
48
50
  const IP6_PROTOCOL = protocols('ip6')
49
51
 
52
+ interface UDPMuxServer {
53
+ server: Promise<StunServer>
54
+ isIPv4: boolean
55
+ isIPv6: boolean
56
+ port: number
57
+ }
58
+
50
59
  export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> implements Listener {
51
- private server?: StunServer
60
+ private readonly servers: UDPMuxServer[]
52
61
  private readonly multiaddrs: Multiaddr[]
53
62
  private certificate?: TransportCertificate
54
63
  private readonly connections: Map<string, DirectRTCPeerConnection>
@@ -63,6 +72,7 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
63
72
  this.init = init
64
73
  this.components = components
65
74
  this.multiaddrs = []
75
+ this.servers = []
66
76
  this.connections = new Map()
67
77
  this.log = components.logger.forComponent('libp2p:webrtc-direct:listener')
68
78
  this.certificate = init.certificates?.[0]
@@ -89,7 +99,7 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
89
99
  if (host == null) {
90
100
  throw new Error('IP4/6 host must be specified in webrtc-direct mulitaddr')
91
101
  }
92
- let port = parseInt(parts
102
+ const port = parseInt(parts
93
103
  .filter(([code, value]) => code === UDP_PROTOCOL.code)
94
104
  .pop()?.[1] ?? '')
95
105
 
@@ -97,41 +107,73 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
97
107
  throw new Error('UDP port must be specified in webrtc-direct mulitaddr')
98
108
  }
99
109
 
100
- if (port === 0 && this.init.useLibjuice !== false) {
101
- // libjuice doesn't map 0 to a random free port so we have to do it
102
- // ourselves
103
- port = await getPort()
104
- }
105
-
106
- this.server = await stunListener(host, port, this.log, (ufrag, remoteHost, remotePort) => {
107
- this.incomingConnection(ufrag, remoteHost, remotePort)
108
- .catch(err => {
109
- this.log.error('error processing incoming STUN request', err)
110
- })
111
- })
112
-
113
- let certificate = this.certificate
114
-
115
- if (certificate == null) {
116
- const keyPair = await crypto.subtle.generateKey({
117
- name: 'ECDSA',
118
- namedCurve: 'P-256'
119
- }, true, ['sign', 'verify'])
120
-
121
- certificate = this.certificate = await generateTransportCertificate(keyPair, {
122
- days: 365
123
- })
110
+ // have to do this before any async work happens so starting two listeners
111
+ // for the same port concurrently (e.g. ipv4/ipv6 both port 0) results in a
112
+ // single mux listener. This is necessary because libjuice binds to all
113
+ // interfaces for a given port so we we need to key on just the port number
114
+ // not the host + the port number
115
+ let existingServer = this.servers.find(s => s.port === port)
116
+
117
+ // if the server has not been started yet, or the port is a wildcard port
118
+ // and there is already a wildcard port for this address family, start a new
119
+ // UDP mux server
120
+ const wildcardPorts = port === 0 && existingServer?.port === 0
121
+ const sameAddressFamily = (existingServer?.isIPv4 === true && isIPv4(host)) || (existingServer?.isIPv6 === true && isIPv6(host))
122
+
123
+ if (existingServer == null || (wildcardPorts && sameAddressFamily)) {
124
+ existingServer = this.startUDPMuxServer(host, port)
125
+ this.servers.push(existingServer)
124
126
  }
125
127
 
126
- const address = this.server.address()
128
+ const server = await existingServer.server
129
+ const address = server.address()
127
130
 
128
- getNetworkAddresses(address.address, address.port, ipVersion).forEach((ma) => {
129
- this.multiaddrs.push(multiaddr(`${ma}/webrtc-direct/certhash/${certificate.certhash}`))
131
+ getNetworkAddresses(host, address.port, ipVersion).forEach((ma) => {
132
+ this.multiaddrs.push(multiaddr(`${ma}/webrtc-direct/certhash/${this.certificate?.certhash}`))
130
133
  })
131
134
 
132
135
  this.safeDispatchEvent('listening')
133
136
  }
134
137
 
138
+ private startUDPMuxServer (host: string, port: number): UDPMuxServer {
139
+ return {
140
+ port,
141
+ isIPv4: isIPv4(host),
142
+ isIPv6: isIPv6(host),
143
+ server: Promise.resolve()
144
+ .then(async (): Promise<StunServer> => {
145
+ if (port === 0) {
146
+ // libjuice doesn't map 0 to a random free port so we have to do it
147
+ // ourselves
148
+ port = await getPort()
149
+ }
150
+
151
+ // ensure we have a certificate
152
+ if (this.certificate == null) {
153
+ const keyPair = await crypto.subtle.generateKey({
154
+ name: 'ECDSA',
155
+ namedCurve: 'P-256'
156
+ }, true, ['sign', 'verify'])
157
+
158
+ const certificate = await generateTransportCertificate(keyPair, {
159
+ days: 365 * 10
160
+ })
161
+
162
+ if (this.certificate == null) {
163
+ this.certificate = certificate
164
+ }
165
+ }
166
+
167
+ return stunListener(host, port, this.log, (ufrag, remoteHost, remotePort) => {
168
+ this.incomingConnection(ufrag, remoteHost, remotePort)
169
+ .catch(err => {
170
+ this.log.error('error processing incoming STUN request', err)
171
+ })
172
+ })
173
+ })
174
+ }
175
+ }
176
+
135
177
  private async incomingConnection (ufrag: string, remoteHost: string, remotePort: number): Promise<void> {
136
178
  const key = `${remoteHost}:${remotePort}:${ufrag}`
137
179
  let peerConnection = this.connections.get(key)
@@ -184,12 +226,46 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
184
226
  return this.multiaddrs
185
227
  }
186
228
 
229
+ updateAnnounceAddrs (multiaddrs: Multiaddr[]): void {
230
+ for (let i = 0; i < multiaddrs.length; i++) {
231
+ let ma = multiaddrs[i]
232
+
233
+ if (!WebRTCDirect.exactMatch(ma)) {
234
+ continue
235
+ }
236
+
237
+ // add the certhash if it is missing
238
+ const tuples = ma.stringTuples()
239
+
240
+ for (let j = 0; j < tuples.length; j++) {
241
+ if (tuples[j][0] !== CODEC_WEBRTC_DIRECT) {
242
+ continue
243
+ }
244
+
245
+ const certhashIndex = j + 1
246
+
247
+ if (tuples[certhashIndex] == null || tuples[certhashIndex][0] !== CODEC_CERTHASH) {
248
+ tuples.splice(certhashIndex, 0, [CODEC_CERTHASH, this.certificate?.certhash])
249
+
250
+ ma = fromStringTuples(tuples)
251
+ multiaddrs[i] = ma
252
+ }
253
+ }
254
+ }
255
+ }
256
+
187
257
  async close (): Promise<void> {
188
258
  for (const connection of this.connections.values()) {
189
259
  connection.close()
190
260
  }
191
261
 
192
- await this.server?.close()
262
+ // stop all UDP mux listeners
263
+ await Promise.all(
264
+ this.servers.map(async p => {
265
+ const server = await p.server
266
+ await server.close()
267
+ })
268
+ )
193
269
 
194
270
  // RTCPeerConnections will be removed from the connections map when their
195
271
  // connection state changes to 'closed'/'disconnected'/'failed
@@ -202,7 +278,7 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
202
278
  }
203
279
 
204
280
  function getNetworkAddresses (host: string, port: number, version: 4 | 6): string[] {
205
- if (host === '0.0.0.0' || host === '::1') {
281
+ if (host === '0.0.0.0' || host === '::') {
206
282
  // return all ip4 interfaces
207
283
  return Object.entries(networkInterfaces())
208
284
  .flatMap(([_, addresses]) => addresses)