@libp2p/webrtc 6.0.27-e86b39e5d → 6.0.28

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.
Files changed (55) hide show
  1. package/dist/index.min.js +6 -6
  2. package/dist/index.min.js.map +3 -3
  3. package/dist/src/constants.d.ts +28 -0
  4. package/dist/src/constants.d.ts.map +1 -1
  5. package/dist/src/constants.js +28 -0
  6. package/dist/src/constants.js.map +1 -1
  7. package/dist/src/muxer.d.ts +36 -4
  8. package/dist/src/muxer.d.ts.map +1 -1
  9. package/dist/src/muxer.js +115 -8
  10. package/dist/src/muxer.js.map +1 -1
  11. package/dist/src/private-to-private/initiate-connection.d.ts +2 -1
  12. package/dist/src/private-to-private/initiate-connection.d.ts.map +1 -1
  13. package/dist/src/private-to-private/initiate-connection.js +6 -2
  14. package/dist/src/private-to-private/initiate-connection.js.map +1 -1
  15. package/dist/src/private-to-private/transport.d.ts +7 -0
  16. package/dist/src/private-to-private/transport.d.ts.map +1 -1
  17. package/dist/src/private-to-private/transport.js +25 -9
  18. package/dist/src/private-to-private/transport.js.map +1 -1
  19. package/dist/src/private-to-public/listener.d.ts +1 -0
  20. package/dist/src/private-to-public/listener.d.ts.map +1 -1
  21. package/dist/src/private-to-public/listener.js +3 -1
  22. package/dist/src/private-to-public/listener.js.map +1 -1
  23. package/dist/src/private-to-public/transport.browser.d.ts +7 -0
  24. package/dist/src/private-to-public/transport.browser.d.ts.map +1 -1
  25. package/dist/src/private-to-public/transport.browser.js +4 -1
  26. package/dist/src/private-to-public/transport.browser.js.map +1 -1
  27. package/dist/src/private-to-public/utils/connect.js +2 -0
  28. package/dist/src/private-to-public/utils/connect.js.map +1 -1
  29. package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.d.ts.map +1 -1
  30. package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js +3 -1
  31. package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js.map +1 -1
  32. package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts +4 -1
  33. package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts.map +1 -1
  34. package/dist/src/private-to-public/utils/get-rtcpeerconnection.js +21 -13
  35. package/dist/src/private-to-public/utils/get-rtcpeerconnection.js.map +1 -1
  36. package/dist/src/private-to-public/utils/stun-listener.d.ts.map +1 -1
  37. package/dist/src/private-to-public/utils/stun-listener.js +2 -5
  38. package/dist/src/private-to-public/utils/stun-listener.js.map +1 -1
  39. package/dist/src/util.d.ts +4 -2
  40. package/dist/src/util.d.ts.map +1 -1
  41. package/dist/src/util.js +21 -0
  42. package/dist/src/util.js.map +1 -1
  43. package/dist/typedoc-urls.json +14 -0
  44. package/package.json +8 -8
  45. package/src/constants.ts +31 -0
  46. package/src/muxer.ts +160 -13
  47. package/src/private-to-private/initiate-connection.ts +7 -2
  48. package/src/private-to-private/transport.ts +33 -9
  49. package/src/private-to-public/listener.ts +4 -1
  50. package/src/private-to-public/transport.browser.ts +12 -1
  51. package/src/private-to-public/utils/connect.ts +2 -0
  52. package/src/private-to-public/utils/get-rtcpeerconnection.browser.ts +3 -1
  53. package/src/private-to-public/utils/get-rtcpeerconnection.ts +27 -14
  54. package/src/private-to-public/utils/stun-listener.ts +2 -7
  55. package/src/util.ts +29 -2
package/src/muxer.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { AbstractStreamMuxer } from '@libp2p/utils'
2
- import { MUXER_PROTOCOL } from './constants.ts'
2
+ import { DEFAULT_MAX_EARLY_STREAMS, MAX_EARLY_DATA_CHANNEL_BYTES, MAX_EARLY_DATA_CHANNEL_MESSAGES, MUXER_PROTOCOL } from './constants.ts'
3
3
  import { createStream, WebRTCStream } from './stream.ts'
4
4
  import type { DataChannelOptions } from './index.ts'
5
- import type { ComponentLogger, CounterGroup, StreamMuxer, StreamMuxerFactory, CreateStreamOptions, MultiaddrConnection } from '@libp2p/interface'
5
+ import type { ComponentLogger, CounterGroup, Logger, StreamMuxer, StreamMuxerFactory, CreateStreamOptions, MultiaddrConnection } from '@libp2p/interface'
6
6
 
7
7
  export interface DataChannelMuxerFactoryInit {
8
8
  /**
@@ -20,6 +20,18 @@ export interface DataChannelMuxerFactoryInit {
20
20
  */
21
21
  metrics?: CounterGroup
22
22
 
23
+ /**
24
+ * Optional logger, used to report early data channels that are dropped or
25
+ * rejected, and best-effort close errors
26
+ */
27
+ log?: Logger
28
+
29
+ /**
30
+ * Caps the early data channel buffer and the muxer's early streams from one
31
+ * value (see `DEFAULT_MAX_EARLY_STREAMS`)
32
+ */
33
+ maxEarlyStreams?: number
34
+
23
35
  /**
24
36
  * Options used to create data channels
25
37
  */
@@ -30,6 +42,41 @@ export interface DataChannelMuxerFactoryComponents {
30
42
  logger: ComponentLogger
31
43
  }
32
44
 
45
+ interface EarlyDataChannel {
46
+ channel: RTCDataChannel
47
+
48
+ /**
49
+ * Messages buffered before the muxer adopted the channel; without these they
50
+ * would be dropped, since the stream's `message` listener is only attached
51
+ * when the muxer is created
52
+ */
53
+ messages: Array<MessageEvent<ArrayBuffer>>
54
+ }
55
+
56
+ /**
57
+ * Close a data channel. Closing is best-effort and wrapped so a synchronous
58
+ * throw from an already-closed or stale channel cannot escape and abort
59
+ * disposal of the remaining early channels.
60
+ */
61
+ function closeChannel (channel: RTCDataChannel, log?: Logger): void {
62
+ try {
63
+ channel.close()
64
+ } catch (err) {
65
+ log?.trace('error closing early data channel - %e', err)
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Detach the buffering handler, release the buffered messages and close the
71
+ * channel. Used both when disposing a single over-limit channel and when
72
+ * discarding the whole early buffer.
73
+ */
74
+ function disposeEarlyDataChannel (early: EarlyDataChannel, log?: Logger): void {
75
+ early.channel.onmessage = null
76
+ early.messages.length = 0
77
+ closeChannel(early.channel, log)
78
+ }
79
+
33
80
  export class DataChannelMuxerFactory implements StreamMuxerFactory {
34
81
  public readonly protocol: string
35
82
 
@@ -38,25 +85,90 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory {
38
85
  */
39
86
  private readonly peerConnection: RTCPeerConnection
40
87
  private readonly metrics?: CounterGroup
88
+ private readonly log?: Logger
41
89
  private readonly dataChannelOptions?: DataChannelOptions
42
- private readonly earlyDataChannels: RTCDataChannel[]
90
+ private readonly maxEarlyStreams: number
91
+ private readonly earlyDataChannels: EarlyDataChannel[]
92
+ private handedOff = false
43
93
 
44
94
  constructor (init: DataChannelMuxerFactoryInit) {
45
95
  this.onEarlyDataChannel = this.onEarlyDataChannel.bind(this)
46
96
 
47
97
  this.peerConnection = init.peerConnection
48
98
  this.metrics = init.metrics
99
+ this.log = init.log
49
100
  this.protocol = init.protocol ?? MUXER_PROTOCOL
50
101
  this.dataChannelOptions = init.dataChannelOptions ?? {}
102
+ this.maxEarlyStreams = init.maxEarlyStreams ?? DEFAULT_MAX_EARLY_STREAMS
51
103
  this.peerConnection.addEventListener('datachannel', this.onEarlyDataChannel)
52
104
  this.earlyDataChannels = []
53
105
  }
54
106
 
55
107
  private onEarlyDataChannel (evt: RTCDataChannelEvent): void {
56
- this.earlyDataChannels.push(evt.channel)
108
+ const channel = evt.channel
109
+
110
+ // reject (don't buffer) channels beyond the count cap, keeping the connection
111
+ if (this.earlyDataChannels.length >= this.maxEarlyStreams) {
112
+ this.log?.('rejecting early data channel %d - too many early channels', channel.id)
113
+ this.metrics?.increment({ early_data_channel_count_exceeded: true })
114
+ closeChannel(channel, this.log)
115
+ return
116
+ }
117
+
118
+ // deliver binary as ArrayBuffer so it can be sized and replayed to the stream
119
+ channel.binaryType = 'arraybuffer'
120
+
121
+ const early: EarlyDataChannel = {
122
+ channel,
123
+ messages: []
124
+ }
125
+
126
+ // buffer until the muxer adopts the channel, bounded so a remote cannot hold
127
+ // unbounded data pre-admission. Must stay on `.onmessage` so `createStream`
128
+ // can overwrite it on adoption (see stream.ts)
129
+ channel.onmessage = (messageEvent) => {
130
+ const { data } = messageEvent
131
+
132
+ // text frames arrive as strings despite binaryType - can't size them, reject
133
+ if (!(data instanceof ArrayBuffer)) {
134
+ this.closeEarlyDataChannel(early, 'invalid_message')
135
+ return
136
+ }
137
+
138
+ // cap count too, else a flood of ~0-byte messages evades the byte cap
139
+ if (early.messages.length >= MAX_EARLY_DATA_CHANNEL_MESSAGES) {
140
+ this.closeEarlyDataChannel(early, 'message_count_exceeded')
141
+ return
142
+ }
143
+
144
+ const buffered = early.messages.reduce((total, m) => total + m.data.byteLength, 0)
145
+ if (buffered + data.byteLength > MAX_EARLY_DATA_CHANNEL_BYTES) {
146
+ this.closeEarlyDataChannel(early, 'byte_count_exceeded')
147
+ return
148
+ }
149
+
150
+ early.messages.push(messageEvent)
151
+ }
152
+
153
+ this.earlyDataChannels.push(early)
154
+ }
155
+
156
+ private closeEarlyDataChannel (early: EarlyDataChannel, reason: string): void {
157
+ this.log?.('closing early data channel %d - %s', early.channel.id, reason)
158
+ this.metrics?.increment({ [`early_data_channel_${reason}`]: true })
159
+
160
+ disposeEarlyDataChannel(early, this.log)
161
+
162
+ const index = this.earlyDataChannels.indexOf(early)
163
+ if (index !== -1) {
164
+ this.earlyDataChannels.splice(index, 1)
165
+ }
57
166
  }
58
167
 
59
168
  createStreamMuxer (maConn: MultiaddrConnection): StreamMuxer {
169
+ // ownership of the buffered early channels transfers to the muxer, so a
170
+ // later `close()` from the transport must not dispose them
171
+ this.handedOff = true
60
172
  this.peerConnection.removeEventListener('datachannel', this.onEarlyDataChannel)
61
173
 
62
174
  return new DataChannelMuxer(maConn, {
@@ -64,19 +176,41 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory {
64
176
  dataChannelOptions: this.dataChannelOptions,
65
177
  metrics: this.metrics,
66
178
  protocol: this.protocol,
179
+ maxEarlyStreams: this.maxEarlyStreams,
67
180
  earlyDataChannels: this.earlyDataChannels
68
181
  })
69
182
  }
183
+
184
+ /**
185
+ * Discards any early data channels buffered before the muxer was created and
186
+ * detaches the `datachannel` listener. Called by the transport whenever
187
+ * connection establishment fails; it is a no-op once `createStreamMuxer` has
188
+ * handed the channels to the muxer, and otherwise ensures a peer whose
189
+ * connection is rejected cannot leave buffered data or listeners behind.
190
+ */
191
+ close (): void {
192
+ if (this.handedOff) {
193
+ return
194
+ }
195
+
196
+ this.peerConnection.removeEventListener('datachannel', this.onEarlyDataChannel)
197
+
198
+ for (const early of this.earlyDataChannels) {
199
+ disposeEarlyDataChannel(early, this.log)
200
+ }
201
+
202
+ this.earlyDataChannels.length = 0
203
+ }
70
204
  }
71
205
 
72
206
  export interface DataChannelMuxerInit extends DataChannelMuxerFactoryInit {
73
207
  protocol: string
74
208
 
75
209
  /**
76
- * Incoming data channels that were opened by the remote before the peer
77
- * connection was established
210
+ * Incoming data channels opened by the remote before the muxer was created,
211
+ * along with the messages that arrived on them in that window
78
212
  */
79
- earlyDataChannels: RTCDataChannel[]
213
+ earlyDataChannels: EarlyDataChannel[]
80
214
  }
81
215
 
82
216
  export interface DataChannelMuxerComponents {
@@ -112,26 +246,32 @@ export class DataChannelMuxer extends AbstractStreamMuxer<WebRTCStream> implemen
112
246
 
113
247
  queueMicrotask(() => {
114
248
  if (this.status !== 'open') {
115
- init.earlyDataChannels.forEach(channel => {
116
- channel.close()
249
+ // the connection was torn down before we could adopt these channels -
250
+ // detach the buffer, release it, and close the channel
251
+ init.earlyDataChannels.forEach((early) => {
252
+ disposeEarlyDataChannel(early, this.log)
117
253
  })
118
254
  return
119
255
  }
120
256
 
121
- init.earlyDataChannels.forEach(channel => {
122
- this.onDataChannel(channel)
257
+ init.earlyDataChannels.forEach(({ channel, messages }) => {
258
+ try {
259
+ this.onDataChannel(channel, messages)
260
+ } catch (err) {
261
+ this.log.error('error adopting early data channel %d - %e', channel.id, err)
262
+ }
123
263
  })
124
264
  })
125
265
  }
126
266
 
127
- private onDataChannel (channel: RTCDataChannel): void {
267
+ private onDataChannel (channel: RTCDataChannel, earlyMessages?: MessageEvent[]): void {
128
268
  this.log('incoming datachannel with channel id %d, protocol %s and status %s', channel.id, channel.protocol, channel.readyState)
129
269
 
130
270
  // 'init' channel is only used during connection establishment, it is
131
271
  // closed by the initiator
132
272
  if (channel.label === 'init') {
133
273
  this.log.trace('closing init channel %d', channel.id)
134
- channel.close()
274
+ closeChannel(channel, this.log)
135
275
 
136
276
  return
137
277
  }
@@ -144,6 +284,13 @@ export class DataChannelMuxer extends AbstractStreamMuxer<WebRTCStream> implemen
144
284
  log: this.log
145
285
  })
146
286
 
287
+ // replay any messages that arrived before the muxer was created - the
288
+ // stream has just attached its `message` handler so this preserves
289
+ // ordering with any messages that arrive later
290
+ earlyMessages?.forEach(messageEvent => {
291
+ channel.onmessage?.(messageEvent)
292
+ })
293
+
147
294
  this.onRemoteStream(stream)
148
295
  }
149
296
 
@@ -23,6 +23,7 @@ export interface IncomingStreamOptions extends AbortOptions {
23
23
  export interface ConnectOptions extends LoggerOptions, ProgressOptions<WebRTCDialEvents> {
24
24
  rtcConfiguration?: RTCConfiguration
25
25
  dataChannel?: DataChannelOptions
26
+ maxEarlyStreams?: number
26
27
  multiaddr: Multiaddr
27
28
  connectionManager: ConnectionManager
28
29
  transportManager: TransportManager
@@ -32,7 +33,7 @@ export interface ConnectOptions extends LoggerOptions, ProgressOptions<WebRTCDia
32
33
  logger: ComponentLogger
33
34
  }
34
35
 
35
- export async function initiateConnection ({ rtcConfiguration, dataChannel, signal, metrics, multiaddr: ma, connectionManager, transportManager, log, logger, onProgress }: ConnectOptions): Promise<{ remoteAddress: Multiaddr, peerConnection: globalThis.RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> {
36
+ export async function initiateConnection ({ rtcConfiguration, dataChannel, maxEarlyStreams, signal, metrics, multiaddr: ma, connectionManager, transportManager, log, logger, onProgress }: ConnectOptions): Promise<{ remoteAddress: Multiaddr, peerConnection: globalThis.RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> {
36
37
  const { circuitAddress, targetPeer } = splitAddr(ma)
37
38
 
38
39
  metrics?.dialerEvents.increment({ open: true })
@@ -83,7 +84,10 @@ export async function initiateConnection ({ rtcConfiguration, dataChannel, signa
83
84
  const muxerFactory = new DataChannelMuxerFactory({
84
85
  // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
85
86
  peerConnection,
86
- dataChannelOptions: dataChannel
87
+ dataChannelOptions: dataChannel,
88
+ maxEarlyStreams,
89
+ metrics: metrics?.dialerEvents,
90
+ log
87
91
  })
88
92
 
89
93
  try {
@@ -216,6 +220,7 @@ export async function initiateConnection ({ rtcConfiguration, dataChannel, signa
216
220
  } catch (err: any) {
217
221
  log.error('outgoing signaling error - %e', err)
218
222
 
223
+ muxerFactory.close()
219
224
  peerConnection.close()
220
225
  stream.abort(err)
221
226
  throw err
@@ -31,6 +31,14 @@ export interface WebRTCTransportInit {
31
31
  * Any options here will be applied to any RTCDataChannels that are opened.
32
32
  */
33
33
  dataChannel?: DataChannelOptions
34
+
35
+ /**
36
+ * Caps the inbound streams accepted before the connection surfaces them, and
37
+ * the data channels buffered before the muxer exists (excess are closed).
38
+ *
39
+ * @default 10
40
+ */
41
+ maxEarlyStreams?: number
34
42
  }
35
43
 
36
44
  export interface WebRTCTransportComponents {
@@ -163,11 +171,13 @@ export class WebRTCTransport implements Transport<WebRTCDialEvents>, Startable {
163
171
  const { remoteAddress, peerConnection, muxerFactory } = await initiateConnection({
164
172
  rtcConfiguration: await getRtcConfiguration(this.init.rtcConfiguration),
165
173
  dataChannel: this.init.dataChannel,
174
+ maxEarlyStreams: this.init.maxEarlyStreams,
166
175
  multiaddr: ma,
167
176
  dataChannelOptions: this.init.dataChannel,
168
177
  signal: options.signal,
169
178
  connectionManager: this.components.connectionManager,
170
179
  transportManager: this.components.transportManager,
180
+ metrics: this.metrics,
171
181
  log: this.log,
172
182
  logger: this.components.logger,
173
183
  onProgress: options.onProgress
@@ -181,14 +191,24 @@ export class WebRTCTransport implements Transport<WebRTCDialEvents>, Startable {
181
191
  log: this.components.logger.forComponent('libp2p:webrtc:connection')
182
192
  })
183
193
 
184
- const connection = await options.upgrader.upgradeOutbound(webRTCConn, {
185
- skipProtection: true,
186
- skipEncryption: true,
187
- remotePeer: getRemotePeer(ma),
188
- muxerFactory,
189
- onProgress: options.onProgress,
190
- signal: options.signal
191
- })
194
+ let connection: Connection
195
+
196
+ try {
197
+ connection = await options.upgrader.upgradeOutbound(webRTCConn, {
198
+ skipProtection: true,
199
+ skipEncryption: true,
200
+ remotePeer: getRemotePeer(ma),
201
+ muxerFactory,
202
+ onProgress: options.onProgress,
203
+ signal: options.signal
204
+ })
205
+ } catch (err) {
206
+ // discard any early data channels buffered before the upgrade failed and
207
+ // close the peer connection
208
+ muxerFactory.close()
209
+ peerConnection.close()
210
+ throw err
211
+ }
192
212
 
193
213
  // close the connection on shut down
194
214
  this._closeOnShutdown(peerConnection, webRTCConn)
@@ -213,7 +233,10 @@ export class WebRTCTransport implements Transport<WebRTCDialEvents>, Startable {
213
233
  const muxerFactory = new DataChannelMuxerFactory({
214
234
  // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
215
235
  peerConnection,
216
- dataChannelOptions: this.init.dataChannel
236
+ dataChannelOptions: this.init.dataChannel,
237
+ maxEarlyStreams: this.init.maxEarlyStreams,
238
+ metrics: this.metrics?.listenerEvents,
239
+ log: this.log
217
240
  })
218
241
 
219
242
  try {
@@ -251,6 +274,7 @@ export class WebRTCTransport implements Transport<WebRTCDialEvents>, Startable {
251
274
  } catch (err: any) {
252
275
  this.log.error('incoming signaling error - %e', err)
253
276
 
277
+ muxerFactory.close()
254
278
  peerConnection.close()
255
279
  stream.abort(err)
256
280
  throw err
@@ -34,6 +34,7 @@ export interface WebRTCDirectListenerInit {
34
34
  upgrader: Upgrader
35
35
  certificate: TransportCertificate
36
36
  maxInboundStreams?: number
37
+ maxEarlyStreams?: number
37
38
  dataChannel?: DataChannelOptions
38
39
  rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
39
40
  emitter: TypedEventTarget<WebRTCDirectTransportCertificateEvents>
@@ -189,7 +190,9 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
189
190
  rtcConfiguration: this.init.rtcConfiguration,
190
191
  certificate: this.certificate,
191
192
  events: this.metrics?.listenerEvents,
192
- dataChannel: this.init.dataChannel
193
+ log: this.log,
194
+ dataChannel: this.init.dataChannel,
195
+ maxEarlyStreams: this.init.maxEarlyStreams
193
196
  })
194
197
  peerConnection = results.peerConnection
195
198
 
@@ -39,6 +39,14 @@ export interface WebRTCTransportDirectInit {
39
39
  * The default configuration used by all created RTCDataChannels
40
40
  */
41
41
  dataChannel?: DataChannelOptions
42
+
43
+ /**
44
+ * Caps the inbound streams accepted before the connection surfaces them, and
45
+ * the data channels buffered before the muxer exists (excess are closed).
46
+ *
47
+ * @default 10
48
+ */
49
+ maxEarlyStreams?: number
42
50
  }
43
51
 
44
52
  export class WebRTCDirectTransport implements Transport {
@@ -92,7 +100,10 @@ export class WebRTCDirectTransport implements Transport {
92
100
  muxerFactory
93
101
  } = await createDialerRTCPeerConnection('client', ufrag, {
94
102
  rtcConfiguration: typeof this.init.rtcConfiguration === 'function' ? await this.init.rtcConfiguration() : this.init.rtcConfiguration ?? {},
95
- dataChannel: this.init.dataChannel
103
+ events: this.metrics?.dialerEvents,
104
+ log: this.log,
105
+ dataChannel: this.init.dataChannel,
106
+ maxEarlyStreams: this.init.maxEarlyStreams
96
107
  })
97
108
 
98
109
  try {
@@ -193,6 +193,8 @@ export async function connect (peerConnection: RTCPeerConnection | DirectRTCPeer
193
193
  signal: options.signal
194
194
  })
195
195
  } catch (err) {
196
+ // discard any early data channels buffered before the upgrade failed
197
+ muxerFactory.close()
196
198
  handshakeDataChannel.close()
197
199
  peerConnection.close()
198
200
 
@@ -29,7 +29,9 @@ export async function createDialerRTCPeerConnection (role: 'client' | 'server',
29
29
  const muxerFactory = new DataChannelMuxerFactory({
30
30
  peerConnection,
31
31
  metrics: options.events,
32
- dataChannelOptions: options.dataChannel
32
+ log: options.log,
33
+ dataChannelOptions: options.dataChannel,
34
+ maxEarlyStreams: options.maxEarlyStreams
33
35
  })
34
36
 
35
37
  return {
@@ -1,11 +1,13 @@
1
+ import { InvalidParametersError } from '@libp2p/interface'
1
2
  import { Crypto } from '@peculiar/webcrypto'
2
3
  import { PeerConnection } from 'node-datachannel'
3
4
  import { RTCPeerConnection } from 'node-datachannel/polyfill'
4
5
  import { DEFAULT_ICE_SERVERS, MAX_MESSAGE_SIZE } from '../../constants.ts'
5
6
  import { DataChannelMuxerFactory } from '../../muxer.ts'
7
+ import { isValidUfrag } from '../../util.ts'
6
8
  import { generateTransportCertificate } from './generate-certificates.ts'
7
9
  import type { DataChannelOptions, TransportCertificate } from '../../index.ts'
8
- import type { CounterGroup } from '@libp2p/interface'
10
+ import type { CounterGroup, Logger } from '@libp2p/interface'
9
11
  import type { CertificateFingerprint } from 'node-datachannel'
10
12
 
11
13
  const crypto = new Crypto()
@@ -40,28 +42,35 @@ export class DirectRTCPeerConnection extends RTCPeerConnection {
40
42
 
41
43
  async createOffer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
42
44
  // have to set ufrag before creating offer
43
- if (this.connectionState === 'new') {
44
- this.peerConnection?.setLocalDescription('offer', {
45
- iceUfrag: this.ufrag,
46
- icePwd: this.ufrag
47
- })
48
- }
45
+ this.setLocalUfrag('offer')
49
46
 
50
47
  return super.createOffer()
51
48
  }
52
49
 
53
50
  async createAnswer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
54
51
  // have to set ufrag before creating answer
55
- if (this.connectionState === 'new') {
56
- this.peerConnection?.setLocalDescription('answer', {
57
- iceUfrag: this.ufrag,
58
- icePwd: this.ufrag
59
- })
60
- }
52
+ this.setLocalUfrag('answer')
61
53
 
62
54
  return super.createAnswer()
63
55
  }
64
56
 
57
+ // this.ufrag is reused as the ICE password; an invalid value aborts the process
58
+ // inside the native ICE stack, so reject it with a recoverable error first
59
+ private setLocalUfrag (type: 'offer' | 'answer'): void {
60
+ if (this.connectionState !== 'new') {
61
+ return
62
+ }
63
+
64
+ if (!isValidUfrag(this.ufrag)) {
65
+ throw new InvalidParametersError('ufrag is not a valid ICE credential')
66
+ }
67
+
68
+ this.peerConnection?.setLocalDescription(type, {
69
+ iceUfrag: this.ufrag,
70
+ icePwd: this.ufrag
71
+ })
72
+ }
73
+
65
74
  remoteFingerprint (): CertificateFingerprint {
66
75
  if (this.peerConnection == null) {
67
76
  throw new Error('Invalid state: peer connection not set')
@@ -91,7 +100,9 @@ export interface CreateDialerRTCPeerConnectionOptions {
91
100
  rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
92
101
  certificate?: TransportCertificate
93
102
  events?: CounterGroup
103
+ log?: Logger
94
104
  dataChannel?: DataChannelOptions
105
+ maxEarlyStreams?: number
95
106
  }
96
107
 
97
108
  export async function createDialerRTCPeerConnection (role: 'client', ufrag: string, options?: CreateDialerRTCPeerConnectionOptions): Promise<{ peerConnection: globalThis.RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }>
@@ -132,7 +143,9 @@ export async function createDialerRTCPeerConnection (role: 'client' | 'server',
132
143
  // @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
133
144
  peerConnection,
134
145
  metrics: options.events,
135
- dataChannelOptions: options.dataChannel
146
+ log: options.log,
147
+ dataChannelOptions: options.dataChannel,
148
+ maxEarlyStreams: options.maxEarlyStreams
136
149
  })
137
150
 
138
151
  return {
@@ -1,5 +1,6 @@
1
1
  import { isIPv4 } from '@chainsafe/is-ip'
2
2
  import { IceUdpMuxListener } from 'node-datachannel'
3
+ import { handleStunRequest } from '../../util.ts'
3
4
  import type { Logger } from '@libp2p/interface'
4
5
  import type { AddressInfo } from 'node:net'
5
6
 
@@ -15,13 +16,7 @@ export interface Callback {
15
16
  export async function stunListener (host: string, port: number, log: Logger, cb: Callback): Promise<StunServer> {
16
17
  const listener = new IceUdpMuxListener(port, host)
17
18
  listener.onUnhandledStunRequest(request => {
18
- if (request.ufrag == null) {
19
- return
20
- }
21
-
22
- log.trace('incoming STUN packet from %s:%d %s', request.host, request.port, request.ufrag)
23
-
24
- cb(request.ufrag, request.host, request.port)
19
+ handleStunRequest(request, log, cb)
25
20
  })
26
21
 
27
22
  return {
package/src/util.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import pDefer from 'p-defer'
2
2
  import pTimeout from 'p-timeout'
3
3
  import { DATA_CHANNEL_DRAIN_TIMEOUT, DEFAULT_ICE_SERVERS, UFRAG_ALPHABET, UFRAG_PREFIX } from './constants.ts'
4
- import type { LoggerOptions } from '@libp2p/interface'
4
+ import type { Logger, LoggerOptions } from '@libp2p/interface'
5
5
  import type { Duplex, Source } from 'it-stream-types'
6
- import type { PeerConnection } from 'node-datachannel'
6
+ import type { IceUdpMuxRequest, PeerConnection } from 'node-datachannel'
7
7
 
8
8
  export const nopSource = async function * nop (): AsyncGenerator<Uint8Array, any, unknown> {}
9
9
 
@@ -105,3 +105,30 @@ export async function getRtcConfiguration (config?: RTCConfiguration | (() => RT
105
105
  export const genUfrag = (len: number = 32): string => {
106
106
  return UFRAG_PREFIX + [...Array(len)].map(() => UFRAG_ALPHABET.at(Math.floor(Math.random() * UFRAG_ALPHABET.length))).join('')
107
107
  }
108
+
109
+ // WebRTC-direct reuses the ufrag as the ICE password, which RFC 8445 requires to
110
+ // be 22-256 chars from the ice-char set [a-zA-Z0-9+/]; other values crash the
111
+ // process during native ICE setup, so reject them beforehand.
112
+ const ICE_CREDENTIAL_REGEX = /^[a-zA-Z0-9+/]{22,256}$/
113
+
114
+ export const isValidUfrag = (ufrag: string): boolean => {
115
+ return ICE_CREDENTIAL_REGEX.test(ufrag)
116
+ }
117
+
118
+ // Validate and forward a STUN request received by the UDP mux listener. The ufrag
119
+ // is attacker-controlled and reused as the ICE password in native setup, where an
120
+ // invalid value aborts the process, so drop anything that is not a valid credential.
121
+ export function handleStunRequest (request: IceUdpMuxRequest, log: Logger, cb: (ufrag: string, host: string, port: number) => void): void {
122
+ if (request.ufrag == null) {
123
+ return
124
+ }
125
+
126
+ if (!isValidUfrag(request.ufrag)) {
127
+ log.trace('dropping incoming STUN packet from %s:%d with invalid ufrag', request.host, request.port)
128
+ return
129
+ }
130
+
131
+ log.trace('incoming STUN packet from %s:%d %s', request.host, request.port, request.ufrag)
132
+
133
+ cb(request.ufrag, request.host, request.port)
134
+ }