Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/integration-tests/.aegir.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export default {
await before.goLibp2pRelay?.proc.kill()
await before.libp2pLimitedRelay?.stop()

// node-datachannel sometimes causes the process to hang
// Force exit after cleanup (WebRTC native modules may hold process open)
process.exit(0)
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/transport-webrtc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,18 @@
},
"dependencies": {
"@chainsafe/is-ip": "^2.1.0",
"@chainsafe/libp2p-noise": "^17.0.0",
"@libp2p/crypto": "^5.1.13",
"@libp2p/interface": "^3.1.0",
"@libp2p/interface-internal": "^3.0.9",
"@libp2p/keychain": "^6.0.9",
"@chainsafe/libp2p-noise": "^17.0.0",
"@libp2p/peer-id": "^6.0.4",
"@libp2p/utils": "^7.0.9",
"@multiformats/multiaddr": "^13.0.1",
"@multiformats/multiaddr-matcher": "^3.0.1",
"@peculiar/webcrypto": "^1.5.0",
"@peculiar/x509": "^1.13.0",
"@roamhq/wrtc": "^0.9.1",
"detect-browser": "^5.3.0",
"get-port": "^7.1.0",
"interface-datastore": "^9.0.1",
Expand All @@ -65,7 +66,6 @@
"it-stream-types": "^2.0.2",
"main-event": "^1.0.1",
"multiformats": "^13.4.0",
"node-datachannel": "^0.29.0",
"p-defer": "^4.0.1",
"p-event": "^7.0.0",
"p-timeout": "^7.0.0",
Expand All @@ -74,6 +74,7 @@
"protons-runtime": "^5.6.0",
"race-signal": "^2.0.0",
"react-native-webrtc": "^124.0.6",
"stun": "^2.1.0",
"uint8-varint": "^2.0.4",
"uint8arraylist": "^2.4.8",
"uint8arrays": "^5.1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,7 @@ export async function initiateConnection ({ rtcConfiguration, dataChannel, signa
const messageStream = pbStream(stream).pb(Message)
const peerConnection = new RTCPeerConnection(rtcConfiguration)

// make sure C++ peer connection is garbage collected
// https://github.com/murat-dogan/node-datachannel/issues/366#issuecomment-3228453155
peerConnection.addEventListener('connectionstatechange', () => {
switch (peerConnection.connectionState) {
case 'closed':
peerConnection.close()
break
default:
break
}
})

const muxerFactory = new DataChannelMuxerFactory({
// @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
peerConnection,
dataChannelOptions: dataChannel
})
Expand Down Expand Up @@ -209,7 +196,6 @@ export async function initiateConnection ({ rtcConfiguration, dataChannel, signa

return {
remoteAddress: ma,
// @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
peerConnection,
muxerFactory
}
Expand Down
14 changes: 0 additions & 14 deletions packages/transport-webrtc/src/private-to-private/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,19 +199,7 @@ export class WebRTCTransport implements Transport<WebRTCDialEvents>, Startable {
async _onProtocol (stream: Stream, connection: Connection, signal: AbortSignal): Promise<void> {
const peerConnection = new RTCPeerConnection(await getRtcConfiguration(this.init.rtcConfiguration))

// make sure C++ peer connection is garbage collected
// https://github.com/murat-dogan/node-datachannel/issues/366#issuecomment-3228453155
peerConnection.addEventListener('connectionstatechange', () => {
switch (peerConnection.connectionState) {
case 'closed':
peerConnection.close()
break
default:
break
}
})
const muxerFactory = new DataChannelMuxerFactory({
// @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
peerConnection,
dataChannelOptions: this.init.dataChannel
})
Expand All @@ -229,7 +217,6 @@ export class WebRTCTransport implements Transport<WebRTCDialEvents>, Startable {
})

const webRTCConn = toMultiaddrConnection({
// @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
peerConnection,
remoteAddr: remoteAddress,
metrics: this.metrics?.listenerEvents,
Expand All @@ -246,7 +233,6 @@ export class WebRTCTransport implements Transport<WebRTCDialEvents>, Startable {
})

// close the connection on shut down
// @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
this._closeOnShutdown(peerConnection, webRTCConn)
} catch (err: any) {
this.log.error('incoming signaling error - %e', err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { createDialerRTCPeerConnection } from './utils/get-rtcpeerconnection.js'
import { stunListener } from './utils/stun-listener.js'
import type { DataChannelOptions, TransportCertificate } from '../index.js'
import type { WebRTCDirectTransportCertificateEvents } from './transport.js'
import type { DirectRTCPeerConnection } from './utils/get-rtcpeerconnection.js'
import type { StunServer } from './utils/stun-listener.js'
import type { PeerId, ListenerEvents, Listener, Upgrader, ComponentLogger, Logger, CounterGroup, Metrics, PrivateKey } from '@libp2p/interface'
import type { Keychain } from '@libp2p/keychain'
Expand Down Expand Up @@ -58,7 +57,7 @@ export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> impl
private listeningMultiaddr?: Multiaddr
private certificate: TransportCertificate
private stunServer?: StunServer
private readonly connections: Map<string, DirectRTCPeerConnection>
private readonly connections: Map<string, RTCPeerConnection>
private readonly log: Logger
private readonly init: WebRTCDirectListenerInit
private readonly components: WebRTCDirectListenerComponents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createStream } from '../../stream.js'
import { isFirefox } from '../../util.js'
import { generateNoisePrologue } from './generate-noise-prologue.js'
import * as sdp from './sdp.js'
import type { DirectRTCPeerConnection } from './get-rtcpeerconnection.js'
import { extractRemoteFingerprint } from './get-rtcpeerconnection.js'
import type { DataChannelOptions } from '../../index.js'
import type { ComponentLogger, Connection, CounterGroup, Logger, PeerId, PrivateKey, Upgrader } from '@libp2p/interface'
import type { Multiaddr } from '@multiformats/multiaddr'
Expand Down Expand Up @@ -36,13 +36,9 @@ export interface ServerOptions extends ConnectOptions {

const CONNECTION_STATE_CHANGE_EVENT = isFirefox ? 'iceconnectionstatechange' : 'connectionstatechange'

function isServer (options: ClientOptions | ServerOptions, peerConnection: any): peerConnection is DirectRTCPeerConnection {
return options.role === 'server'
}

export async function connect (peerConnection: RTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ClientOptions): Promise<Connection>
export async function connect (peerConnection: DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ServerOptions): Promise<void>
export async function connect (peerConnection: RTCPeerConnection | DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ClientOptions | ServerOptions): Promise<any> {
export async function connect (peerConnection: RTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ServerOptions): Promise<void>
export async function connect (peerConnection: RTCPeerConnection, muxerFactory: DataChannelMuxerFactory, ufrag: string, options: ClientOptions | ServerOptions): Promise<any> {
// create data channel for running the noise handshake. Once the data
// channel is opened, the listener will initiate the noise handshake. This
// is used to confirm the identity of the peer.
Expand Down Expand Up @@ -91,10 +87,10 @@ export async function connect (peerConnection: RTCPeerConnection | DirectRTCPeer

options.log.trace('%s handshake channel opened', options.role)

if (isServer(options, peerConnection)) {
if (options.role === 'server') {
// now that the connection has been opened, add the remote's certhash to
// it's multiaddr so we can complete the noise handshake
const remoteFingerprint = peerConnection.remoteFingerprint()?.value ?? ''
const remoteFingerprint = extractRemoteFingerprint(peerConnection) ?? ''
options.remoteAddr = options.remoteAddr.encapsulate(sdp.fingerprint2Ma(remoteFingerprint))
}

Expand Down Expand Up @@ -127,7 +123,6 @@ export async function connect (peerConnection: RTCPeerConnection | DirectRTCPeer
// Creating the connection before completion of the noise
// handshake ensures that the stream opening callback is set up
const maConn = toMultiaddrConnection({
// @ts-expect-error types are broken
peerConnection,
remoteAddr: options.remoteAddr,
metrics: options.events,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import { DataChannelMuxerFactory } from '../../muxer.ts'
import type { CreateDialerRTCPeerConnectionOptions } from './get-rtcpeerconnection.ts'

/**
* Helper to extract remote fingerprint from RTCPeerConnection
* Used by WebRTC Direct server to get remote certificate info
*/
export function extractRemoteFingerprint (pc: RTCPeerConnection): string | undefined {
if (pc.remoteDescription?.sdp == null) {
return undefined
}

const match = pc.remoteDescription.sdp.match(/a=fingerprint:(\S+)\s+(\S+)/)
if (match != null) {
return match[2] // Return just the fingerprint hash, not the algorithm
}

return undefined
}

export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, options: CreateDialerRTCPeerConnectionOptions = {}): Promise<{ peerConnection: RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> {
// @ts-expect-error options type is wrong
let certificate: RTCCertificate = options.certificate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,102 +1,38 @@
import { Crypto } from '@peculiar/webcrypto'
import { PeerConnection } from 'node-datachannel'
import { RTCPeerConnection } from 'node-datachannel/polyfill'
import { RTCPeerConnection } from '../../webrtc/index.js'
import { DEFAULT_ICE_SERVERS, MAX_MESSAGE_SIZE } from '../../constants.js'
import { DataChannelMuxerFactory } from '../../muxer.ts'
import { generateTransportCertificate } from './generate-certificates.js'
import type { DataChannelOptions, TransportCertificate } from '../../index.js'
import type { CounterGroup } from '@libp2p/interface'
import type { CertificateFingerprint } from 'node-datachannel'

const crypto = new Crypto()

interface DirectRTCPeerConnectionInit extends RTCConfiguration {
ufrag: string
peerConnection: PeerConnection
}

export class DirectRTCPeerConnection extends RTCPeerConnection {
private peerConnection: PeerConnection
private readonly ufrag: string

constructor (init: DirectRTCPeerConnectionInit) {
super(init)

this.peerConnection = init.peerConnection
this.ufrag = init.ufrag

// make sure C++ peer connection is garbage collected
// https://github.com/murat-dogan/node-datachannel/issues/366#issuecomment-3228453155
this.addEventListener('connectionstatechange', () => {
switch (this.connectionState) {
case 'closed':
this.peerConnection.close()
break
default:
break
}
})
}

async createOffer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
// have to set ufrag before creating offer
if (this.connectionState === 'new') {
this.peerConnection?.setLocalDescription('offer', {
iceUfrag: this.ufrag,
icePwd: this.ufrag
})
}

return super.createOffer()
}

async createAnswer (): Promise<globalThis.RTCSessionDescriptionInit | any> {
// have to set ufrag before creating answer
if (this.connectionState === 'new') {
this.peerConnection?.setLocalDescription('answer', {
iceUfrag: this.ufrag,
icePwd: this.ufrag
})
}

return super.createAnswer()
}

remoteFingerprint (): CertificateFingerprint {
if (this.peerConnection == null) {
throw new Error('Invalid state: peer connection not set')
}

return this.peerConnection.remoteFingerprint()
}
}

function mapIceServers (iceServers?: RTCIceServer[]): string[] {
return iceServers
?.map((server) => {
const urls = Array.isArray(server.urls) ? server.urls : [server.urls]

return urls.map((url) => {
if (server.username != null && server.credential != null) {
const [protocol, rest] = url.split(/:(.*)/)
return `${protocol}:${server.username}:${server.credential}@${rest}`
}
return url
})
})
.flat() ?? []
}

export interface CreateDialerRTCPeerConnectionOptions {
rtcConfiguration?: RTCConfiguration | (() => RTCConfiguration | Promise<RTCConfiguration>)
certificate?: TransportCertificate
events?: CounterGroup
dataChannel?: DataChannelOptions
}

export async function createDialerRTCPeerConnection (role: 'client', ufrag: string, options?: CreateDialerRTCPeerConnectionOptions): Promise<{ peerConnection: globalThis.RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }>
export async function createDialerRTCPeerConnection (role: 'server', ufrag: string, options?: CreateDialerRTCPeerConnectionOptions): Promise<{ peerConnection: DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory }>
export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, options: CreateDialerRTCPeerConnectionOptions = {}): Promise<{ peerConnection: globalThis.RTCPeerConnection | DirectRTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> {
/**
* Helper to extract remote fingerprint from RTCPeerConnection
* Used by WebRTC Direct server to get remote certificate info
*/
export function extractRemoteFingerprint (pc: RTCPeerConnection): string | undefined {
if (pc.remoteDescription?.sdp == null) {
return undefined
}

const match = pc.remoteDescription.sdp.match(/a=fingerprint:(\S+)\s+(\S+)/)
if (match != null) {
return match[2] // Return just the fingerprint hash, not the algorithm
}

return undefined
}

export async function createDialerRTCPeerConnection (role: 'client' | 'server', ufrag: string, options: CreateDialerRTCPeerConnectionOptions = {}): Promise<{ peerConnection: RTCPeerConnection, muxerFactory: DataChannelMuxerFactory }> {
if (options.certificate == null) {
// ECDSA is preferred over RSA here. From our testing we find that P-256
// elliptic curve is supported by Pion, webrtc-rs, as well as Chromium
Expand All @@ -114,22 +50,15 @@ export async function createDialerRTCPeerConnection (role: 'client' | 'server',

const rtcConfig = typeof options.rtcConfiguration === 'function' ? await options.rtcConfiguration() : options.rtcConfiguration

const peerConnection = new DirectRTCPeerConnection({
// @roamhq/wrtc uses standard browser-like RTCPeerConnection API
// Certificate is handled differently - wrtc auto-generates certificates
// We'll rely on SDP manipulation for ufrag (done in connect.ts via sdp.munge)
const peerConnection = new RTCPeerConnection({
...rtcConfig,
ufrag,
peerConnection: new PeerConnection(`${role}-${Date.now()}`, {
disableFingerprintVerification: true,
disableAutoNegotiation: true,
certificatePemFile: options.certificate.pem,
keyPemFile: options.certificate.privateKey,
enableIceUdpMux: role === 'server',
maxMessageSize: MAX_MESSAGE_SIZE,
iceServers: mapIceServers(rtcConfig?.iceServers ?? DEFAULT_ICE_SERVERS.map(urls => ({ urls })))
})
iceServers: rtcConfig?.iceServers ?? DEFAULT_ICE_SERVERS.map(urls => ({ urls }))
})

const muxerFactory = new DataChannelMuxerFactory({
// @ts-expect-error https://github.com/murat-dogan/node-datachannel/pull/370
peerConnection,
metrics: options.events,
dataChannelOptions: options.dataChannel
Expand Down
Loading