@libp2p/webrtc 6.0.32 → 6.0.33-16fba5630

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 (64) hide show
  1. package/README.md +22 -1
  2. package/dist/index.min.js +9 -9
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/constants.d.ts +2 -1
  5. package/dist/src/constants.d.ts.map +1 -1
  6. package/dist/src/constants.js +2 -1
  7. package/dist/src/constants.js.map +1 -1
  8. package/dist/src/index.d.ts +22 -1
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +22 -1
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/private-to-public/listener.d.ts.map +1 -1
  13. package/dist/src/private-to-public/listener.js +9 -7
  14. package/dist/src/private-to-public/listener.js.map +1 -1
  15. package/dist/src/private-to-public/transport.browser.d.ts +9 -0
  16. package/dist/src/private-to-public/transport.browser.d.ts.map +1 -1
  17. package/dist/src/private-to-public/transport.browser.js +33 -5
  18. package/dist/src/private-to-public/transport.browser.js.map +1 -1
  19. package/dist/src/private-to-public/utils/connect.d.ts +16 -0
  20. package/dist/src/private-to-public/utils/connect.d.ts.map +1 -1
  21. package/dist/src/private-to-public/utils/connect.js +77 -16
  22. package/dist/src/private-to-public/utils/connect.js.map +1 -1
  23. package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts +4 -1
  24. package/dist/src/private-to-public/utils/get-rtcpeerconnection.d.ts.map +1 -1
  25. package/dist/src/private-to-public/utils/get-rtcpeerconnection.js +18 -11
  26. package/dist/src/private-to-public/utils/get-rtcpeerconnection.js.map +1 -1
  27. package/dist/src/private-to-public/utils/sdp.d.ts +3 -1
  28. package/dist/src/private-to-public/utils/sdp.d.ts.map +1 -1
  29. package/dist/src/private-to-public/utils/sdp.js +16 -2
  30. package/dist/src/private-to-public/utils/sdp.js.map +1 -1
  31. package/dist/src/private-to-public/utils/stun-listener.browser.d.ts +9 -0
  32. package/dist/src/private-to-public/utils/stun-listener.browser.d.ts.map +1 -0
  33. package/dist/src/private-to-public/utils/stun-listener.browser.js +6 -0
  34. package/dist/src/private-to-public/utils/stun-listener.browser.js.map +1 -0
  35. package/dist/src/private-to-public/utils/stun-listener.d.ts +2 -4
  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 +8 -2
  38. package/dist/src/private-to-public/utils/stun-listener.js.map +1 -1
  39. package/dist/src/private-to-public/utils/stun.d.ts +34 -0
  40. package/dist/src/private-to-public/utils/stun.d.ts.map +1 -0
  41. package/dist/src/private-to-public/utils/stun.js +107 -0
  42. package/dist/src/private-to-public/utils/stun.js.map +1 -0
  43. package/dist/src/util.d.ts +3 -5
  44. package/dist/src/util.d.ts.map +1 -1
  45. package/dist/src/util.js +3 -24
  46. package/dist/src/util.js.map +1 -1
  47. package/dist/src/webrtc/node-datachannel.browser.d.ts +7 -0
  48. package/dist/src/webrtc/node-datachannel.browser.d.ts.map +1 -0
  49. package/dist/src/webrtc/node-datachannel.browser.js +16 -0
  50. package/dist/src/webrtc/node-datachannel.browser.js.map +1 -0
  51. package/package.json +13 -10
  52. package/src/constants.ts +2 -1
  53. package/src/index.ts +22 -1
  54. package/src/private-to-public/listener.ts +9 -7
  55. package/src/private-to-public/transport.browser.ts +48 -5
  56. package/src/private-to-public/utils/connect.ts +112 -17
  57. package/src/private-to-public/utils/get-rtcpeerconnection.ts +20 -11
  58. package/src/private-to-public/utils/sdp.ts +21 -2
  59. package/src/private-to-public/utils/stun-listener.browser.ts +14 -0
  60. package/src/private-to-public/utils/stun-listener.ts +9 -7
  61. package/src/private-to-public/utils/stun.ts +128 -0
  62. package/src/util.ts +5 -32
  63. package/src/webrtc/node-datachannel.browser.ts +17 -0
  64. 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 UFRAG_PREFIX = 'libp2p+webrtc+v1/'
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 uses a technique known as [SDP munging](https://webrtchacks.com/not-a-guide-to-sdp-munging/) to skip 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.
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, (ufrag, remoteHost, remotePort) => {
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(ufrag, remoteHost, remotePort, signal)
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 (ufrag: string, remoteHost: string, remotePort: number, signal: AbortSignal): Promise<void> {
175
- const key = `${remoteHost}:${remotePort}:${ufrag}`
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#browser-to-public-server
189
- const results = await createDialerRTCPeerConnection('server', ufrag, {
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, ufrag, {
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
- const ufrag = genUfrag()
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
- // https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md#browser-to-public-server
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
- return await connect(peerConnection, muxerFactory, ufrag, {
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 answerSdp = sdp.serverAnswerFromMultiaddr(options.remoteAddr, ufrag)
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 offerSdp = sdp.clientOfferFromMultiAddr(options.remoteAddr, ufrag)
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
- const mungedAnswerSdp = sdp.munge(answerSdp, ufrag)
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(mungedAnswerSdp)
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
- return await options.upgrader.upgradeOutbound(maConn, {
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 ufrag before creating offer
45
- this.setLocalUfrag('offer')
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 ufrag before creating answer
52
- this.setLocalUfrag('answer')
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
- // 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
+ // 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 (!isValidUfrag(this.ufrag)) {
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.ufrag
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:${ufrag}
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 '../../util.ts'
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 interface Callback {
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
- handleStunRequest(request, log, cb)
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 {