@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.
- package/dist/index.min.js +6 -6
- package/dist/index.min.js.map +3 -3
- package/dist/src/constants.d.ts +28 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +28 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/muxer.d.ts +36 -4
- package/dist/src/muxer.d.ts.map +1 -1
- package/dist/src/muxer.js +115 -8
- package/dist/src/muxer.js.map +1 -1
- package/dist/src/private-to-private/initiate-connection.d.ts +2 -1
- package/dist/src/private-to-private/initiate-connection.d.ts.map +1 -1
- package/dist/src/private-to-private/initiate-connection.js +6 -2
- package/dist/src/private-to-private/initiate-connection.js.map +1 -1
- package/dist/src/private-to-private/transport.d.ts +7 -0
- package/dist/src/private-to-private/transport.d.ts.map +1 -1
- package/dist/src/private-to-private/transport.js +25 -9
- package/dist/src/private-to-private/transport.js.map +1 -1
- package/dist/src/private-to-public/listener.d.ts +1 -0
- package/dist/src/private-to-public/listener.d.ts.map +1 -1
- package/dist/src/private-to-public/listener.js +3 -1
- package/dist/src/private-to-public/listener.js.map +1 -1
- package/dist/src/private-to-public/transport.browser.d.ts +7 -0
- package/dist/src/private-to-public/transport.browser.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.browser.js +4 -1
- package/dist/src/private-to-public/transport.browser.js.map +1 -1
- package/dist/src/private-to-public/utils/connect.js +2 -0
- package/dist/src/private-to-public/utils/connect.js.map +1 -1
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js +3 -1
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js.map +1 -1
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts +4 -1
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.js +21 -13
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.js.map +1 -1
- package/dist/src/private-to-public/utils/stun-listener.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/stun-listener.js +2 -5
- package/dist/src/private-to-public/utils/stun-listener.js.map +1 -1
- package/dist/src/util.d.ts +4 -2
- package/dist/src/util.d.ts.map +1 -1
- package/dist/src/util.js +21 -0
- package/dist/src/util.js.map +1 -1
- package/dist/typedoc-urls.json +14 -0
- package/package.json +8 -8
- package/src/constants.ts +31 -0
- package/src/muxer.ts +160 -13
- package/src/private-to-private/initiate-connection.ts +7 -2
- package/src/private-to-private/transport.ts +33 -9
- package/src/private-to-public/listener.ts +4 -1
- package/src/private-to-public/transport.browser.ts +12 -1
- package/src/private-to-public/utils/connect.ts +2 -0
- package/src/private-to-public/utils/get-rtcpeerconnection.browser.ts +3 -1
- package/src/private-to-public/utils/get-rtcpeerconnection.ts +27 -14
- package/src/private-to-public/utils/stun-listener.ts +2 -7
- 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
|
|
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
|
-
|
|
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
|
|
77
|
-
*
|
|
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:
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|