Skip to content

Commit ca38ae1

Browse files
authored
feat: add WebRTC private to private example (#173)
Restores old example of WebRTC between two browsers
1 parent 83dd9df commit ca38ae1

File tree

10 files changed

+562
-0
lines changed

10 files changed

+562
-0
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
- js-libp2p-example-custom-protocols
2828
- js-libp2p-example-delegated-routing
2929
- js-libp2p-example-discovery-mechanisms
30+
- js-libp2p-example-webrtc-private-to-private
3031
defaults:
3132
run:
3233
working-directory: examples/${{ matrix.project }}
@@ -81,6 +82,7 @@ jobs:
8182
- js-libp2p-example-custom-protocols
8283
- js-libp2p-example-delegated-routing
8384
- js-libp2p-example-discovery-mechanisms
85+
- js-libp2p-example-webrtc-private-to-private
8486
steps:
8587
- uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be
8688
with:
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# ⚠️ IMPORTANT ⚠️
2+
3+
# Please do not create a Pull Request for this repository
4+
5+
The contents of this repository are automatically synced from the parent [js-libp2p Examples Project](https://github.com/libp2p/js-libp2p-examples) so any changes made to the standalone repository will be lost after the next sync.
6+
7+
Please open a PR against [js-libp2p Examples](https://github.com/libp2p/js-libp2p-examples) instead.
8+
9+
## Contributing
10+
11+
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
12+
13+
1. Fork the [js-libp2p Examples Project](https://github.com/libp2p/js-libp2p-examples)
14+
2. Create your Feature Branch (`git checkout -b feature/amazing-example`)
15+
3. Commit your Changes (`git commit -a -m 'feat: add some amazing example'`)
16+
4. Push to the Branch (`git push origin feature/amazing-example`)
17+
5. Open a Pull Request
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: pull
2+
3+
on:
4+
workflow_dispatch
5+
6+
jobs:
7+
sync:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v2
11+
- name: Pull from another repository
12+
uses: ipfs-examples/actions-pull-directory-from-repo@main
13+
with:
14+
source-repo: libp2p/js-libp2p-examples
15+
source-folder-path: examples/${{ github.event.repository.name }}
16+
source-branch: main
17+
target-branch: main
18+
git-username: github-actions
19+
git-email: [email protected]
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# @libp2p/example-webrtc-private-to-private
2+
3+
[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
4+
[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io)
5+
[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-examples.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-examples)
6+
[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p-examples/ci.yml?branch=main\&style=flat-square)](https://github.com/libp2p/js-libp2p-examples/actions/workflows/ci.yml?query=branch%3Amain)
7+
8+
In libp2p terms a "private" node is one behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation) that prevents it from being dialed externally.
9+
10+
This could be a browser, a node.js process or something else.
11+
12+
Nodes that support the [libp2p WebRTC transport](https://github.com/libp2p/specs/blob/master/webrtc/webrtc.md) such as browsers can by dialed via this method even if they are behind a NAT.
13+
14+
When establishing a WebRTC connection, the two browsers must first exchange a series of messages that establish the required capabilities of the nodes (we only require RTC data channels, no video or audio), and their internet-facing addresses/ports.
15+
16+
This is referred to as the "SDP handshake". The WebRTC spec requires this to take place out-of-band, so libp2p performs the handshake via a [Circuit Relay Server](https://docs.libp2p.io/concepts/nat/circuit-relay/) - this is another network node that has made some resources available for the good of the network.
17+
18+
When two browsers dial each other the following steps occur:
19+
20+
1. The listener makes a reservation on a relay with a free slot
21+
2. The dialer obtains the listener's relay address
22+
3. The dialer dials the relay and specifies the listeners PeerId as part of the Circuit Relay HOP protocol
23+
4. The relay opens a stream on the listener as part of the Circuit Relay STOP protocol
24+
5. A virtual connection is created between the dialer and the listener via the relay
25+
6. The dialer opens a stream on the virtual connection to perform the SDP handshake
26+
7. SDP messages are exchanged
27+
8. A direct WebRTC connection is opened between the two browsers
28+
29+
At this point the browsers are directly connected and the relay plays no further part.
30+
31+
## Running the Example
32+
33+
### Build the `@libp2p/example-webrtc-private-to-private` package
34+
35+
Build example by calling `npm i && npm run build` in the repository root.
36+
37+
### Running the Relay Server
38+
39+
For browsers to communicate, we first need to run a relay server:
40+
41+
```shell
42+
npm run relay
43+
```
44+
45+
The [multiaddress](https://docs.libp2p.io/concepts/fundamentals/addressing/) the relay is listening on will be printed to the console. Copy one of them to your clipboard.
46+
47+
### Running the Clients
48+
49+
In a separate console tab, start the web server:
50+
51+
```shell
52+
npm start
53+
```
54+
55+
A browser window will automatically open. Let's call this `Browser A`.
56+
57+
Using the copied multiaddrs from the relay server, paste it into the `Remote MultiAddress` input and click the `Connect` button.
58+
`Browser A` is now connected to the relay server.
59+
60+
Copy the multiaddr located after the `Listening on` message.
61+
62+
Now open a second tab with the url `http://localhost:5173/`, perhaps in a different browser or a private window. Let's call this `Browser B`.
63+
64+
Using the copied multiaddress from `Listening on` section in `Browser A`, paste it into the `Remote MultiAddress` input and click the `Connect` button.
65+
66+
The peers are now connected to each other.
67+
68+
Enter a message and click the `Send` button in either/both browsers and see the echo'd messages.
69+
70+
The output should look like:
71+
72+
`Browser A`
73+
```text
74+
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk'
75+
Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-circuit/webrtc/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC
76+
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-circuit/webrtc/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9'
77+
Sending message 'helloa'
78+
Received message 'helloa'
79+
Received message 'hellob'
80+
```
81+
82+
`Browser B`
83+
```text
84+
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-circuit/webrtc/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC'
85+
Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-circuit/webrtc/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9
86+
Received message 'helloa'
87+
Sending message 'hellob'
88+
Received message 'hellob'
89+
```
90+
91+
## Next steps
92+
93+
The WebRTC transport is not limited to browsers.
94+
95+
Why don't you try to create a Node.js version of the [browser peer script](./index.js)?
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>js-libp2p WebRTC</title>
7+
<style>
8+
label,
9+
button {
10+
display: block;
11+
font-weight: bold;
12+
margin: 5px 0;
13+
}
14+
div {
15+
margin-bottom: 20px;
16+
}
17+
#send-section {
18+
display: none;
19+
}
20+
input[type="text"] {
21+
width: 800px;
22+
}
23+
</style>
24+
</head>
25+
<body>
26+
<div id="app">
27+
<div>
28+
<label for="peer">Remote MultiAddress:</label>
29+
<input type="text" id="peer" />
30+
<button id="connect">Connect</button>
31+
</div>
32+
<div id="send-section">
33+
<label for="message">Message:</label>
34+
<input type="text" id="message" value="hello" />
35+
<button id="send">Send</button>
36+
</div>
37+
<div id="connectionsWrapper">
38+
<h3>Active Connections:</h3>
39+
<ul id="connections"></ul>
40+
</div>
41+
<div id="listeningAddressesWrapper">
42+
<h3>Listening addresses:</h3>
43+
<ul id="multiaddrs"></ul>
44+
</div>
45+
<h3>Output:</h3>
46+
<div id="output"></div>
47+
</div>
48+
<script type="module" src="./index.js"></script>
49+
</body>
50+
</html>
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { noise } from '@chainsafe/libp2p-noise'
2+
import { yamux } from '@chainsafe/libp2p-yamux'
3+
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
4+
import { identify, identifyPush } from '@libp2p/identify'
5+
import { ping } from '@libp2p/ping'
6+
import { webRTC } from '@libp2p/webrtc'
7+
import { webSockets } from '@libp2p/websockets'
8+
import * as filters from '@libp2p/websockets/filters'
9+
import { multiaddr, protocols } from '@multiformats/multiaddr'
10+
import { byteStream } from 'it-byte-stream'
11+
import { createLibp2p } from 'libp2p'
12+
import { fromString, toString } from 'uint8arrays'
13+
14+
const WEBRTC_CODE = protocols('webrtc').code
15+
16+
const output = document.getElementById('output')
17+
const sendSection = document.getElementById('send-section')
18+
const appendOutput = (line) => {
19+
const div = document.createElement('div')
20+
div.appendChild(document.createTextNode(line))
21+
output.append(div)
22+
}
23+
const CHAT_PROTOCOL = '/libp2p/examples/chat/1.0.0'
24+
let ma
25+
let chatStream
26+
27+
const node = await createLibp2p({
28+
addresses: {
29+
listen: [
30+
'/webrtc'
31+
]
32+
},
33+
transports: [
34+
webSockets({
35+
filter: filters.all
36+
}),
37+
webRTC(),
38+
circuitRelayTransport({
39+
discoverRelays: 1
40+
})
41+
],
42+
connectionEncrypters: [noise()],
43+
streamMuxers: [yamux()],
44+
connectionGater: {
45+
denyDialMultiaddr: () => {
46+
// by default we refuse to dial local addresses from the browser since they
47+
// are usually sent by remote peers broadcasting undialable multiaddrs but
48+
// here we are explicitly connecting to a local node so do not deny dialing
49+
// any discovered address
50+
return false
51+
}
52+
},
53+
services: {
54+
identify: identify(),
55+
identifyPush: identifyPush(),
56+
ping: ping()
57+
}
58+
})
59+
60+
await node.start()
61+
62+
function updateConnList () {
63+
// Update connections list
64+
const connListEls = node.getConnections()
65+
.map((connection) => {
66+
if (connection.remoteAddr.protoCodes().includes(WEBRTC_CODE)) {
67+
ma = connection.remoteAddr
68+
sendSection.style.display = 'block'
69+
}
70+
71+
const el = document.createElement('li')
72+
el.textContent = connection.remoteAddr.toString()
73+
return el
74+
})
75+
document.getElementById('connections').replaceChildren(...connListEls)
76+
}
77+
78+
node.addEventListener('connection:open', (event) => {
79+
updateConnList()
80+
})
81+
node.addEventListener('connection:close', (event) => {
82+
updateConnList()
83+
})
84+
85+
node.addEventListener('self:peer:update', (event) => {
86+
// Update multiaddrs list, only show WebRTC addresses
87+
const multiaddrs = node.getMultiaddrs()
88+
.filter(ma => isWebrtc(ma))
89+
.map((ma) => {
90+
const el = document.createElement('li')
91+
el.textContent = ma.toString()
92+
return el
93+
})
94+
document.getElementById('multiaddrs').replaceChildren(...multiaddrs)
95+
})
96+
97+
node.handle(CHAT_PROTOCOL, async ({ stream }) => {
98+
chatStream = byteStream(stream)
99+
100+
while (true) {
101+
const buf = await chatStream.read()
102+
appendOutput(`Received message '${toString(buf.subarray())}'`)
103+
}
104+
})
105+
106+
const isWebrtc = (ma) => {
107+
return ma.protoCodes().includes(WEBRTC_CODE)
108+
}
109+
110+
window.connect.onclick = async () => {
111+
ma = multiaddr(window.peer.value)
112+
appendOutput(`Dialing '${ma}'`)
113+
114+
const signal = AbortSignal.timeout(5000)
115+
116+
try {
117+
if (isWebrtc(ma)) {
118+
const rtt = await node.services.ping.ping(ma, {
119+
signal
120+
})
121+
appendOutput(`Connected to '${ma}'`)
122+
appendOutput(`RTT to ${ma.getPeerId()} was ${rtt}ms`)
123+
} else {
124+
await node.dial(ma, {
125+
signal
126+
})
127+
appendOutput('Connected to relay')
128+
}
129+
} catch (err) {
130+
if (signal.aborted) {
131+
appendOutput(`Timed out connecting to '${ma}'`)
132+
} else {
133+
appendOutput(`Connecting to '${ma}' failed - ${err.message}`)
134+
}
135+
}
136+
}
137+
138+
window.send.onclick = async () => {
139+
if (chatStream == null) {
140+
appendOutput('Opening chat stream')
141+
142+
const signal = AbortSignal.timeout(5000)
143+
144+
try {
145+
const stream = await node.dialProtocol(ma, CHAT_PROTOCOL, {
146+
signal
147+
})
148+
chatStream = byteStream(stream)
149+
150+
Promise.resolve().then(async () => {
151+
while (true) {
152+
const buf = await chatStream.read()
153+
appendOutput(`Received message '${toString(buf.subarray())}'`)
154+
}
155+
})
156+
} catch (err) {
157+
if (signal.aborted) {
158+
appendOutput('Timed out opening chat stream')
159+
} else {
160+
appendOutput(`Opening chat stream failed - ${err.message}`)
161+
}
162+
163+
return
164+
}
165+
}
166+
167+
const message = window.message.value.toString().trim()
168+
appendOutput(`Sending message '${message}'`)
169+
chatStream.write(fromString(message))
170+
.catch(err => {
171+
appendOutput(`Error sending message - ${err.message}`)
172+
})
173+
}

0 commit comments

Comments
 (0)