-
Notifications
You must be signed in to change notification settings - Fork 459
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
WebRTC transport apparently broken in the NodeJS environment #2425
Comments
This is a simplified version of your script that fixes type errors and removes a lot of unnecessary listeners/services/setup: import { noise } from "@chainsafe/libp2p-noise";
import { yamux } from "@chainsafe/libp2p-yamux";
import { webRTC } from "@libp2p/webrtc";
import { webSockets } from "@libp2p/websockets";
import { createLibp2p } from "libp2p";
import { circuitRelayTransport, circuitRelayServer } from "@libp2p/circuit-relay-v2";
import { identify } from "@libp2p/identify";
import * as filters from '@libp2p/websockets/filters'
import { multiaddr } from '@multiformats/multiaddr'
import { Buffer } from 'buffer';
import { lpStream } from 'it-length-prefixed-stream'
import { Uint8ArrayList } from 'uint8arraylist'
import type { LengthPrefixedStream } from 'it-length-prefixed-stream'
import type { Multiaddr } from '@multiformats/multiaddr'
import type { IncomingStreamData } from '@libp2p/interface/src/stream-handler'
import type { Connection, Libp2p, Stream } from '@libp2p/interface'
async function main() {
// create three nodes, one "server" (listening node) and two "browsers" (non-listening)
const server = await createLibp2p({
addresses: {listen: ['/ip4/127.0.0.1/tcp/31337/ws']},
transports: [
webSockets({filter: filters.all})
],
connectionEncryption: [noise()],
streamMuxers: [yamux()],
services: {
identify: identify(),
relay: circuitRelayServer(),
},
});
// @ts-expect-error not a field
server['name'] = "server"; // just for test output
const browser1 = await createLibp2p({
addresses: {listen: ['/webrtc']},
transports: [
webSockets({filter: filters.all}),
webRTC(),
circuitRelayTransport({
discoverRelays: 1
}),
],
connectionEncryption: [noise()],
streamMuxers: [yamux()],
services: {
identify: identify(),
}
});
// @ts-expect-error not a field
browser1['name'] = "browser1"; // just for test output
await browser1.handle("/verity/1.0.0",
(incomingStreamData: IncomingStreamData) => handleIncoming(incomingStreamData, browser1));
const browser2 = await createLibp2p({
addresses: {listen: ['/webrtc']},
transports: [
webSockets({filter: filters.all}),
webRTC(),
circuitRelayTransport({
discoverRelays: 1
}),
],
connectionEncryption: [noise()],
streamMuxers: [yamux()],
services: {
identify: identify(),
}
});
// @ts-expect-error not a field
browser2['name'] = "browser2"; // just for test output
await browser2.start();
await browser2.handle("/verity/1.0.0",
(incomingStreamData: IncomingStreamData) => handleIncoming(incomingStreamData, browser2));
// connect both "browsers" to the server
await Promise.all([
browser1.dial(multiaddr('/ip4/127.0.0.1/tcp/31337/ws')),
browser2.dial(multiaddr('/ip4/127.0.0.1/tcp/31337/ws'))
]);
assert(server.getConnections().length == 2);
assert(browser1.getConnections().length == 1);
assert(browser2.getConnections().length == 1);
// both browsers should now have a dialable p2p-circuit/webRTC addr
const browser1dialable: Multiaddr = await findWebRTCAddress(browser1);
console.log("browser1's dialable address is: " + browser1dialable)
const browser2dialable: Multiaddr = await findWebRTCAddress(browser2);
console.log("browser2's dialable address is: " + browser2dialable)
// connect browser1 to browser2
const b1ToB2: Connection = await browser1.dial(browser2dialable);
assert(b1ToB2.transient === false);
assert(b1ToB2.status === "open");
const b1b2rawstream: Stream = await b1ToB2.newStream("/verity/1.0.0");
const b1tob2stream: LengthPrefixedStream = lpStream(b1b2rawstream);
console.log("Sending message from browser1 to browser2");
await b1tob2stream.write(Buffer.from("Message from browser1 while server still connected", 'utf8'));
await new Promise(resolve => setTimeout(resolve, 100)); // give it some time
console.log("Stopping server");
await server.stop();
console.log("Sending message from browser1 to browser2 after server stopped");
await b1tob2stream.write(Buffer.from("Message from browser1 after server disconnected", 'utf8'));
await new Promise(resolve => setTimeout(resolve, 1000)); // give it some time
console.log("teardown: stopping browser nodes");
await browser1.stop();
await browser2.stop();
console.log("end of main, NodeJS should terminate now");
}
async function handleIncoming(incomingStreamData: IncomingStreamData, node: any) {
console.log("handling incoming stream at " + node.name)
const stream: LengthPrefixedStream = lpStream(incomingStreamData.stream);
let msg: Uint8ArrayList;
try {
while (msg = await stream.read()) {
const msgBuf: Buffer = Buffer.from(
msg.subarray() // Note: Here, subarray() re-assembles a message which
// may have been received in many fragments.
);
console.log(`Message received at ${node.name}: ${msgBuf.toString('utf8')}`);
}
} catch(error) {
console.log("error reading stream at " + node.name + ": " + error);
}
console.log("stopping to handle stream at " + node.name)
}
function assert(assertion: boolean) {
if (!assertion) throw Error("assertion failed");
}
async function findWebRTCAddress (node: Libp2p): Promise<Multiaddr> {
while (true) {
// check for a dialable p2p-circuit/webRTC addr
for (const multiaddr of node.getMultiaddrs()) {
const protos: string[] = multiaddr.protoNames();
if (protos.includes("p2p") && protos.includes("p2p-circuit") && protos.includes("webrtc")) {
return multiaddr;
}
}
// not found relay yet, wait a bit
await new Promise<void>(resolve => {
setTimeout(() => {
resolve()
}, 1000)
})
}
}
main(); Notably it awaits the promise from the second write to When it runs I see:
So the second message is received after the relay server is stopped, however the process still fails to exit. Running with why-is-node-running reveals a thread safe callback reference in |
|
Oops, seems like we needed more information for this issue, please comment with more details or this issue will be closed in 7 days. |
Hi Alex, thank you very much for looking into this, and of course for the fixed release. I can confirm the simplified code works as expected, while my more convoluted example does not. Will try to pinpoint the cause of that, as obviously our application code is even more complex than the example.
|
@achingbrain It appears the second message ("Message from browser1 after server disconnected") fails to arrive when I also activate the webRTC transport on the server node. Can you verify this on this end?
|
Oops, seems like we needed more information for this issue, please comment with more details or this issue will be closed in 7 days. |
This issue was closed because it is missing author input. |
Please reopen; issue is neither completed nor was it stale. |
I think I found the problem, we were using the cleanup function from It's an escape hatch that destroys all C++ objects associated with RTCPeerConnections or RTCDataChannels so not really great for a server. That said, the problem appears in the script above because the relay and the two clients all run in the same process - I don't know how realistic this is as a deployment target. Either way we shouldn't be calling this function, This will be fixed in #2498 |
@achingbrain I just noticed I never gave any feedback -- apologies for that. I can confirm the fix works as expected, so again, thank you very much for taking the time to look into this 👍 |
WebRTC / circuit-relay-transport
Severity:
Medium
Description:
The WebRTC transport does not seem to function correctly within the NodeJS environment. While it is obviously intended for the browser environment and not NodeJS, that means that in a usual setup any jest tests for a web project will misbehave in very strange ways:
WebRTC connections do not actually seem to be independent WebRTC connections but rather keep relying on the brokering circuit-relay-server, even though their transient flag is false. Once the server goes away, the "WebRTC" connection fails.
Once we initiate any WebRTC connection, the nodes do not shut down cleanly anymore, i.e. NodeJS does not terminate.
Steps to reproduce the error:
Here's a minimal test case:
In a TypeScript file to be run e.g. with
npx tsx
, we create three libp2p nodes, a "server" (i.e. a node listening on a dialable address) and two "browser" nodes (i.e. not listening on any dialable address but simply on '/webrtc').We then proceed to connect both "browsers" to the "server" and reserve circuit-relay-transport slots for the browsers at the server.
After that, we initiate a WebRTC connection between the two browsers. Through this connection, we send a message directly from browser1 to browser2.
We then shut down the server and send another message from browser1 to browser2.
Finally, we shut down the browser nodes to end the test.
Expected behavior:
Actual behaviour:
Test code:
Test output:
(Note there is only one line stating "Message received at browser2" while we'd expect there to be two.)
The text was updated successfully, but these errors were encountered: