Skip to content

Commit 200c2bd

Browse files
authored
feat: WebRTC-Direct support for Node.js (#2583)
Supports listening on and dialing WebRTC-Direct multiaddrs in Node.js. Closes: - #2581
1 parent b030ead commit 200c2bd

39 files changed

+1246
-483
lines changed

interop/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ $ docker build . -f ./interop/BrowserDockerfile -t js-libp2p-browsers
7575
- When starting the docker container add `-e GOLOG_LOG_LEVEL=debug`
7676
4. Build the version you want to test against
7777
```console
78-
$ cd multidim-interop/impl/$IMPL/$VERSION
78+
$ cd transport-interop/impl/$IMPL/$VERSION
7979
$ make
8080
...
8181
```

interop/firefox-version.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
}
1414
],
1515
"secureChannels": ["noise"],
16-
"muxers": ["mplex", "yamux"]
16+
"muxers": ["yamux", "mplex"]
1717
}

interop/node-version.json

+15-18
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
{
2-
"id": "node-js-libp2p-head",
3-
"containerImageID": "node-js-libp2p-head",
4-
"transports": [
5-
"tcp",
6-
"ws",
7-
{
8-
"name": "wss",
9-
"onlyDial": true
10-
}
11-
],
12-
"secureChannels": [
13-
"noise"
14-
],
15-
"muxers": [
16-
"mplex",
17-
"yamux"
18-
]
19-
}
2+
"id": "node-js-libp2p-head",
3+
"containerImageID": "node-js-libp2p-head",
4+
"transports": [
5+
"tcp",
6+
"ws",
7+
{
8+
"name": "wss",
9+
"onlyDial": true
10+
},
11+
"webrtc",
12+
"webrtc-direct"
13+
],
14+
"secureChannels": ["noise"],
15+
"muxers": ["yamux", "mplex"]
16+
}

interop/test/dialer.spec.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import type { Libp2p } from '@libp2p/interface'
88
import type { PingService } from '@libp2p/ping'
99

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

1313
describe('ping test (dialer)', function () {
1414
if (!isDialer) {
1515
return
1616
}
1717

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

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

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

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

4747
console.error(`node ${node.peerId.toString()} pings: ${otherMa}`)
48-
const pingRTT = await node.services.ping.ping(multiaddr(otherMa))
48+
const pingRTT = await node.services.ping.ping(multiaddr(otherMa), {
49+
signal: AbortSignal.timeout(timeoutMs)
50+
})
4951
const handshakePlusOneRTT = Date.now() - handshakeStartInstant
5052
console.log(JSON.stringify({
5153
handshakePlusOneRTTMillis: handshakePlusOneRTT,

interop/test/listener.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ describe('ping test (listener)', function () {
6767
}
6868

6969
console.error('inform redis of dial address')
70+
console.error(multiaddrs)
7071
// Send the listener addr over the proxy server so this works on both the Browser and Node
7172
await redisProxy(['RPUSH', 'listenerAddr', multiaddrs[0]])
7273
// Wait
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import tests from '@libp2p/interface-compliance-tests/transport'
2+
import { webRTCDirect } from '@libp2p/webrtc'
3+
import { WebRTCDirect } from '@multiformats/multiaddr-matcher'
4+
import { isNode, isElectron } from 'wherearewe'
5+
6+
describe('WebRTC-Direct interface-transport compliance', () => {
7+
if (!isNode && !isElectron) {
8+
return
9+
}
10+
11+
tests({
12+
async setup () {
13+
const dialer = {
14+
transports: [
15+
webRTCDirect()
16+
],
17+
connectionMonitor: {
18+
enabled: false
19+
}
20+
}
21+
22+
return {
23+
dialer,
24+
listener: {
25+
addresses: {
26+
listen: [
27+
'/ip4/127.0.0.1/udp/0/webrtc-direct',
28+
'/ip4/127.0.0.1/udp/0/webrtc-direct'
29+
]
30+
},
31+
...dialer
32+
},
33+
dialMultiaddrMatcher: WebRTCDirect,
34+
listenMultiaddrMatcher: WebRTCDirect
35+
}
36+
},
37+
async teardown () {}
38+
})
39+
})

packages/integration-tests/test/interop.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { mplex } from '@libp2p/mplex'
1515
import { plaintext } from '@libp2p/plaintext'
1616
import { tcp } from '@libp2p/tcp'
1717
import { tls } from '@libp2p/tls'
18+
import { webRTCDirect } from '@libp2p/webrtc'
1819
import { multiaddr } from '@multiformats/multiaddr'
1920
import { execa } from 'execa'
2021
import { path as p2pd } from 'go-libp2p'
@@ -131,20 +132,26 @@ async function createJsPeer (options: SpawnOptions): Promise<Daemon> {
131132
addresses: {
132133
listen: []
133134
},
134-
transports: [tcp(), circuitRelayTransport()],
135+
transports: [
136+
tcp(),
137+
circuitRelayTransport(),
138+
webRTCDirect()
139+
],
135140
streamMuxers: [],
136141
connectionEncrypters: [noise()]
137142
}
138143

139144
if (options.noListen !== true) {
140145
if (options.transport == null || options.transport === 'tcp') {
141146
opts.addresses?.listen?.push('/ip4/127.0.0.1/tcp/0')
147+
} else if (options.transport === 'webrtc-direct') {
148+
opts.addresses?.listen?.push('/ip4/127.0.0.1/udp/0/webrtc-direct')
142149
} else {
143150
throw new UnsupportedError()
144151
}
145152
}
146153

147-
if (options.transport === 'webtransport' || options.transport === 'webrtc-direct') {
154+
if (options.transport === 'webtransport') {
148155
throw new UnsupportedError()
149156
}
150157

@@ -191,7 +198,7 @@ async function createJsPeer (options: SpawnOptions): Promise<Daemon> {
191198
services
192199
})
193200

194-
const server = createServer(multiaddr('/ip4/0.0.0.0/tcp/0'), node)
201+
const server = createServer(multiaddr('/ip4/127.0.0.1/tcp/0'), node)
195202
await server.start()
196203

197204
return {

packages/interface-compliance-tests/src/transport/index.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,13 @@ export default (common: TestSetup<TransportTestFixtures>): void => {
191191
throw new Error('Oh noes!')
192192
}
193193

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

197202
expect(dialer.getConnections().filter(conn => {
198203
return dialMultiaddrMatcher.exactMatch(conn.remoteAddr)

packages/transport-webrtc/README.md

+16-21
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,6 @@ A WebRTC Direct multiaddr also includes a certhash of the target peer - this is
4444

4545
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.
4646

47-
## Support
48-
49-
WebRTC is supported in both Node.js and browsers.
50-
51-
At the time of writing, WebRTC Direct is dial-only in browsers and not supported in Node.js at all.
52-
53-
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).
54-
55-
WebRTC Direct support is available in rust-libp2p and arriving soon in go-libp2p.
56-
57-
See the WebRTC section of <https://connectivity.libp2p.io> for more information.
58-
5947
## Example - WebRTC
6048

6149
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.
@@ -180,26 +168,33 @@ The only implementation that supports a WebRTC Direct listener is go-libp2p and
180168

181169
```TypeScript
182170
import { createLibp2p } from 'libp2p'
183-
import { noise } from '@chainsafe/libp2p-noise'
184171
import { multiaddr } from '@multiformats/multiaddr'
185172
import { pipe } from 'it-pipe'
186173
import { fromString, toString } from 'uint8arrays'
187174
import { webRTCDirect } from '@libp2p/webrtc'
188175

189-
const node = await createLibp2p({
176+
const listener = await createLibp2p({
177+
addresses: {
178+
listen: [
179+
'/ip4/0.0.0.0/udp/0/webrtc-direct'
180+
]
181+
},
182+
transports: [
183+
webRTCDirect()
184+
]
185+
})
186+
187+
await listener.start()
188+
189+
const dialer = await createLibp2p({
190190
transports: [
191191
webRTCDirect()
192-
],
193-
connectionEncrypters: [
194-
noise()
195192
]
196193
})
197194

198-
await node.start()
195+
await dialer.start()
199196

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

packages/transport-webrtc/package.json

+14-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"generate": "protons src/private-to-private/pb/message.proto src/pb/message.proto",
4141
"build": "aegir build",
4242
"test": "aegir test -t node -t browser",
43-
"test:node": "aegir test -t node --cov -- --exit",
43+
"test:node": "aegir test -t node --cov",
4444
"test:chrome": "aegir test -t browser --cov",
4545
"test:firefox": "aegir test -t browser -- --browser firefox",
4646
"test:webkit": "aegir test -t browser -- --browser webkit",
@@ -51,27 +51,35 @@
5151
"doc-check": "aegir doc-check"
5252
},
5353
"dependencies": {
54+
"@chainsafe/is-ip": "^2.0.2",
5455
"@chainsafe/libp2p-noise": "^16.0.0",
56+
"@ipshipyard/node-datachannel": "^0.26.4",
5557
"@libp2p/interface": "^2.5.0",
5658
"@libp2p/interface-internal": "^2.3.0",
5759
"@libp2p/peer-id": "^5.0.12",
5860
"@libp2p/utils": "^6.5.1",
5961
"@multiformats/multiaddr": "^12.3.3",
6062
"@multiformats/multiaddr-matcher": "^1.6.0",
63+
"@peculiar/webcrypto": "^1.5.0",
64+
"@peculiar/x509": "^1.11.0",
65+
"any-signal": "^4.1.1",
6166
"detect-browser": "^5.3.0",
67+
"get-port": "^7.1.0",
6268
"it-length-prefixed": "^9.1.0",
6369
"it-protobuf-stream": "^1.1.5",
6470
"it-pushable": "^3.2.3",
6571
"it-stream-types": "^2.0.2",
6672
"multiformats": "^13.3.1",
67-
"node-datachannel": "^0.11.0",
6873
"p-defer": "^4.0.1",
6974
"p-event": "^6.0.1",
7075
"p-timeout": "^6.1.3",
76+
"p-wait-for": "^5.0.2",
7177
"progress-events": "^1.0.1",
7278
"protons-runtime": "^5.5.0",
79+
"race-event": "^1.3.0",
7380
"race-signal": "^1.1.0",
7481
"react-native-webrtc": "^124.0.4",
82+
"stun": "^2.1.0",
7583
"uint8-varint": "^2.0.4",
7684
"uint8arraylist": "^2.4.8",
7785
"uint8arrays": "^5.1.0"
@@ -91,7 +99,10 @@
9199
"sinon-ts": "^2.0.0"
92100
},
93101
"browser": {
94-
"./dist/src/webrtc/index.js": "./dist/src/webrtc/index.browser.js"
102+
"./dist/src/webrtc/index.js": "./dist/src/webrtc/index.browser.js",
103+
"./dist/src/private-to-public/listener.js": "./dist/src/private-to-public/listener.browser.js",
104+
"./dist/src/private-to-public/utils/get-rtcpeerconnection.js": "./dist/src/private-to-public/utils/get-rtcpeerconnection.browser.js",
105+
"node:net": false
95106
},
96107
"react-native": {
97108
"./dist/src/webrtc/index.js": "./dist/src/webrtc/index.react-native.js"

packages/transport-webrtc/src/constants.ts

+4
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ export const DEFAULT_ICE_SERVERS = [
1212
'stun:stun.cloudflare.com:3478',
1313
'stun:stun.services.mozilla.com:3478'
1414
]
15+
16+
export const UFRAG_ALPHABET = Array.from('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
17+
18+
export const UFRAG_PREFIX = 'libp2p+webrtc+v1/'

0 commit comments

Comments
 (0)