Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: WebRTC-Direct support for Node.js #2583

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import tests from '@libp2p/interface-compliance-tests/transport'
import { webRTCDirect } from '@libp2p/webrtc'
import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
import { isNode, isElectron } from 'wherearewe'

describe('WebRTC-Direct interface-transport compliance', () => {
if (!isNode && !isElectron) {
return
}

tests({
async setup () {
const dialer = {
transports: [
webRTCDirect()
],
connectionMonitor: {
enabled: false
}
}

return {
dialer,
listener: {
addresses: {
listen: [
'/ip4/127.0.0.1/udp/0/webrtc-direct',
'/ip4/127.0.0.1/udp/0/webrtc-direct'
]
},
...dialer
},
dialMultiaddrMatcher: WebRTCDirect,
listenMultiaddrMatcher: WebRTCDirect
}
},
async teardown () {}
})
})
13 changes: 10 additions & 3 deletions packages/integration-tests/test/interop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { mplex } from '@libp2p/mplex'
import { plaintext } from '@libp2p/plaintext'
import { tcp } from '@libp2p/tcp'
import { tls } from '@libp2p/tls'
import { webRTCDirect } from '@libp2p/webrtc'
import { multiaddr } from '@multiformats/multiaddr'
import { execa } from 'execa'
import { path as p2pd } from 'go-libp2p'
Expand Down Expand Up @@ -131,20 +132,26 @@ async function createJsPeer (options: SpawnOptions): Promise<Daemon> {
addresses: {
listen: []
},
transports: [tcp(), circuitRelayTransport()],
transports: [
tcp(),
circuitRelayTransport(),
webRTCDirect()
],
streamMuxers: [],
connectionEncrypters: [noise()]
}

if (options.noListen !== true) {
if (options.transport == null || options.transport === 'tcp') {
opts.addresses?.listen?.push('/ip4/127.0.0.1/tcp/0')
} else if (options.transport === 'webrtc-direct') {
opts.addresses?.listen?.push('/ip4/127.0.0.1/udp/0/webrtc-direct')
} else {
throw new UnsupportedError()
}
}

if (options.transport === 'webtransport' || options.transport === 'webrtc-direct') {
if (options.transport === 'webtransport') {
throw new UnsupportedError()
}

Expand Down Expand Up @@ -191,7 +198,7 @@ async function createJsPeer (options: SpawnOptions): Promise<Daemon> {
services
})

const server = createServer(multiaddr('/ip4/0.0.0.0/tcp/0'), node)
const server = createServer(multiaddr('/ip4/127.0.0.1/tcp/0'), node)
await server.start()

return {
Expand Down
9 changes: 7 additions & 2 deletions packages/interface-compliance-tests/src/transport/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,13 @@ export default (common: TestSetup<TransportTestFixtures>): void => {
throw new Error('Oh noes!')
}

await expect(dialer.dial(dialAddrs[0])).to.eventually.be.rejected
.with.property('name', 'EncryptionFailedError')
// transports with their own muxers/encryption will perform the upgrade
// after the connection has been established (e.g. peer ids have been
// exchanged) so perform the dial and wait for the remote to attempt the
// upgrade - if it fails the listener should close the underlying
// connection which should remove the it from the dialer's connection map
await dialer.dial(dialAddrs[0]).catch(() => {})
await delay(1000)

expect(dialer.getConnections().filter(conn => {
return dialMultiaddrMatcher.exactMatch(conn.remoteAddr)
Expand Down
17 changes: 14 additions & 3 deletions packages/transport-webrtc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"scripts": {
"generate": "protons src/private-to-private/pb/message.proto src/pb/message.proto",
"build": "aegir build",
"test": "aegir test -t node -t browser",
"test": "aegir test -t node -t browser -- --exit",
"test:node": "aegir test -t node --cov -- --exit",
"test:chrome": "aegir test -t browser --cov",
"test:firefox": "aegir test -t browser -- --browser firefox",
Expand All @@ -50,27 +50,35 @@
"doc-check": "aegir doc-check"
},
"dependencies": {
"@chainsafe/is-ip": "^2.0.2",
"@chainsafe/libp2p-noise": "^16.0.0",
"@libp2p/interface": "^2.4.0",
"@libp2p/interface-internal": "^2.2.2",
"@libp2p/peer-id": "^5.0.10",
"@libp2p/utils": "^6.3.1",
"@multiformats/multiaddr": "^12.3.3",
"@multiformats/multiaddr-matcher": "^1.6.0",
"@peculiar/webcrypto": "^1.5.0",
"@peculiar/x509": "^1.11.0",
"any-signal": "^4.1.1",
"detect-browser": "^5.3.0",
"get-port": "^7.1.0",
"it-length-prefixed": "^9.1.0",
"it-protobuf-stream": "^1.1.5",
"it-pushable": "^3.2.3",
"it-stream-types": "^2.0.2",
"multiformats": "^13.3.1",
"node-datachannel": "^0.11.0",
"node-datachannel": "^0.24.0",
"p-defer": "^4.0.1",
"p-event": "^6.0.1",
"p-timeout": "^6.1.3",
"p-wait-for": "^5.0.2",
"progress-events": "^1.0.1",
"protons-runtime": "^5.5.0",
"race-event": "^1.3.0",
"race-signal": "^1.1.0",
"react-native-webrtc": "^124.0.4",
"stun": "^2.1.0",
"uint8-varint": "^2.0.4",
"uint8arraylist": "^2.4.8",
"uint8arrays": "^5.1.0"
Expand All @@ -90,7 +98,10 @@
"sinon-ts": "^2.0.0"
},
"browser": {
"./dist/src/webrtc/index.js": "./dist/src/webrtc/index.browser.js"
"./dist/src/webrtc/index.js": "./dist/src/webrtc/index.browser.js",
"./dist/src/private-to-public/listener.js": "./dist/src/private-to-public/listener.browser.js",
"./dist/src/private-to-public/utils/get-rtcpeerconnection.js": "./dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js",
"node:net": false
},
"react-native": {
"./dist/src/webrtc/index.js": "./dist/src/webrtc/index.react-native.js"
Expand Down
19 changes: 19 additions & 0 deletions packages/transport-webrtc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,25 @@ export interface DataChannelOptions {
openTimeout?: number
}

/**
* PEM format server certificate and private key
*/
export interface TransportCertificate {
/**
* The private key for the certificate in PEM format
*/
privateKey: string
/**
* PEM format certificate
*/
pem: string

/**
* The hash of the certificate
*/
certhash: string
}

export type { WebRTCTransportDirectInit, WebRTCDirectTransportComponents }

function webRTCDirect (init?: WebRTCTransportDirectInit): (components: WebRTCDirectTransportComponents) => Transport {
Expand Down
7 changes: 4 additions & 3 deletions packages/transport-webrtc/src/maconn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,13 @@ export class WebRTCMultiaddrConnection implements MultiaddrConnection {
this.timeline = init.timeline
this.peerConnection = init.peerConnection

const initialState = this.peerConnection.connectionState
const peerConnection = this.peerConnection
const initialState = peerConnection.connectionState

this.peerConnection.onconnectionstatechange = () => {
this.log.trace('peer connection state change', this.peerConnection.connectionState, 'initial state', initialState)
this.log.trace('peer connection state change', peerConnection.connectionState, 'initial state', initialState)

if (this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed' || this.peerConnection.connectionState === 'closed') {
if (peerConnection.connectionState === 'disconnected' || peerConnection.connectionState === 'failed' || peerConnection.connectionState === 'closed') {
// nothing else to do but close the connection
this.timeline.close = Date.now()
}
Expand Down
4 changes: 2 additions & 2 deletions packages/transport-webrtc/src/muxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory {
this.metrics = init.metrics
this.protocol = init.protocol ?? PROTOCOL
this.dataChannelOptions = init.dataChannelOptions ?? {}
this.log = components.logger.forComponent('libp2p:webrtc:datachannelmuxerfactory')
this.log = components.logger.forComponent('libp2p:webrtc:muxerfactory')

// store any datachannels opened before upgrade has been completed
this.peerConnection.ondatachannel = ({ channel }) => {
Expand Down Expand Up @@ -243,7 +243,7 @@ export class DataChannelMuxer implements StreamMuxer {
sink: Sink<Source<Uint8Array | Uint8ArrayList>, Promise<void>> = nopSink

newStream (): Stream {
// The spec says the label SHOULD be an empty string: https://github.com/libp2p/specs/blob/master/webrtc/README.md#rtcdatachannel-label
// The spec says the label MUST be an empty string: https://github.com/libp2p/specs/blob/master/webrtc/README.md#rtcdatachannel-label
const channel = this.peerConnection.createDataChannel('')
// lib-datachannel throws if `.getId` is called on a closed channel so
// memoize it
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const UFRAG_PREFIX = 'libp2p+webrtc+v1/'

// https://gist.github.com/mondain/b0ec1cf5f60ae726202e
export const DEFAULT_STUN_SERVERS = [
'stun.l.google.com:19302',
'global.stun.twilio.com:3478',
'stun.cloudflare.com:3478'
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TypedEventEmitter } from '@libp2p/interface'
import { UnimplementedError } from '../error.js'
import type { PeerId, ListenerEvents, Listener } from '@libp2p/interface'
import type { TransportManager } from '@libp2p/interface-internal'
import type { Multiaddr } from '@multiformats/multiaddr'

export interface WebRTCDirectListenerComponents {
peerId: PeerId
transportManager: TransportManager
}

export interface WebRTCDirectListenerInit {
shutdownController: AbortController
}

export class WebRTCDirectListener extends TypedEventEmitter<ListenerEvents> implements Listener {
async listen (): Promise<void> {
throw new UnimplementedError('WebRTCTransport.createListener')
}

getAddrs (): Multiaddr[] {
return []
}

async close (): Promise<void> {

}
}
Loading
Loading