Skip to content

Commit

Permalink
Add a plugin transport for WebRTC signalling protocol (#3) (#4)
Browse files Browse the repository at this point in the history
* Add scaffolding for WebRTC signalling plugin

* Create a signalling stream when a peer connects to primary relay

* Negotiate WebRTC connections between peers over signalling stream

* Stop listener on disconnecting from primary relay node

* Filter out circuit addresses

* Forward signalling messages over a pubsub topic if required

* Update todos

* Fix lint errors

* Update dependencies

* Update package version
  • Loading branch information
prathamesh0 authored Mar 23, 2023
1 parent f2d9a5d commit f2111a1
Show file tree
Hide file tree
Showing 15 changed files with 895 additions and 8 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cerc-io/libp2p",
"version": "0.42.2-laconic-0.1.1",
"version": "0.42.2-laconic-0.1.2",
"description": "JavaScript implementation of libp2p, a modular peer to peer network stack",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/libp2p/js-libp2p#readme",
Expand Down Expand Up @@ -123,6 +123,7 @@
"@libp2p/peer-store": "^6.0.0",
"@libp2p/tracked-map": "^3.0.0",
"@libp2p/utils": "^3.0.2",
"@libp2p/webrtc-peer": "^2.0.2",
"@multiformats/mafmt": "^11.0.2",
"@multiformats/multiaddr": "^11.0.0",
"abortable-iterator": "^4.0.2",
Expand All @@ -143,6 +144,7 @@
"it-merge": "^2.0.0",
"it-pair": "^2.0.2",
"it-pipe": "^2.0.3",
"it-pushable": "^3.1.2",
"it-sort": "^2.0.0",
"it-stream-types": "^1.0.4",
"merge-options": "^3.0.4",
Expand Down Expand Up @@ -173,7 +175,7 @@
"@libp2p/interface-compliance-tests": "^3.0.2",
"@libp2p/interface-connection-compliance-tests": "^2.0.3",
"@libp2p/interface-connection-encrypter-compliance-tests": "^4.0.0",
"@libp2p/interface-mocks": "^9.0.0",
"@libp2p/interface-mocks": "9.1.3",
"@libp2p/interop": "^4.0.0",
"@libp2p/kad-dht": "^7.0.0",
"@libp2p/mdns": "^6.0.0",
Expand All @@ -192,7 +194,6 @@
"delay": "^5.0.0",
"execa": "^6.1.0",
"go-libp2p": "^0.0.6",
"it-pushable": "^3.0.0",
"it-to-buffer": "^3.0.0",
"npm-run-all": "^4.1.5",
"p-defer": "^4.0.0",
Expand Down
8 changes: 8 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ const DefaultConfig: Partial<Libp2pInit> = {
maxListeners: 2
}
},
webRTCSignal: {
enabled: false,
isSignallingNode: false,
autoSignal: {
enabled: false,
relayPeerId: ''
}
},
identify: {
protocolPrefix: 'ipfs',
host: {
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import type { NatManagerInit } from './nat-manager.js'
import type { AddressManagerInit } from './address-manager/index.js'
import type { PeerRoutingInit } from './peer-routing.js'
import type { ConnectionManagerInit } from './connection-manager/index.js'
import type { WebRTCSignalConfig } from './webrtc-signal/index.js'

/**
* For Libp2p configurations and modules details read the [Configuration Document](./CONFIGURATION.md).
Expand Down Expand Up @@ -105,6 +106,8 @@ export interface Libp2pInit {
*/
relay: RelayConfig

webRTCSignal: WebRTCSignalConfig

/**
* libp2p identify protocol options
*/
Expand Down
14 changes: 12 additions & 2 deletions src/libp2p.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import { PeerSet } from '@libp2p/peer-collections'
import { DefaultDialer } from './connection-manager/dialer/index.js'
import { peerIdFromString } from '@libp2p/peer-id'
import type { Datastore } from 'interface-datastore'
import { WebRTCSignal } from './webrtc-signal/transport.js'
import { AutoSignal } from './webrtc-signal/index.js'

const log = logger('libp2p')

Expand Down Expand Up @@ -233,6 +235,14 @@ export class Libp2pNode extends EventEmitter<Libp2pEvents> implements Libp2p {
}))
}

if (init.webRTCSignal.enabled) {
this.components.transportManager.add(this.configureComponent(new WebRTCSignal(this.components, init.webRTCSignal)))

if (!init.webRTCSignal.isSignallingNode && init.webRTCSignal.autoSignal?.enabled) {
this.configureComponent(new AutoSignal(this.components, init.webRTCSignal.autoSignal))
}
}

this.fetchService = this.configureComponent(new FetchService(this.components, {
...init.fetch
}))
Expand Down Expand Up @@ -282,7 +292,7 @@ export class Libp2pNode extends EventEmitter<Libp2pEvents> implements Libp2p {

// start any startables
await Promise.all(
this.services.map(service => service.start())
this.services.map(async service => await service.start())
)

await Promise.all(
Expand Down Expand Up @@ -322,7 +332,7 @@ export class Libp2pNode extends EventEmitter<Libp2pEvents> implements Libp2p {
)

await Promise.all(
this.services.map(service => service.stop())
this.services.map(async service => await service.stop())
)

await Promise.all(
Expand Down
9 changes: 9 additions & 0 deletions src/webrtc-signal/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Time to wait for a connection to close gracefully before destroying it manually
export const CLOSE_TIMEOUT = 2000

// Use a supported protocol id in multiaddr to listen through signalling stream
// Need to use one of the supported protocol names (list: https://github.com/multiformats/multiaddr/blob/master/protocols.csv) for the multiaddr to be valid
export const P2P_WEBRTC_STAR_ID = 'p2p-webrtc-star'

// Pubsub topic over which signalling nodes forward the signalling messages if not connected to the destination
export const WEBRTC_SIGNAL_TOPIC = 'webrtc-signal'
133 changes: 133 additions & 0 deletions src/webrtc-signal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { logger } from '@libp2p/logger'
import type { PeerId } from '@libp2p/interface-peer-id'
import type { PeerStore, PeerProtocolsChangeData } from '@libp2p/interface-peer-store'
import type { Connection } from '@libp2p/interface-connection'
import type { ConnectionManager } from '@libp2p/interface-connection-manager'
import type { TransportManager } from '@libp2p/interface-transport'

import { WEBRTC_SIGNAL_CODEC } from './multicodec.js'
import { P2P_WEBRTC_STAR_ID } from './constants.js'

const log = logger('libp2p:webrtc-signal:auto-signal')

export interface WebRTCSignalConfig {
enabled: boolean
isSignallingNode: boolean
autoSignal: AutoSignalConfig
}

export interface AutoSignalConfig {
enabled: boolean
relayPeerId: string
}

export interface SignalComponents {
peerStore: PeerStore
connectionManager: ConnectionManager
transportManager: TransportManager
}

export class AutoSignal {
private readonly components: SignalComponents
private readonly relayPeerId: string
private isListening: boolean = false
// TODO Done in circuit-relay implementation, required here?
// private readonly onError: (error: Error, msg?: string) => void

constructor (components: SignalComponents, init: AutoSignalConfig) {
this.components = components
this.relayPeerId = init.relayPeerId

this._onProtocolChange = this._onProtocolChange.bind(this)
this._onPeerConnected = this._onPeerConnected.bind(this)
this._onPeerDisconnected = this._onPeerDisconnected.bind(this)

this.components.peerStore.addEventListener('change:protocols', (evt) => {
void this._onProtocolChange(evt).catch(err => {
log.error(err)
})
})

this.components.connectionManager.addEventListener('peer:connect', (evt) => {
void this._onPeerConnected(evt).catch(err => {
log.error(err)
})
})

this.components.connectionManager.addEventListener('peer:disconnect', (evt) => this._onPeerDisconnected(evt))
}

async _onProtocolChange (evt: CustomEvent<PeerProtocolsChangeData>) {
const {
peerId,
protocols
} = evt.detail

await this._handleProtocols(peerId, protocols)
}

async _onPeerConnected (evt: CustomEvent<Connection>) {
const connection = evt.detail
const peerId = connection.remotePeer
const protocols = await this.components.peerStore.protoBook.get(peerId)

// Handle protocols on peer connection as change:protocols event is not triggered after reconnection between peers.
await this._handleProtocols(peerId, protocols)
}

_onPeerDisconnected (evt: CustomEvent<Connection>) {
const connection = evt.detail

if (connection.remotePeer.toString() === this.relayPeerId.toString()) {
this.isListening = false
}
}

async _handleProtocols (peerId: PeerId, protocols: string[]) {
// Ignore if we are already listening or it's not the primary relay node
if (this.isListening || peerId.toString() !== this.relayPeerId) {
return
}

// Check if it has the protocol
const hasProtocol = protocols.find(protocol => protocol === WEBRTC_SIGNAL_CODEC)

// Ignore if protocol is not supported
if (hasProtocol == null) {
return
}

// If required protocol is supported, start the listener
const connections = this.components.connectionManager.getConnections(peerId)
if (connections.length === 0) {
return
}

const connection = connections[0]

// TODO Done in circuit-relay implementation, required here?
// await this.components.peerStore.metadataBook.setValue(peerId, HOP_METADATA_KEY, uint8ArrayFromString(HOP_METADATA_VALUE))

await this._addListener(connection)
}

/**
* Attempt to listen on the given connection with relay node
*/
async _addListener (connection: Connection): Promise<void> {
try {
const remoteAddr = connection.remoteAddr

// Attempt to listen on relay
const multiaddr = remoteAddr.encapsulate(`/${P2P_WEBRTC_STAR_ID}`)

// Announce multiaddr will update on listen success by TransportManager event being triggered
await this.components.transportManager.listen([multiaddr])
this.isListening = true
} catch (err: any) {
log.error('error listening on signalling address', err)
this.isListening = false
throw err
}
}
}
Loading

0 comments on commit f2111a1

Please sign in to comment.