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

Merged
merged 28 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e2b6537
feat: WebRTC-Direct support for Node.js
achingbrain Jun 7, 2024
40c198a
chore: run interface compliance tests
achingbrain Jun 11, 2024
b572498
Merge remote-tracking branch 'origin/main' into feat/webrtc-direct-in…
achingbrain Jun 14, 2024
03044e0
chore: use libjuice for udp muxing
achingbrain Jun 14, 2024
1e052de
chore: allow function based config
achingbrain Jun 14, 2024
71bf4cb
Merge remote-tracking branch 'origin/main' into feat/webrtc-direct-in…
achingbrain Jun 18, 2024
f58bf18
Merge remote-tracking branch 'origin/main' into feat/webrtc-direct-in…
achingbrain Jan 14, 2025
7139c5a
chore: tests starting to pass
achingbrain Jan 16, 2025
cf56f63
chore: tests passing
achingbrain Jan 21, 2025
4c36a5f
chore: update node-datachannel api
achingbrain Jan 27, 2025
e08ad8a
chore: update to client/server nomenclature
achingbrain Feb 6, 2025
c7fec03
fix: update dialer sig
achingbrain Feb 7, 2025
ca0251f
chore: use node-datachannel fork
achingbrain Feb 10, 2025
d76b6ac
Merge branch 'main' into feat/webrtc-direct-in-node-js
achingbrain Feb 10, 2025
ffb2c89
chore: close stream
achingbrain Feb 10, 2025
e05910e
chore: remove exit
achingbrain Feb 10, 2025
dcd1af2
chore: add missing interop tests
achingbrain Feb 10, 2025
8bb99b7
chore: add missing interop tests
achingbrain Feb 10, 2025
9f40522
chore: fix segfault
achingbrain Feb 11, 2025
44b6093
chore: disable logging
achingbrain Feb 11, 2025
88f41b7
chore: enable muxing and update dep
achingbrain Feb 11, 2025
f0325d3
chore: remove logging
achingbrain Feb 11, 2025
cf9e076
chore: fix interop
achingbrain Feb 11, 2025
b7ab8d9
chore: update deps
achingbrain Feb 12, 2025
85f9862
chore: adhere to timeout
achingbrain Feb 12, 2025
fc8893f
chore: fix build
achingbrain Feb 12, 2025
9134a46
chore: make arg string
achingbrain Feb 12, 2025
4ebf4a3
chore: make arg string
achingbrain Feb 12, 2025
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 interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ $ docker build . -f ./interop/BrowserDockerfile -t js-libp2p-browsers
- When starting the docker container add `-e GOLOG_LOG_LEVEL=debug`
4. Build the version you want to test against
```console
$ cd multidim-interop/impl/$IMPL/$VERSION
$ cd transport-interop/impl/$IMPL/$VERSION
$ make
...
```
Expand Down
2 changes: 1 addition & 1 deletion interop/firefox-version.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
}
],
"secureChannels": ["noise"],
"muxers": ["mplex", "yamux"]
"muxers": ["yamux", "mplex"]
}
33 changes: 15 additions & 18 deletions interop/node-version.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
{
"id": "node-js-libp2p-head",
"containerImageID": "node-js-libp2p-head",
"transports": [
"tcp",
"ws",
{
"name": "wss",
"onlyDial": true
}
],
"secureChannels": [
"noise"
],
"muxers": [
"mplex",
"yamux"
]
}
"id": "node-js-libp2p-head",
"containerImageID": "node-js-libp2p-head",
"transports": [
"tcp",
"ws",
{
"name": "wss",
"onlyDial": true
},
"webrtc",
"webrtc-direct"
],
"secureChannels": ["noise"],
"muxers": ["yamux", "mplex"]
}
10 changes: 6 additions & 4 deletions interop/test/dialer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import type { Libp2p } from '@libp2p/interface'
import type { PingService } from '@libp2p/ping'

const isDialer: boolean = process.env.is_dialer === 'true'
const timeoutSecs: string = process.env.test_timeout_secs ?? '180'
const timeoutMs: number = parseInt(process.env.test_timeout_secs ?? '180') * 1000

describe('ping test (dialer)', function () {
if (!isDialer) {
return
}

// make the default timeout longer than the listener timeout
this.timeout((parseInt(timeoutSecs) * 1000) + 30000)
this.timeout(timeoutMs + 30_000)
let node: Libp2p<{ ping: PingService }>

beforeEach(async () => {
Expand All @@ -32,7 +32,7 @@ describe('ping test (dialer)', function () {
})

it('should dial and ping', async function () {
let [, otherMaStr]: string[] = await redisProxy(['BLPOP', 'listenerAddr', timeoutSecs])
let [, otherMaStr]: string[] = await redisProxy(['BLPOP', 'listenerAddr', `${timeoutMs / 1000}`])

// Hack until these are merged:
// - https://github.com/multiformats/js-multiaddr-to-uri/pull/120
Expand All @@ -45,7 +45,9 @@ describe('ping test (dialer)', function () {
await node.dial(otherMa)

console.error(`node ${node.peerId.toString()} pings: ${otherMa}`)
const pingRTT = await node.services.ping.ping(multiaddr(otherMa))
const pingRTT = await node.services.ping.ping(multiaddr(otherMa), {
signal: AbortSignal.timeout(timeoutMs)
})
const handshakePlusOneRTT = Date.now() - handshakeStartInstant
console.log(JSON.stringify({
handshakePlusOneRTTMillis: handshakePlusOneRTT,
Expand Down
1 change: 1 addition & 0 deletions interop/test/listener.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe('ping test (listener)', function () {
}

console.error('inform redis of dial address')
console.error(multiaddrs)
// Send the listener addr over the proxy server so this works on both the Browser and Node
await redisProxy(['RPUSH', 'listenerAddr', multiaddrs[0]])
// Wait
Expand Down
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
37 changes: 16 additions & 21 deletions packages/transport-webrtc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,6 @@ A WebRTC Direct multiaddr also includes a certhash of the target peer - this is

In both cases, once the connection is established a [Noise handshake](https://noiseprotocol.org/noise.html) is carried out to ensure that the remote peer has the private key that corresponds to the public key that makes up their PeerId, giving you both encryption and authentication.

## Support

WebRTC is supported in both Node.js and browsers.

At the time of writing, WebRTC Direct is dial-only in browsers and not supported in Node.js at all.

Support in Node.js is possible but PRs will need to be opened to [libdatachannel](https://github.com/paullouisageneau/libdatachannel) and the appropriate APIs exposed in [node-datachannel](https://github.com/murat-dogan/node-datachannel).

WebRTC Direct support is available in rust-libp2p and arriving soon in go-libp2p.

See the WebRTC section of <https://connectivity.libp2p.io> for more information.

## Example - WebRTC

WebRTC requires use of a relay to connect two nodes. The listener first discovers a relay server and makes a reservation, then the dialer can connect via the relayed address.
Expand Down Expand Up @@ -180,26 +168,33 @@ The only implementation that supports a WebRTC Direct listener is go-libp2p and

```TypeScript
import { createLibp2p } from 'libp2p'
import { noise } from '@chainsafe/libp2p-noise'
import { multiaddr } from '@multiformats/multiaddr'
import { pipe } from 'it-pipe'
import { fromString, toString } from 'uint8arrays'
import { webRTCDirect } from '@libp2p/webrtc'

const node = await createLibp2p({
const listener = await createLibp2p({
addresses: {
listen: [
'/ip4/0.0.0.0/udp/0/webrtc-direct'
]
},
transports: [
webRTCDirect()
]
})

await listener.start()

const dialer = await createLibp2p({
transports: [
webRTCDirect()
],
connectionEncrypters: [
noise()
]
})

await node.start()
await dialer.start()

// this multiaddr corresponds to a remote node running a WebRTC Direct listener
const ma = multiaddr('/ip4/0.0.0.0/udp/56093/webrtc-direct/certhash/uEiByaEfNSLBexWBNFZy_QB1vAKEj7JAXDizRs4_SnTflsQ')
const stream = await node.dialProtocol(ma, '/my-protocol/1.0.0', {
const stream = await dialer.dialProtocol(listener.getMultiaddrs(), '/my-protocol/1.0.0', {
signal: AbortSignal.timeout(10_000)
})

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 @@ -40,7 +40,7 @@
"generate": "protons src/private-to-private/pb/message.proto src/pb/message.proto",
"build": "aegir build",
"test": "aegir test -t node -t browser",
"test:node": "aegir test -t node --cov -- --exit",
"test:node": "aegir test -t node --cov",
"test:chrome": "aegir test -t browser --cov",
"test:firefox": "aegir test -t browser -- --browser firefox",
"test:webkit": "aegir test -t browser -- --browser webkit",
Expand All @@ -51,27 +51,35 @@
"doc-check": "aegir doc-check"
},
"dependencies": {
"@chainsafe/is-ip": "^2.0.2",
"@chainsafe/libp2p-noise": "^16.0.0",
"@ipshipyard/node-datachannel": "^0.26.4",
"@libp2p/interface": "^2.5.0",
"@libp2p/interface-internal": "^2.3.0",
"@libp2p/peer-id": "^5.0.12",
"@libp2p/utils": "^6.5.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",
"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 @@ -91,7 +99,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
4 changes: 4 additions & 0 deletions packages/transport-webrtc/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ export const DEFAULT_ICE_SERVERS = [
'stun:stun.cloudflare.com:3478',
'stun:stun.services.mozilla.com:3478'
]

export const UFRAG_ALPHABET = Array.from('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')

export const UFRAG_PREFIX = 'libp2p+webrtc+v1/'
Loading
Loading