@libp2p/webrtc 5.1.1 → 5.2.0-2b49a5f74

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.
package/src/index.ts CHANGED
@@ -33,7 +33,6 @@
33
33
  * import { identify } from '@libp2p/identify'
34
34
  * import { webRTC } from '@libp2p/webrtc'
35
35
  * import { webSockets } from '@libp2p/websockets'
36
- * import * as filters from '@libp2p/websockets/filters'
37
36
  * import { WebRTC } from '@multiformats/multiaddr-matcher'
38
37
  * import delay from 'delay'
39
38
  * import { pipe } from 'it-pipe'
@@ -47,10 +46,13 @@
47
46
  * listen: ['/ip4/127.0.0.1/tcp/0/ws']
48
47
  * },
49
48
  * transports: [
50
- * webSockets({filter: filters.all})
49
+ * webSockets()
51
50
  * ],
52
51
  * connectionEncrypters: [noise()],
53
52
  * streamMuxers: [yamux()],
53
+ * connectionGater: {
54
+ * denyDialMultiaddr: () => false
55
+ * },
54
56
  * services: {
55
57
  * identify: identify(),
56
58
  * relay: circuitRelayServer()
@@ -68,12 +70,15 @@
68
70
  * ]
69
71
  * },
70
72
  * transports: [
71
- * webSockets({filter: filters.all}),
73
+ * webSockets(),
72
74
  * webRTC(),
73
75
  * circuitRelayTransport()
74
76
  * ],
75
77
  * connectionEncrypters: [noise()],
76
78
  * streamMuxers: [yamux()],
79
+ * connectionGater: {
80
+ * denyDialMultiaddr: () => false
81
+ * },
77
82
  * services: {
78
83
  * identify: identify(),
79
84
  * echo: echo()
@@ -105,12 +110,15 @@
105
110
  * // direct WebRTC connection
106
111
  * const dialer = await createLibp2p({
107
112
  * transports: [
108
- * webSockets({filter: filters.all}),
113
+ * webSockets(),
109
114
  * webRTC(),
110
115
  * circuitRelayTransport()
111
116
  * ],
112
117
  * connectionEncrypters: [noise()],
113
118
  * streamMuxers: [yamux()],
119
+ * connectionGater: {
120
+ * denyDialMultiaddr: () => false
121
+ * },
114
122
  * services: {
115
123
  * identify: identify(),
116
124
  * echo: echo()
@@ -139,9 +147,22 @@
139
147
  *
140
148
  * @example WebRTC Direct
141
149
  *
142
- * At the time of writing WebRTC Direct is dial-only in browsers and unsupported in Node.js.
150
+ * WebRTC Direct allows a client to establish a WebRTC connection to a server
151
+ * without using a relay to first exchange SDP messages.
152
+ *
153
+ * Instead the server listens on a public UDP port and embeds its certificate
154
+ * hash in the published multiaddr. It derives the client's SDP offer based on
155
+ * the incoming IP/port of STUN messages sent to this public port.
156
+ *
157
+ * The client derives the server's SDP answer based on the information in the
158
+ * multiaddr so no SDP handshake via a third party is required.
159
+ *
160
+ * Full details of the connection protocol can be found in the [WebRTC Direct spec](https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md).
161
+ *
162
+ * Browsers cannot listen on WebRTC Direct addresses since they cannot open
163
+ * ports, but they can dial all spec-compliant servers.
143
164
  *
144
- * The only implementation that supports a WebRTC Direct listener is go-libp2p and it's not yet enabled by default.
165
+ * Node.js/go and rust-libp2p can listen on and dial WebRTC Direct addresses.
145
166
  *
146
167
  * ```TypeScript
147
168
  * import { createLibp2p } from 'libp2p'
@@ -36,7 +36,7 @@ export class WebRTCPeerListener extends TypedEventEmitter<ListenerEvents> implem
36
36
  getAddrs (): Multiaddr[] {
37
37
  return this.transportManager
38
38
  .getListeners()
39
- .filter(l => l !== this)
39
+ .filter(l => !(l instanceof WebRTCPeerListener))
40
40
  .map(l => l.getAddrs()
41
41
  .filter(ma => Circuit.exactMatch(ma))
42
42
  .map(ma => {
@@ -46,6 +46,10 @@ export class WebRTCPeerListener extends TypedEventEmitter<ListenerEvents> implem
46
46
  .flat()
47
47
  }
48
48
 
49
+ updateAnnounceAddrs (): void {
50
+
51
+ }
52
+
49
53
  async close (): Promise<void> {
50
54
  this.shutdownController.abort()
51
55
  queueMicrotask(() => {
@@ -22,6 +22,10 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
22
22
  return []
23
23
  }
24
24
 
25
+ updateAnnounceAddrs (): void {
26
+
27
+ }
28
+
25
29
  async close (): Promise<void> {
26
30
 
27
31
  }
@@ -1,8 +1,8 @@
1
1
  import { networkInterfaces } from 'node:os'
2
2
  import { isIPv4, isIPv6 } from '@chainsafe/is-ip'
3
- import { TypedEventEmitter } from '@libp2p/interface'
4
- import { multiaddr, protocols } from '@multiformats/multiaddr'
5
- import { IP4 } from '@multiformats/multiaddr-matcher'
3
+ import { InvalidPeerIdError, TypedEventEmitter } from '@libp2p/interface'
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,18 @@ 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
+ owner: WebRTCDirectListener
58
+ peerId: PeerId
59
+ }
60
+
61
+ let UDP_MUX_LISTENERS: UDPMuxServer[] = []
62
+
50
63
  export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> implements Listener {
51
- private server?: StunServer
52
64
  private readonly multiaddrs: Multiaddr[]
53
65
  private certificate?: TransportCertificate
54
66
  private readonly connections: Map<string, DirectRTCPeerConnection>
@@ -89,7 +101,7 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
89
101
  if (host == null) {
90
102
  throw new Error('IP4/6 host must be specified in webrtc-direct mulitaddr')
91
103
  }
92
- let port = parseInt(parts
104
+ const port = parseInt(parts
93
105
  .filter(([code, value]) => code === UDP_PROTOCOL.code)
94
106
  .pop()?.[1] ?? '')
95
107
 
@@ -97,41 +109,90 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
97
109
  throw new Error('UDP port must be specified in webrtc-direct mulitaddr')
98
110
  }
99
111
 
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()
112
+ // have to do this before any async work happens so starting two listeners
113
+ // for the same port concurrently (e.g. ipv4/ipv6 both port 0) results in a
114
+ // single mux listener. This is necessary because libjuice binds to all
115
+ // interfaces for a given port so we we need to key on just the port number
116
+ // not the host + the port number
117
+ let existingServer = UDP_MUX_LISTENERS.find(s => s.port === port)
118
+
119
+ // if the server has not been started yet, or the port is a wildcard port
120
+ // and there is already a wildcard port for this address family, start a new
121
+ // UDP mux server
122
+ const wildcardPorts = port === 0 && existingServer?.port === 0
123
+ const sameAddressFamily = (existingServer?.isIPv4 === true && isIPv4(host)) || (existingServer?.isIPv6 === true && isIPv6(host))
124
+ let createdMuxServer = false
125
+
126
+ if (existingServer == null || (wildcardPorts && sameAddressFamily)) {
127
+ this.log('starting UDP mux server on %s:%p', host, port)
128
+ existingServer = this.startUDPMuxServer(host, port)
129
+ UDP_MUX_LISTENERS.push(existingServer)
130
+ createdMuxServer = true
104
131
  }
105
132
 
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
133
+ if (!existingServer.peerId.equals(this.components.peerId)) {
134
+ // this would have to be another in-process peer so we are likely in a
135
+ // testing environment
136
+ throw new InvalidPeerIdError(`Another peer is already performing UDP mux on ${host}:${existingServer.port}`)
137
+ }
114
138
 
115
- if (certificate == null) {
116
- const keyPair = await crypto.subtle.generateKey({
117
- name: 'ECDSA',
118
- namedCurve: 'P-256'
119
- }, true, ['sign', 'verify'])
139
+ const server = await existingServer.server
140
+ const address = server.address()
120
141
 
121
- certificate = this.certificate = await generateTransportCertificate(keyPair, {
122
- days: 365
123
- })
142
+ if (!createdMuxServer) {
143
+ this.log('reused existing UDP mux server on %s:%p', host, address.port)
124
144
  }
125
145
 
126
- const address = this.server.address()
127
-
128
- getNetworkAddresses(address.address, address.port, ipVersion).forEach((ma) => {
129
- this.multiaddrs.push(multiaddr(`${ma}/webrtc-direct/certhash/${certificate.certhash}`))
146
+ getNetworkAddresses(host, address.port, ipVersion).forEach((ma) => {
147
+ this.multiaddrs.push(multiaddr(`${ma}/webrtc-direct/certhash/${this.certificate?.certhash}`))
130
148
  })
131
149
 
132
150
  this.safeDispatchEvent('listening')
133
151
  }
134
152
 
153
+ private startUDPMuxServer (host: string, port: number): UDPMuxServer {
154
+ return {
155
+ peerId: this.components.peerId,
156
+ owner: this,
157
+ port,
158
+ isIPv4: isIPv4(host),
159
+ isIPv6: isIPv6(host),
160
+ server: Promise.resolve()
161
+ .then(async (): Promise<StunServer> => {
162
+ // ensure we have a certificate
163
+ if (this.certificate == null) {
164
+ this.log.trace('creating TLS certificate')
165
+ const keyPair = await crypto.subtle.generateKey({
166
+ name: 'ECDSA',
167
+ namedCurve: 'P-256'
168
+ }, true, ['sign', 'verify'])
169
+
170
+ const certificate = await generateTransportCertificate(keyPair, {
171
+ days: 365 * 10
172
+ })
173
+
174
+ if (this.certificate == null) {
175
+ this.certificate = certificate
176
+ }
177
+ }
178
+
179
+ if (port === 0) {
180
+ // libjuice doesn't map 0 to a random free port so we have to do it
181
+ // ourselves
182
+ this.log.trace('searching for free port')
183
+ port = await getPort()
184
+ }
185
+
186
+ return stunListener(host, port, this.log, (ufrag, remoteHost, remotePort) => {
187
+ this.incomingConnection(ufrag, remoteHost, remotePort)
188
+ .catch(err => {
189
+ this.log.error('error processing incoming STUN request', err)
190
+ })
191
+ })
192
+ })
193
+ }
194
+ }
195
+
135
196
  private async incomingConnection (ufrag: string, remoteHost: string, remotePort: number): Promise<void> {
136
197
  const key = `${remoteHost}:${remotePort}:${ufrag}`
137
198
  let peerConnection = this.connections.get(key)
@@ -184,12 +245,51 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
184
245
  return this.multiaddrs
185
246
  }
186
247
 
248
+ updateAnnounceAddrs (multiaddrs: Multiaddr[]): void {
249
+ for (let i = 0; i < multiaddrs.length; i++) {
250
+ let ma = multiaddrs[i]
251
+
252
+ if (!WebRTCDirect.exactMatch(ma)) {
253
+ continue
254
+ }
255
+
256
+ // add the certhash if it is missing
257
+ const tuples = ma.stringTuples()
258
+
259
+ for (let j = 0; j < tuples.length; j++) {
260
+ if (tuples[j][0] !== CODEC_WEBRTC_DIRECT) {
261
+ continue
262
+ }
263
+
264
+ const certhashIndex = j + 1
265
+
266
+ if (tuples[certhashIndex] == null || tuples[certhashIndex][0] !== CODEC_CERTHASH) {
267
+ tuples.splice(certhashIndex, 0, [CODEC_CERTHASH, this.certificate?.certhash])
268
+
269
+ ma = fromStringTuples(tuples)
270
+ multiaddrs[i] = ma
271
+ }
272
+ }
273
+ }
274
+ }
275
+
187
276
  async close (): Promise<void> {
188
277
  for (const connection of this.connections.values()) {
189
278
  connection.close()
190
279
  }
191
280
 
192
- await this.server?.close()
281
+ // stop our UDP mux listeners
282
+ await Promise.all(
283
+ UDP_MUX_LISTENERS
284
+ .filter(listener => listener.owner === this)
285
+ .map(async listener => {
286
+ const server = await listener.server
287
+ await server.close()
288
+ })
289
+ )
290
+
291
+ // remove our stopped UDP mux listeners
292
+ UDP_MUX_LISTENERS = UDP_MUX_LISTENERS.filter(listener => listener.owner !== this)
193
293
 
194
294
  // RTCPeerConnections will be removed from the connections map when their
195
295
  // connection state changes to 'closed'/'disconnected'/'failed
@@ -202,7 +302,7 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
202
302
  }
203
303
 
204
304
  function getNetworkAddresses (host: string, port: number, version: 4 | 6): string[] {
205
- if (host === '0.0.0.0' || host === '::1') {
305
+ if (host === '0.0.0.0' || host === '::') {
206
306
  // return all ip4 interfaces
207
307
  return Object.entries(networkInterfaces())
208
308
  .flatMap(([_, addresses]) => addresses)
@@ -1,14 +0,0 @@
1
- {
2
- "DataChannelOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webrtc.DataChannelOptions.html",
3
- ".:DataChannelOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webrtc.DataChannelOptions.html",
4
- "TransportCertificate": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webrtc.TransportCertificate.html",
5
- ".:TransportCertificate": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webrtc.TransportCertificate.html",
6
- "WebRTCDirectTransportComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webrtc.WebRTCDirectTransportComponents.html",
7
- "WebRTCTransportComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webrtc.WebRTCTransportComponents.html",
8
- "WebRTCTransportDirectInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webrtc.WebRTCTransportDirectInit.html",
9
- "WebRTCTransportInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_webrtc.WebRTCTransportInit.html",
10
- "webRTC": "https://libp2p.github.io/js-libp2p/functions/_libp2p_webrtc.webRTC.html",
11
- ".:webRTC": "https://libp2p.github.io/js-libp2p/functions/_libp2p_webrtc.webRTC.html",
12
- "webRTCDirect": "https://libp2p.github.io/js-libp2p/functions/_libp2p_webrtc.webRTCDirect.html",
13
- ".:webRTCDirect": "https://libp2p.github.io/js-libp2p/functions/_libp2p_webrtc.webRTCDirect.html"
14
- }