@libp2p/webrtc 6.0.32 → 6.0.33-183116ba0
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/README.md +22 -1
- package/dist/index.min.js +9 -9
- package/dist/index.min.js.map +4 -4
- package/dist/src/constants.d.ts +2 -1
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +2 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +22 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +22 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/private-to-public/listener.d.ts.map +1 -1
- package/dist/src/private-to-public/listener.js +9 -7
- package/dist/src/private-to-public/listener.js.map +1 -1
- package/dist/src/private-to-public/transport.browser.d.ts +9 -0
- package/dist/src/private-to-public/transport.browser.d.ts.map +1 -1
- package/dist/src/private-to-public/transport.browser.js +33 -5
- package/dist/src/private-to-public/transport.browser.js.map +1 -1
- package/dist/src/private-to-public/utils/connect.d.ts +16 -0
- package/dist/src/private-to-public/utils/connect.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/connect.js +77 -16
- package/dist/src/private-to-public/utils/connect.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 +18 -11
- package/dist/src/private-to-public/utils/get-rtcpeerconnection.js.map +1 -1
- package/dist/src/private-to-public/utils/sdp.d.ts +3 -1
- package/dist/src/private-to-public/utils/sdp.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/sdp.js +16 -2
- package/dist/src/private-to-public/utils/sdp.js.map +1 -1
- package/dist/src/private-to-public/utils/stun-listener.browser.d.ts +9 -0
- package/dist/src/private-to-public/utils/stun-listener.browser.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/stun-listener.browser.js +6 -0
- package/dist/src/private-to-public/utils/stun-listener.browser.js.map +1 -0
- package/dist/src/private-to-public/utils/stun-listener.d.ts +2 -4
- package/dist/src/private-to-public/utils/stun-listener.d.ts.map +1 -1
- package/dist/src/private-to-public/utils/stun-listener.js +8 -2
- package/dist/src/private-to-public/utils/stun-listener.js.map +1 -1
- package/dist/src/private-to-public/utils/stun.d.ts +34 -0
- package/dist/src/private-to-public/utils/stun.d.ts.map +1 -0
- package/dist/src/private-to-public/utils/stun.js +107 -0
- package/dist/src/private-to-public/utils/stun.js.map +1 -0
- package/dist/src/util.d.ts +3 -5
- package/dist/src/util.d.ts.map +1 -1
- package/dist/src/util.js +3 -24
- package/dist/src/util.js.map +1 -1
- package/dist/src/webrtc/node-datachannel.browser.d.ts +7 -0
- package/dist/src/webrtc/node-datachannel.browser.d.ts.map +1 -0
- package/dist/src/webrtc/node-datachannel.browser.js +16 -0
- package/dist/src/webrtc/node-datachannel.browser.js.map +1 -0
- package/package.json +13 -10
- package/src/constants.ts +2 -1
- package/src/index.ts +22 -1
- package/src/private-to-public/listener.ts +9 -7
- package/src/private-to-public/transport.browser.ts +48 -5
- package/src/private-to-public/utils/connect.ts +112 -17
- package/src/private-to-public/utils/get-rtcpeerconnection.ts +20 -11
- package/src/private-to-public/utils/sdp.ts +21 -2
- package/src/private-to-public/utils/stun-listener.browser.ts +14 -0
- package/src/private-to-public/utils/stun-listener.ts +9 -7
- package/src/private-to-public/utils/stun.ts +128 -0
- package/src/util.ts +5 -32
- package/src/webrtc/node-datachannel.browser.ts +17 -0
- package/dist/typedoc-urls.json +0 -14
package/src/constants.ts
CHANGED
|
@@ -24,7 +24,8 @@ export const UFRAG_ALPHABET = Array.from('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL
|
|
|
24
24
|
/**
|
|
25
25
|
* Used to detect the version of the WebRTC Direct connection protocol in use
|
|
26
26
|
*/
|
|
27
|
-
export const
|
|
27
|
+
export const UFRAG_PREFIX_V1 = 'libp2p+webrtc+v1/'
|
|
28
|
+
export const UFRAG_PREFIX_V2 = 'libp2p+webrtc+v2/'
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* How much can be buffered to the DataChannel at once
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* The WebRTC transport uses libp2p [Circuit Relay](https://libp2p.io/docs/circuit-relay/)s to forward SDP messages. Once a direct connection is formed the relay plays no further part in the exchange.
|
|
17
17
|
*
|
|
18
|
-
* WebRTC Direct
|
|
18
|
+
* WebRTC Direct skips the handshake step, instead encoding enough information in the connection request that the responder can derive what would have been in the handshake messages and so requires no third parties to establish a connection. Version 1 of the protocol does this with a technique known as [SDP munging](https://webrtchacks.com/not-a-guide-to-sdp-munging/); version 2 avoids munging, which browsers are removing support for. See WebRTC Direct versions below.
|
|
19
19
|
*
|
|
20
20
|
* A WebRTC Direct multiaddr also includes a certhash of the target peer - this is used to allow opening a connection to the remote, which would otherwise be denied due to use of a self-signed certificate.
|
|
21
21
|
*
|
|
@@ -197,6 +197,27 @@
|
|
|
197
197
|
* })
|
|
198
198
|
* ```
|
|
199
199
|
*
|
|
200
|
+
* ## WebRTC Direct versions
|
|
201
|
+
*
|
|
202
|
+
* There are two versions of the WebRTC Direct connection flow. Both use the
|
|
203
|
+
* same `/webrtc-direct` multiaddr and certificate hash; they differ in how the
|
|
204
|
+
* client's ICE credentials reach the server.
|
|
205
|
+
*
|
|
206
|
+
* Version 1 munges the client's SDP offer so that the server can predict the
|
|
207
|
+
* client's ICE credentials. Browsers are removing the ability to do this, so
|
|
208
|
+
* v1 will stop working for browser clients.
|
|
209
|
+
*
|
|
210
|
+
* Version 2 leaves the local SDP offer untouched and instead carries the
|
|
211
|
+
* client's ICE password in the ICE username fragment of the synthetic server
|
|
212
|
+
* answer, where the server reads it back from the incoming STUN request.
|
|
213
|
+
*
|
|
214
|
+
* No configuration is needed for either version. Listeners accept both on the
|
|
215
|
+
* same port and select the version from the incoming username fragment prefix.
|
|
216
|
+
* The first dial tries v1 and, if the runtime does not apply the munged ICE
|
|
217
|
+
* credentials, continues as v2; later dials then start at v2. The
|
|
218
|
+
* `dialerVersion` option skips this and forces one version, for example `2`
|
|
219
|
+
* for a server known to support it (go-libp2p does from v0.49.0).
|
|
220
|
+
*
|
|
200
221
|
* ## WebRTC Direct certificate hashes
|
|
201
222
|
*
|
|
202
223
|
* WebRTC Direct listeners publish the hash of their TLS certificate as part of
|
|
@@ -156,10 +156,10 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
|
|
|
156
156
|
this.log.trace('listening on free port %d', port)
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
return stunListener(host, port, this.log, (
|
|
159
|
+
return stunListener(host, port, this.log, (serverUfrag, clientUfrag, clientPwd, remoteHost, remotePort) => {
|
|
160
160
|
const signal = this.components.upgrader.createInboundAbortSignal(this.shutdownController.signal)
|
|
161
161
|
|
|
162
|
-
this.incomingConnection(
|
|
162
|
+
this.incomingConnection(serverUfrag, clientUfrag, clientPwd, remoteHost, remotePort, signal)
|
|
163
163
|
.catch(err => {
|
|
164
164
|
this.log.error('error processing incoming STUN request - %e', err)
|
|
165
165
|
})
|
|
@@ -171,8 +171,8 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
private async incomingConnection (
|
|
175
|
-
const key = `${remoteHost}:${remotePort}:${
|
|
174
|
+
private async incomingConnection (serverUfrag: string, clientUfrag: string, clientPwd: string | undefined, remoteHost: string, remotePort: number, signal: AbortSignal): Promise<void> {
|
|
175
|
+
const key = `${remoteHost}:${remotePort}:${serverUfrag}:${clientUfrag}`
|
|
176
176
|
let peerConnection = this.connections.get(key)
|
|
177
177
|
|
|
178
178
|
if (peerConnection != null) {
|
|
@@ -185,8 +185,8 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
|
|
|
185
185
|
// do not create RTCPeerConnection objects if the signal has aborted already
|
|
186
186
|
signal.throwIfAborted()
|
|
187
187
|
|
|
188
|
-
// https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md
|
|
189
|
-
const results = await createDialerRTCPeerConnection('server',
|
|
188
|
+
// https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md
|
|
189
|
+
const results = await createDialerRTCPeerConnection('server', serverUfrag, {
|
|
190
190
|
rtcConfiguration: this.init.rtcConfiguration,
|
|
191
191
|
certificate: this.certificate,
|
|
192
192
|
events: this.metrics?.listenerEvents,
|
|
@@ -211,12 +211,14 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
|
|
|
211
211
|
})
|
|
212
212
|
|
|
213
213
|
try {
|
|
214
|
-
await connect(peerConnection, results.muxerFactory,
|
|
214
|
+
await connect(peerConnection, results.muxerFactory, serverUfrag, {
|
|
215
215
|
role: 'server',
|
|
216
216
|
log: this.log,
|
|
217
217
|
logger: this.components.logger,
|
|
218
218
|
events: this.metrics?.listenerEvents,
|
|
219
219
|
signal,
|
|
220
|
+
remoteUfrag: clientUfrag,
|
|
221
|
+
remotePwd: clientPwd,
|
|
220
222
|
remoteAddr: multiaddr(`/ip${isIPv4(remoteHost) ? 4 : 6}/${remoteHost}/udp/${remotePort}/webrtc-direct`),
|
|
221
223
|
dataChannel: this.init.dataChannel,
|
|
222
224
|
upgrader: this.init.upgrader,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { serviceCapabilities, transportSymbol } from '@libp2p/interface'
|
|
1
|
+
import { InvalidParametersError, serviceCapabilities, transportSymbol } from '@libp2p/interface'
|
|
2
2
|
import { peerIdFromString } from '@libp2p/peer-id'
|
|
3
3
|
import { CODE_P2P } from '@multiformats/multiaddr'
|
|
4
4
|
import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
|
|
5
|
+
import { UFRAG_PREFIX_V1 } from '../constants.ts'
|
|
5
6
|
import { UnimplementedError } from '../error.ts'
|
|
6
7
|
import { genUfrag } from '../util.ts'
|
|
7
8
|
import { connect } from './utils/connect.ts'
|
|
@@ -30,6 +31,15 @@ export interface WebRTCMetrics {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export interface WebRTCTransportDirectInit {
|
|
34
|
+
/**
|
|
35
|
+
* Force the version of the WebRTC Direct connection flow to dial with. When
|
|
36
|
+
* not set, the first dial tries v1 and continues as v2 if the runtime does
|
|
37
|
+
* not apply the munged ICE credentials in the local SDP offer, and later
|
|
38
|
+
* dials start at whichever version that showed the runtime supports. The
|
|
39
|
+
* server must support the version in use, listeners accept both.
|
|
40
|
+
*/
|
|
41
|
+
dialerVersion?: 1 | 2
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* The default configuration used by all created RTCPeerConnections
|
|
35
45
|
*/
|
|
@@ -54,12 +64,19 @@ export class WebRTCDirectTransport implements Transport {
|
|
|
54
64
|
protected readonly metrics?: WebRTCMetrics
|
|
55
65
|
protected readonly components: WebRTCDirectTransportComponents
|
|
56
66
|
protected readonly init: WebRTCTransportDirectInit
|
|
67
|
+
private mungeable?: boolean
|
|
57
68
|
|
|
58
69
|
constructor (components: WebRTCDirectTransportComponents, init: WebRTCTransportDirectInit = {}) {
|
|
59
70
|
this.log = components.logger.forComponent('libp2p:webrtc-direct')
|
|
60
71
|
this.components = components
|
|
61
72
|
this.init = init
|
|
62
73
|
|
|
74
|
+
const dialerVersion: unknown = init.dialerVersion
|
|
75
|
+
|
|
76
|
+
if (dialerVersion != null && dialerVersion !== 1 && dialerVersion !== 2) {
|
|
77
|
+
throw new InvalidParametersError(`Unknown WebRTC Direct dialer version "${String(dialerVersion)}"`)
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
if (components.metrics != null) {
|
|
64
81
|
this.metrics = {
|
|
65
82
|
dialerEvents: components.metrics.registerCounterGroup('libp2p_webrtc-direct_dialer_events_total', {
|
|
@@ -92,9 +109,20 @@ export class WebRTCDirectTransport implements Transport {
|
|
|
92
109
|
theirPeerId = peerIdFromString(remotePeerString)
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
// an explicit dialer version is always used, otherwise start at v1 until a
|
|
113
|
+
// dial shows this runtime does not apply munged ICE credentials
|
|
114
|
+
const version = this.init.dialerVersion ?? (this.mungeable === false ? 2 : 1)
|
|
115
|
+
const ufrag = version === 2
|
|
116
|
+
? genUfrag(32, '')
|
|
117
|
+
: genUfrag(32, UFRAG_PREFIX_V1)
|
|
118
|
+
const pwd = version === 2 ? genUfrag(22, '') : ufrag
|
|
119
|
+
|
|
120
|
+
// the version actually dialed, v1 may continue as v2
|
|
121
|
+
let dialedVersion = version
|
|
96
122
|
|
|
97
|
-
|
|
123
|
+
this.log('dial %a starting as WebRTC Direct v%d', ma, version)
|
|
124
|
+
|
|
125
|
+
// https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md
|
|
98
126
|
const {
|
|
99
127
|
peerConnection,
|
|
100
128
|
muxerFactory
|
|
@@ -103,16 +131,28 @@ export class WebRTCDirectTransport implements Transport {
|
|
|
103
131
|
events: this.metrics?.dialerEvents,
|
|
104
132
|
log: this.log,
|
|
105
133
|
dataChannel: this.init.dataChannel,
|
|
106
|
-
maxEarlyStreams: this.init.maxEarlyStreams
|
|
134
|
+
maxEarlyStreams: this.init.maxEarlyStreams,
|
|
135
|
+
pwd
|
|
107
136
|
})
|
|
108
137
|
|
|
109
138
|
try {
|
|
110
|
-
|
|
139
|
+
const connection = await connect(peerConnection, muxerFactory, ufrag, {
|
|
111
140
|
role: 'client',
|
|
112
141
|
log: this.log,
|
|
113
142
|
logger: this.components.logger,
|
|
114
143
|
events: this.metrics?.dialerEvents,
|
|
115
144
|
signal: options.signal,
|
|
145
|
+
version,
|
|
146
|
+
fallback: this.init.dialerVersion == null,
|
|
147
|
+
onMungeable: (mungeable) => {
|
|
148
|
+
// munging support belongs to the runtime, not the remote, so it is
|
|
149
|
+
// remembered even if this dial fails
|
|
150
|
+
this.mungeable ??= mungeable
|
|
151
|
+
|
|
152
|
+
if (!mungeable && this.init.dialerVersion == null) {
|
|
153
|
+
dialedVersion = 2
|
|
154
|
+
}
|
|
155
|
+
},
|
|
116
156
|
remoteAddr: ma,
|
|
117
157
|
dataChannel: this.init.dataChannel,
|
|
118
158
|
upgrader: options.upgrader,
|
|
@@ -120,7 +160,10 @@ export class WebRTCDirectTransport implements Transport {
|
|
|
120
160
|
remotePeer: theirPeerId,
|
|
121
161
|
privateKey: this.components.privateKey
|
|
122
162
|
})
|
|
163
|
+
|
|
164
|
+
return connection
|
|
123
165
|
} catch (err) {
|
|
166
|
+
this.log('dial %a failed as WebRTC Direct v%d - %e', ma, dialedVersion, err)
|
|
124
167
|
peerConnection.close()
|
|
125
168
|
throw err
|
|
126
169
|
}
|
|
@@ -6,6 +6,7 @@ import { toMultiaddrConnection } from '../../rtcpeerconnection-to-conn.ts'
|
|
|
6
6
|
import { createStream } from '../../stream.ts'
|
|
7
7
|
import { generateNoisePrologue } from './generate-noise-prologue.ts'
|
|
8
8
|
import * as sdp from './sdp.ts'
|
|
9
|
+
import { isIcePwd, isIceUfrag, serverUfragV2 } from './stun.ts'
|
|
9
10
|
import type { DirectRTCPeerConnection } from './get-rtcpeerconnection.ts'
|
|
10
11
|
import type { DataChannelOptions } from '../../index.ts'
|
|
11
12
|
import type { ComponentLogger, Connection, CounterGroup, Logger, PeerId, PrivateKey, Upgrader } from '@libp2p/interface'
|
|
@@ -23,10 +24,29 @@ export interface ConnectOptions {
|
|
|
23
24
|
remotePeer?: PeerId
|
|
24
25
|
signal: AbortSignal
|
|
25
26
|
privateKey: PrivateKey
|
|
27
|
+
remoteUfrag?: string
|
|
28
|
+
remotePwd?: string
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export interface ClientOptions extends ConnectOptions {
|
|
29
32
|
role: 'client'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Which version of the connection flow to dial with
|
|
36
|
+
*/
|
|
37
|
+
version: 1 | 2
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Continue as v2 when the runtime does not apply the munged ICE credentials
|
|
41
|
+
* a v1 dial needs
|
|
42
|
+
*/
|
|
43
|
+
fallback?: boolean
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Called once a v1 attempt shows whether the runtime applies munged ICE
|
|
47
|
+
* credentials, even if the dial later fails
|
|
48
|
+
*/
|
|
49
|
+
onMungeable?(mungeable: boolean): void
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
export interface ServerOptions extends ConnectOptions {
|
|
@@ -37,6 +57,86 @@ function isServer (options: ClientOptions | ServerOptions, peerConnection: any):
|
|
|
37
57
|
return options.role === 'server'
|
|
38
58
|
}
|
|
39
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Set `offer` as the local description with its ICE ufrag and password munged
|
|
62
|
+
* to `ufrag`, as WebRTC Direct v1 requires, and report whether the runtime
|
|
63
|
+
* applied them. Browsers are removing support for this, in which case
|
|
64
|
+
* setLocalDescription either rejects the offer with an InvalidModificationError
|
|
65
|
+
* or keeps the generated credentials.
|
|
66
|
+
*/
|
|
67
|
+
async function setMungedLocalOffer (peerConnection: RTCPeerConnection | DirectRTCPeerConnection, offer: RTCSessionDescriptionInit, ufrag: string, log: Logger): Promise<boolean> {
|
|
68
|
+
const mungedOffer = sdp.munge({ type: offer.type, sdp: offer.sdp }, ufrag)
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
log.trace('client setting munged local offer %s', mungedOffer.sdp)
|
|
72
|
+
await peerConnection.setLocalDescription(mungedOffer)
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
// any other rejection is a real failure, not the runtime refusing the munge
|
|
75
|
+
if (err.name !== 'InvalidModificationError') {
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
log('runtime rejected the munged local offer - %e', err)
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const localSdp = peerConnection.localDescription?.sdp
|
|
84
|
+
|
|
85
|
+
return sdp.getIceUfragFromSdp(localSdp) === ufrag && sdp.getIcePwdFromSdp(localSdp) === ufrag
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set the local offer for the requested version and return the ufrag for the
|
|
90
|
+
* synthetic server answer.
|
|
91
|
+
*
|
|
92
|
+
* v1 munges the offer so ice-ufrag and ice-pwd both equal `ufrag`, letting the
|
|
93
|
+
* server infer the client's credentials from the STUN USERNAME alone. v2 keeps
|
|
94
|
+
* the credentials the runtime generated and carries the client's ICE password
|
|
95
|
+
* in the server ufrag instead. When a v1 attempt does not stick and fallback
|
|
96
|
+
* is allowed, the dial continues as v2.
|
|
97
|
+
*/
|
|
98
|
+
async function setClientOffer (peerConnection: RTCPeerConnection | DirectRTCPeerConnection, offer: RTCSessionDescriptionInit, ufrag: string, options: ClientOptions): Promise<string> {
|
|
99
|
+
if (options.version === 1) {
|
|
100
|
+
const mungeable = await setMungedLocalOffer(peerConnection, offer, ufrag, options.log)
|
|
101
|
+
options.onMungeable?.(mungeable)
|
|
102
|
+
|
|
103
|
+
if (mungeable) {
|
|
104
|
+
return ufrag
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (options.fallback !== true) {
|
|
108
|
+
throw new WebRTCTransportError('SDP munging is not available in this runtime so WebRTC Direct v1 cannot be used')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
options.log('SDP munging is not available in this runtime, continuing as WebRTC Direct v2')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// a rejected munged offer leaves no local description, an ignored one leaves
|
|
115
|
+
// the runtime's own credentials in place, which is what v2 needs
|
|
116
|
+
if (peerConnection.localDescription == null) {
|
|
117
|
+
options.log.trace('client setting local offer %s', offer.sdp)
|
|
118
|
+
await peerConnection.setLocalDescription(offer)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// encode the client's ICE password in the synthetic server ufrag
|
|
122
|
+
const localPwd = sdp.getIcePwdFromSdp(peerConnection.localDescription?.sdp)
|
|
123
|
+
|
|
124
|
+
if (localPwd == null || !isIcePwd(localPwd)) {
|
|
125
|
+
// without a valid local ICE password we cannot build a valid v2 server
|
|
126
|
+
// ufrag; fail loudly instead of dialing with a ufrag the server would reject
|
|
127
|
+
throw new WebRTCTransportError('Could not read a valid local ICE password from local description for v2 dial')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const serverUfrag = serverUfragV2(localPwd)
|
|
131
|
+
|
|
132
|
+
if (!isIceUfrag(serverUfrag)) {
|
|
133
|
+
// the prefix counts towards the 256 character ufrag limit
|
|
134
|
+
throw new WebRTCTransportError('Local ICE password is too long to encode in a v2 server ufrag')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return serverUfrag
|
|
138
|
+
}
|
|
139
|
+
|
|
40
140
|
export async function connect (peerConnection: RTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ClientOptions): Promise<Connection>
|
|
41
141
|
export async function connect (peerConnection: DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ServerOptions): Promise<void>
|
|
42
142
|
export async function connect (peerConnection: RTCPeerConnection | DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ClientOptions | ServerOptions): Promise<any> {
|
|
@@ -48,37 +148,30 @@ export async function connect (peerConnection: RTCPeerConnection | DirectRTCPeer
|
|
|
48
148
|
try {
|
|
49
149
|
if (options.role === 'client') {
|
|
50
150
|
// the client has to set the local offer before the remote answer
|
|
51
|
-
|
|
52
|
-
// Create offer and munge sdp with ufrag == pwd. This allows the remote to
|
|
53
|
-
// respond to STUN messages without performing an actual SDP exchange.
|
|
54
|
-
// This is because it can infer the passwd field by reading the USERNAME
|
|
55
|
-
// attribute of the STUN message.
|
|
56
151
|
options.log.trace('client creating local offer')
|
|
57
152
|
const offerSdp = await peerConnection.createOffer()
|
|
58
153
|
options.log.trace('client created local offer %s', offerSdp.sdp)
|
|
59
|
-
const mungedOfferSdp = sdp.munge(offerSdp, ufrag)
|
|
60
|
-
options.log.trace('client setting local offer %s', mungedOfferSdp.sdp)
|
|
61
|
-
await peerConnection.setLocalDescription(mungedOfferSdp)
|
|
62
154
|
|
|
63
|
-
const
|
|
155
|
+
const serverUfrag = await setClientOffer(peerConnection, offerSdp, ufrag, options)
|
|
156
|
+
const answerSdp = sdp.serverAnswerFromMultiaddr(options.remoteAddr, serverUfrag)
|
|
64
157
|
options.log.trace('client setting server description %s', answerSdp.sdp)
|
|
65
158
|
await peerConnection.setRemoteDescription(answerSdp)
|
|
66
159
|
} else {
|
|
67
160
|
// the server has to set the remote offer before the local answer
|
|
68
|
-
const
|
|
161
|
+
const remoteUfrag = options.remoteUfrag ?? ufrag
|
|
162
|
+
const remotePwd = options.remotePwd ?? remoteUfrag
|
|
163
|
+
const offerSdp = sdp.clientOfferFromMultiAddr(options.remoteAddr, remoteUfrag, remotePwd)
|
|
69
164
|
options.log.trace('server setting client %s %s', offerSdp.type, offerSdp.sdp)
|
|
70
165
|
await peerConnection.setRemoteDescription(offerSdp)
|
|
71
166
|
|
|
72
|
-
// Create offer and munge sdp with ufrag == pwd. This allows the remote to
|
|
73
|
-
// respond to STUN messages without performing an actual SDP exchange.
|
|
74
|
-
// This is because it can infer the passwd field by reading the USERNAME
|
|
75
|
-
// attribute of the STUN message.
|
|
76
167
|
options.log.trace('server creating local answer')
|
|
77
168
|
const answerSdp = await peerConnection.createAnswer()
|
|
78
169
|
options.log.trace('server created local answer')
|
|
79
|
-
|
|
170
|
+
|
|
171
|
+
// the answer credentials were pinned to the server ufrag when the peer
|
|
172
|
+
// connection was created, so it needs no munging for either version
|
|
80
173
|
options.log.trace('server setting local description %s', answerSdp.sdp)
|
|
81
|
-
await peerConnection.setLocalDescription(
|
|
174
|
+
await peerConnection.setLocalDescription(answerSdp)
|
|
82
175
|
}
|
|
83
176
|
|
|
84
177
|
if (handshakeDataChannel.readyState !== 'open') {
|
|
@@ -162,13 +255,15 @@ export async function connect (peerConnection: RTCPeerConnection | DirectRTCPeer
|
|
|
162
255
|
})
|
|
163
256
|
|
|
164
257
|
options.log.trace('%s upgrade outbound', options.role)
|
|
165
|
-
|
|
258
|
+
const connection = await options.upgrader.upgradeOutbound(maConn, {
|
|
166
259
|
skipProtection: true,
|
|
167
260
|
skipEncryption: true,
|
|
168
261
|
remotePeer: result.remotePeer,
|
|
169
262
|
muxerFactory,
|
|
170
263
|
signal: options.signal
|
|
171
264
|
})
|
|
265
|
+
|
|
266
|
+
return connection
|
|
172
267
|
}
|
|
173
268
|
|
|
174
269
|
// For inbound connections, the server is are expected to start the noise
|
|
@@ -4,8 +4,8 @@ import { PeerConnection } from 'node-datachannel'
|
|
|
4
4
|
import { RTCPeerConnection } from 'node-datachannel/polyfill'
|
|
5
5
|
import { DEFAULT_ICE_SERVERS, MAX_MESSAGE_SIZE } from '../../constants.ts'
|
|
6
6
|
import { DataChannelMuxerFactory } from '../../muxer.ts'
|
|
7
|
-
import { isValidUfrag } from '../../util.ts'
|
|
8
7
|
import { generateTransportCertificate } from './generate-certificates.ts'
|
|
8
|
+
import { isIcePwd, isIceUfrag } from './stun.ts'
|
|
9
9
|
import type { DataChannelOptions, TransportCertificate } from '../../index.ts'
|
|
10
10
|
import type { CounterGroup, Logger } from '@libp2p/interface'
|
|
11
11
|
import type { CertificateFingerprint } from 'node-datachannel'
|
|
@@ -14,18 +14,21 @@ const crypto = new Crypto()
|
|
|
14
14
|
|
|
15
15
|
interface DirectRTCPeerConnectionInit extends RTCConfiguration {
|
|
16
16
|
ufrag: string
|
|
17
|
+
pwd: string
|
|
17
18
|
peerConnection: PeerConnection
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export class DirectRTCPeerConnection extends RTCPeerConnection {
|
|
21
22
|
private peerConnection: PeerConnection
|
|
22
23
|
private readonly ufrag: string
|
|
24
|
+
private readonly pwd: string
|
|
23
25
|
|
|
24
26
|
constructor (init: DirectRTCPeerConnectionInit) {
|
|
25
27
|
super(init)
|
|
26
28
|
|
|
27
29
|
this.peerConnection = init.peerConnection
|
|
28
30
|
this.ufrag = init.ufrag
|
|
31
|
+
this.pwd = init.pwd
|
|
29
32
|
|
|
30
33
|
// make sure C++ peer connection is garbage collected
|
|
31
34
|
// https://github.com/murat-dogan/node-datachannel/issues/366#issuecomment-3228453155
|
|
@@ -41,33 +44,36 @@ export class DirectRTCPeerConnection extends RTCPeerConnection {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
async createOffer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
|
|
44
|
-
// have to set
|
|
45
|
-
this.
|
|
47
|
+
// have to set the ICE credentials before creating the offer
|
|
48
|
+
this.setLocalCredentials('offer')
|
|
46
49
|
|
|
47
50
|
return super.createOffer()
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
async createAnswer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
|
|
51
|
-
// have to set
|
|
52
|
-
this.
|
|
54
|
+
// have to set the ICE credentials before creating the answer
|
|
55
|
+
this.setLocalCredentials('answer')
|
|
53
56
|
|
|
54
57
|
return super.createAnswer()
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
+
// node-datachannel needs the ICE credentials set before the description is
|
|
61
|
+
// created. On the server they come from an attacker-controlled STUN username
|
|
62
|
+
// and an invalid value aborts the process inside the native ICE stack, so
|
|
63
|
+
// reject it with a recoverable error first. The browser path uses a native
|
|
64
|
+
// RTCPeerConnection and never reaches this class.
|
|
65
|
+
private setLocalCredentials (type: 'offer' | 'answer'): void {
|
|
60
66
|
if (this.connectionState !== 'new') {
|
|
61
67
|
return
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
if (!
|
|
65
|
-
throw new InvalidParametersError('ufrag is not a valid ICE credential')
|
|
70
|
+
if (!isIceUfrag(this.ufrag) || !isIcePwd(this.pwd)) {
|
|
71
|
+
throw new InvalidParametersError('ufrag or pwd is not a valid ICE credential')
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
this.peerConnection?.setLocalDescription(type, {
|
|
69
75
|
iceUfrag: this.ufrag,
|
|
70
|
-
icePwd: this.
|
|
76
|
+
icePwd: this.pwd
|
|
71
77
|
})
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -103,6 +109,7 @@ export interface CreateDialerRTCPeerConnectionOptions {
|
|
|
103
109
|
log?: Logger
|
|
104
110
|
dataChannel?: DataChannelOptions
|
|
105
111
|
maxEarlyStreams?: number
|
|
112
|
+
pwd?: string
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
export async function createDialerRTCPeerConnection (role: 'client', ufrag: string, options?: CreateDialerRTCPeerConnectionOptions): Promise<{ peerConnection: globalThis.RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }>
|
|
@@ -124,10 +131,12 @@ export async function createDialerRTCPeerConnection (role: 'client' | 'server',
|
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
const rtcConfig = typeof options.rtcConfiguration === 'function' ? await options.rtcConfiguration() : options.rtcConfiguration
|
|
134
|
+
const pwd = options.pwd ?? ufrag
|
|
127
135
|
|
|
128
136
|
const peerConnection = new DirectRTCPeerConnection({
|
|
129
137
|
...rtcConfig,
|
|
130
138
|
ufrag,
|
|
139
|
+
pwd,
|
|
131
140
|
peerConnection: new PeerConnection(`${role}-${Date.now()}`, {
|
|
132
141
|
disableFingerprintVerification: true,
|
|
133
142
|
disableAutoNegotiation: true,
|
|
@@ -17,6 +17,9 @@ import type { MultihashDigest } from 'multiformats/hashes/interface'
|
|
|
17
17
|
export const multibaseDecoder: any = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b))
|
|
18
18
|
|
|
19
19
|
const fingerprintRegex = /^a=fingerprint:(?:\w+-[0-9]+)\s(?<fingerprint>(:?[0-9a-fA-F]{2})+)$/m
|
|
20
|
+
const iceUfragRegex = /^a=ice-ufrag:(?<ufrag>[^\r\n]+)$/m
|
|
21
|
+
const icePwdRegex = /^a=ice-pwd:(?<pwd>[^\r\n]+)$/m
|
|
22
|
+
|
|
20
23
|
export function getFingerprintFromSdp (sdp: string | undefined): string | undefined {
|
|
21
24
|
if (sdp == null) {
|
|
22
25
|
return undefined
|
|
@@ -26,6 +29,22 @@ export function getFingerprintFromSdp (sdp: string | undefined): string | undefi
|
|
|
26
29
|
return searchResult?.groups?.fingerprint
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
export function getIceUfragFromSdp (sdp: string | undefined): string | undefined {
|
|
33
|
+
if (sdp == null) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return sdp.match(iceUfragRegex)?.groups?.ufrag
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getIcePwdFromSdp (sdp: string | undefined): string | undefined {
|
|
41
|
+
if (sdp == null) {
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return sdp.match(icePwdRegex)?.groups?.pwd
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
// Extract the certhash from a multiaddr
|
|
30
49
|
export function certhash (ma: Multiaddr): string {
|
|
31
50
|
const components = ma.getComponents()
|
|
@@ -136,7 +155,7 @@ a=end-of-candidates
|
|
|
136
155
|
/**
|
|
137
156
|
* Create an offer SDP message from a multiaddr
|
|
138
157
|
*/
|
|
139
|
-
export function clientOfferFromMultiAddr (ma: Multiaddr, ufrag: string): RTCSessionDescriptionInit {
|
|
158
|
+
export function clientOfferFromMultiAddr (ma: Multiaddr, ufrag: string, pwd: string = ufrag): RTCSessionDescriptionInit {
|
|
140
159
|
const { host, port, type } = getNetConfig(ma)
|
|
141
160
|
|
|
142
161
|
if (type !== 'ip4' && type !== 'ip6') {
|
|
@@ -153,7 +172,7 @@ m=application ${port} UDP/DTLS/SCTP webrtc-datachannel
|
|
|
153
172
|
a=mid:0
|
|
154
173
|
a=setup:active
|
|
155
174
|
a=ice-ufrag:${ufrag}
|
|
156
|
-
a=ice-pwd:${
|
|
175
|
+
a=ice-pwd:${pwd}
|
|
157
176
|
a=fingerprint:sha-256 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
|
|
158
177
|
a=sctp-port:5000
|
|
159
178
|
a=max-message-size:${MAX_MESSAGE_SIZE}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { UnimplementedError } from '../../error.ts'
|
|
2
|
+
import type { StunRequestCallback } from './stun.ts'
|
|
3
|
+
import type { Logger } from '@libp2p/interface'
|
|
4
|
+
|
|
5
|
+
export { parseStunUsernameUfrags } from './stun.ts'
|
|
6
|
+
|
|
7
|
+
export interface StunServer {
|
|
8
|
+
close(): Promise<void>
|
|
9
|
+
address(): never
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function stunListener (host: string, port: number, log: Logger, cb: StunRequestCallback): Promise<StunServer> {
|
|
13
|
+
throw new UnimplementedError('stunListener')
|
|
14
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isIPv4 } from '@chainsafe/is-ip'
|
|
2
2
|
import { IceUdpMuxListener } from 'node-datachannel'
|
|
3
|
-
import { handleStunRequest } from '
|
|
3
|
+
import { handleStunRequest } from './stun.ts'
|
|
4
|
+
import type { StunRequestCallback } from './stun.ts'
|
|
4
5
|
import type { Logger } from '@libp2p/interface'
|
|
5
6
|
import type { AddressInfo } from 'node:net'
|
|
6
7
|
|
|
@@ -9,14 +10,15 @@ export interface StunServer {
|
|
|
9
10
|
address(): AddressInfo
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export
|
|
13
|
-
(ufrag: string, remoteHost: string, remotePort: number): void
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function stunListener (host: string, port: number, log: Logger, cb: Callback): Promise<StunServer> {
|
|
13
|
+
export async function stunListener (host: string, port: number, log: Logger, cb: StunRequestCallback): Promise<StunServer> {
|
|
17
14
|
const listener = new IceUdpMuxListener(port, host)
|
|
18
15
|
listener.onUnhandledStunRequest(request => {
|
|
19
|
-
|
|
16
|
+
// this runs inside a native callback where a throw would crash the process
|
|
17
|
+
try {
|
|
18
|
+
handleStunRequest(request, log, cb)
|
|
19
|
+
} catch (err) {
|
|
20
|
+
log.error('error handling STUN request from %s:%d - %e', request.host, request.port, err)
|
|
21
|
+
}
|
|
20
22
|
})
|
|
21
23
|
|
|
22
24
|
return {
|