Skip to content

Commit

Permalink
feat: add WebRTC private to private example (#173)
Browse files Browse the repository at this point in the history
Restores old example of WebRTC between two browsers
  • Loading branch information
achingbrain committed Sep 20, 2024
1 parent 83dd9df commit ca38ae1
Show file tree
Hide file tree
Showing 10 changed files with 562 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
- js-libp2p-example-custom-protocols
- js-libp2p-example-delegated-routing
- js-libp2p-example-discovery-mechanisms
- js-libp2p-example-webrtc-private-to-private
defaults:
run:
working-directory: examples/${{ matrix.project }}
Expand Down Expand Up @@ -81,6 +82,7 @@ jobs:
- js-libp2p-example-custom-protocols
- js-libp2p-example-delegated-routing
- js-libp2p-example-discovery-mechanisms
- js-libp2p-example-webrtc-private-to-private
steps:
- uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be
with:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# ⚠️ IMPORTANT ⚠️

# Please do not create a Pull Request for this repository

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.

Please open a PR against [js-libp2p Examples](https://github.com/libp2p/js-libp2p-examples) instead.

## Contributing

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**.

1. Fork the [js-libp2p Examples Project](https://github.com/libp2p/js-libp2p-examples)
2. Create your Feature Branch (`git checkout -b feature/amazing-example`)
3. Commit your Changes (`git commit -a -m 'feat: add some amazing example'`)
4. Push to the Branch (`git push origin feature/amazing-example`)
5. Open a Pull Request
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: pull

on:
workflow_dispatch

jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Pull from another repository
uses: ipfs-examples/actions-pull-directory-from-repo@main
with:
source-repo: libp2p/js-libp2p-examples
source-folder-path: examples/${{ github.event.repository.name }}
source-branch: main
target-branch: main
git-username: github-actions
git-email: github-actions@github.com
95 changes: 95 additions & 0 deletions examples/js-libp2p-example-webrtc-private-to-private/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# @libp2p/example-webrtc-private-to-private

[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io)
[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-examples.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-examples)
[![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)

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.

This could be a browser, a node.js process or something else.

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.

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.

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.

When two browsers dial each other the following steps occur:

1. The listener makes a reservation on a relay with a free slot
2. The dialer obtains the listener's relay address
3. The dialer dials the relay and specifies the listeners PeerId as part of the Circuit Relay HOP protocol
4. The relay opens a stream on the listener as part of the Circuit Relay STOP protocol
5. A virtual connection is created between the dialer and the listener via the relay
6. The dialer opens a stream on the virtual connection to perform the SDP handshake
7. SDP messages are exchanged
8. A direct WebRTC connection is opened between the two browsers

At this point the browsers are directly connected and the relay plays no further part.

## Running the Example

### Build the `@libp2p/example-webrtc-private-to-private` package

Build example by calling `npm i && npm run build` in the repository root.

### Running the Relay Server

For browsers to communicate, we first need to run a relay server:

```shell
npm run relay
```

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.

### Running the Clients

In a separate console tab, start the web server:

```shell
npm start
```

A browser window will automatically open. Let's call this `Browser A`.

Using the copied multiaddrs from the relay server, paste it into the `Remote MultiAddress` input and click the `Connect` button.
`Browser A` is now connected to the relay server.

Copy the multiaddr located after the `Listening on` message.

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`.

Using the copied multiaddress from `Listening on` section in `Browser A`, paste it into the `Remote MultiAddress` input and click the `Connect` button.

The peers are now connected to each other.

Enter a message and click the `Send` button in either/both browsers and see the echo'd messages.

The output should look like:

`Browser A`
```text
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk'
Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-circuit/webrtc/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-circuit/webrtc/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9'
Sending message 'helloa'
Received message 'helloa'
Received message 'hellob'
```

`Browser B`
```text
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-circuit/webrtc/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC'
Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-circuit/webrtc/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9
Received message 'helloa'
Sending message 'hellob'
Received message 'hellob'
```

## Next steps

The WebRTC transport is not limited to browsers.

Why don't you try to create a Node.js version of the [browser peer script](./index.js)?
50 changes: 50 additions & 0 deletions examples/js-libp2p-example-webrtc-private-to-private/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>js-libp2p WebRTC</title>
<style>
label,
button {
display: block;
font-weight: bold;
margin: 5px 0;
}
div {
margin-bottom: 20px;
}
#send-section {
display: none;
}
input[type="text"] {
width: 800px;
}
</style>
</head>
<body>
<div id="app">
<div>
<label for="peer">Remote MultiAddress:</label>
<input type="text" id="peer" />
<button id="connect">Connect</button>
</div>
<div id="send-section">
<label for="message">Message:</label>
<input type="text" id="message" value="hello" />
<button id="send">Send</button>
</div>
<div id="connectionsWrapper">
<h3>Active Connections:</h3>
<ul id="connections"></ul>
</div>
<div id="listeningAddressesWrapper">
<h3>Listening addresses:</h3>
<ul id="multiaddrs"></ul>
</div>
<h3>Output:</h3>
<div id="output"></div>
</div>
<script type="module" src="./index.js"></script>
</body>
</html>
173 changes: 173 additions & 0 deletions examples/js-libp2p-example-webrtc-private-to-private/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
import { identify, identifyPush } from '@libp2p/identify'
import { ping } from '@libp2p/ping'
import { webRTC } from '@libp2p/webrtc'
import { webSockets } from '@libp2p/websockets'
import * as filters from '@libp2p/websockets/filters'
import { multiaddr, protocols } from '@multiformats/multiaddr'
import { byteStream } from 'it-byte-stream'
import { createLibp2p } from 'libp2p'
import { fromString, toString } from 'uint8arrays'

const WEBRTC_CODE = protocols('webrtc').code

const output = document.getElementById('output')
const sendSection = document.getElementById('send-section')
const appendOutput = (line) => {
const div = document.createElement('div')
div.appendChild(document.createTextNode(line))
output.append(div)
}
const CHAT_PROTOCOL = '/libp2p/examples/chat/1.0.0'
let ma
let chatStream

const node = await createLibp2p({
addresses: {
listen: [
'/webrtc'
]
},
transports: [
webSockets({
filter: filters.all
}),
webRTC(),
circuitRelayTransport({
discoverRelays: 1
})
],
connectionEncrypters: [noise()],
streamMuxers: [yamux()],
connectionGater: {
denyDialMultiaddr: () => {
// by default we refuse to dial local addresses from the browser since they
// are usually sent by remote peers broadcasting undialable multiaddrs but
// here we are explicitly connecting to a local node so do not deny dialing
// any discovered address
return false
}
},
services: {
identify: identify(),
identifyPush: identifyPush(),
ping: ping()
}
})

await node.start()

function updateConnList () {
// Update connections list
const connListEls = node.getConnections()
.map((connection) => {
if (connection.remoteAddr.protoCodes().includes(WEBRTC_CODE)) {
ma = connection.remoteAddr
sendSection.style.display = 'block'
}

const el = document.createElement('li')
el.textContent = connection.remoteAddr.toString()
return el
})
document.getElementById('connections').replaceChildren(...connListEls)
}

node.addEventListener('connection:open', (event) => {
updateConnList()
})
node.addEventListener('connection:close', (event) => {
updateConnList()
})

node.addEventListener('self:peer:update', (event) => {
// Update multiaddrs list, only show WebRTC addresses
const multiaddrs = node.getMultiaddrs()
.filter(ma => isWebrtc(ma))
.map((ma) => {
const el = document.createElement('li')
el.textContent = ma.toString()
return el
})
document.getElementById('multiaddrs').replaceChildren(...multiaddrs)
})

node.handle(CHAT_PROTOCOL, async ({ stream }) => {
chatStream = byteStream(stream)

while (true) {
const buf = await chatStream.read()
appendOutput(`Received message '${toString(buf.subarray())}'`)
}
})

const isWebrtc = (ma) => {
return ma.protoCodes().includes(WEBRTC_CODE)
}

window.connect.onclick = async () => {
ma = multiaddr(window.peer.value)
appendOutput(`Dialing '${ma}'`)

const signal = AbortSignal.timeout(5000)

try {
if (isWebrtc(ma)) {
const rtt = await node.services.ping.ping(ma, {
signal
})
appendOutput(`Connected to '${ma}'`)
appendOutput(`RTT to ${ma.getPeerId()} was ${rtt}ms`)
} else {
await node.dial(ma, {
signal
})
appendOutput('Connected to relay')
}
} catch (err) {
if (signal.aborted) {
appendOutput(`Timed out connecting to '${ma}'`)
} else {
appendOutput(`Connecting to '${ma}' failed - ${err.message}`)
}
}
}

window.send.onclick = async () => {
if (chatStream == null) {
appendOutput('Opening chat stream')

const signal = AbortSignal.timeout(5000)

try {
const stream = await node.dialProtocol(ma, CHAT_PROTOCOL, {
signal
})
chatStream = byteStream(stream)

Promise.resolve().then(async () => {
while (true) {
const buf = await chatStream.read()
appendOutput(`Received message '${toString(buf.subarray())}'`)
}
})
} catch (err) {
if (signal.aborted) {
appendOutput('Timed out opening chat stream')
} else {
appendOutput(`Opening chat stream failed - ${err.message}`)
}

return
}
}

const message = window.message.value.toString().trim()
appendOutput(`Sending message '${message}'`)
chatStream.write(fromString(message))
.catch(err => {
appendOutput(`Error sending message - ${err.message}`)
})
}
Loading

0 comments on commit ca38ae1

Please sign in to comment.