Skip to content

Commit

Permalink
fix: use randomwalk to find circuit relay servers (#2563)
Browse files Browse the repository at this point in the history
Instead of publishing provider records for a pre-shared CID to find circuit relay servers, perform a random walk and attempt to make a reservation on any relay peers discovered.

Fixes #2545
  • Loading branch information
achingbrain authored Jun 6, 2024
1 parent 757fb26 commit 440c9b3
Show file tree
Hide file tree
Showing 19 changed files with 608 additions and 362 deletions.
67 changes: 59 additions & 8 deletions packages/integration-tests/.aegir.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { pipe } from 'it-pipe'
import { execa } from 'execa'
import pDefer from 'p-defer'

/** @type {import('aegir').PartialOptions} */
export default {
Expand All @@ -18,6 +19,7 @@ export default {
const { plaintext } = await import('@libp2p/plaintext')
const { circuitRelayServer, circuitRelayTransport } = await import('@libp2p/circuit-relay-v2')
const { identify } = await import('@libp2p/identify')
const { echo } = await import('@libp2p/echo')

const peerId = await createEd25519PeerId()
const libp2p = await createLibp2p({
Expand Down Expand Up @@ -49,24 +51,73 @@ export default {
reservations: {
maxReservations: Infinity
}
})
}),
echo: echo()
}
})
// Add the echo protocol
await libp2p.handle('/echo/1.0.0', ({ stream }) => {
pipe(stream, stream)
.catch() // sometimes connections are closed before multistream-select finishes which causes an error
})

const goLibp2pRelay = await createGoLibp2pRelay()

return {
libp2p,
goLibp2pRelay,
env: {
RELAY_MULTIADDR: libp2p.getMultiaddrs().filter(ma => WebSockets.matches(ma)).pop()
RELAY_MULTIADDR: libp2p.getMultiaddrs().filter(ma => WebSockets.matches(ma)).pop(),
GO_RELAY_PEER: goLibp2pRelay.peerId,
GO_RELAY_MULTIADDRS: goLibp2pRelay.multiaddrs,
GO_RELAY_APIADDR: goLibp2pRelay.apiAddr
}
}
},
after: async (_, before) => {
await before.libp2p.stop()
await before.goLibp2pRelay.proc.kill()
}
}
}

async function createGoLibp2pRelay () {
const { multiaddr } = await import('@multiformats/multiaddr')
const { path: p2pd } = await import('go-libp2p')
const { createClient } = await import('@libp2p/daemon-client')

const controlPort = Math.floor(Math.random() * (50000 - 10000 + 1)) + 10000
const apiAddr = multiaddr(`/ip4/127.0.0.1/tcp/${controlPort}`)
const deferred = pDefer()
const proc = execa(p2pd(), [
`-listen=${apiAddr.toString()}`,
// listen on TCP, WebSockets and WebTransport
'-hostAddrs=/ip4/127.0.0.1/tcp/0,/ip4/127.0.0.1/tcp/0/ws,/ip4/127.0.0.1/udp/0/quic-v1/webtransport',
'-noise=true',
'-dhtServer',
'-relay',
'-muxer=mplex'
], {
env: {
GOLOG_LOG_LEVEL: 'debug'
}
})
proc.catch(() => {
// go-libp2p daemon throws when killed
})

proc.stdout?.on('data', (buf) => {
const str = buf.toString()

// daemon has started
if (str.includes('Control socket:')) {
deferred.resolve()
}
})
await deferred.promise

const daemonClient = createClient(apiAddr)
const id = await daemonClient.identify()

return {
apiAddr,
peerId: id.peerId.toString(),
multiaddrs: id.addrs.map(ma => ma.toString()).join(','),
proc
}
}
4 changes: 4 additions & 0 deletions packages/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@libp2p/daemon-client": "^8.0.5",
"@libp2p/daemon-server": "^7.0.5",
"@libp2p/dcutr": "^1.0.20",
"@libp2p/echo": "^1.0.7",
"@libp2p/fetch": "^1.0.17",
"@libp2p/floodsub": "^9.0.19",
"@libp2p/identify": "^2.0.1",
Expand All @@ -61,10 +62,13 @@
"@libp2p/tls": "^1.0.10",
"@libp2p/webrtc": "^4.0.32",
"@libp2p/websockets": "^8.0.23",
"@libp2p/webtransport": "^4.0.32",
"@multiformats/mafmt": "^12.1.6",
"@multiformats/multiaddr": "^12.2.3",
"@multiformats/multiaddr-matcher": "^1.2.1",
"aegir": "^43.0.1",
"delay": "^6.0.0",
"detect-browser": "^5.3.0",
"execa": "^9.1.0",
"go-libp2p": "^1.2.0",
"it-all": "^3.0.6",
Expand Down
106 changes: 82 additions & 24 deletions packages/integration-tests/test/circuit-relay-discovery.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@

import { yamux } from '@chainsafe/libp2p-yamux'
import { circuitRelayServer, type CircuitRelayService, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
import { identify } from '@libp2p/identify'
import { stop } from '@libp2p/interface'
import { kadDHT, passthroughMapper } from '@libp2p/kad-dht'
import { plaintext } from '@libp2p/plaintext'
import { tcp } from '@libp2p/tcp'
import { expect } from 'aegir/chai'
import { createLibp2p } from 'libp2p'
import { pEvent } from 'p-event'
import { getRelayAddress, hasRelay, MockContentRouting, mockContentRouting } from './fixtures/utils.js'
import pDefer from 'p-defer'
import { getRelayAddress, hasRelay } from './fixtures/utils.js'
import type { Libp2p } from '@libp2p/interface'
import type { KadDHT } from '@libp2p/kad-dht'

const DHT_PROTOCOL = '/integration-test/circuit-relay/1.0.0'

describe('circuit-relay discovery', () => {
let local: Libp2p
let remote: Libp2p
let relay: Libp2p<{ relay: CircuitRelayService }>
let bootstrapper: Libp2p<{ kadDht: KadDHT }>

beforeEach(async () => {
// create relay first so it has time to advertise itself via content routing
relay = await createLibp2p({
addresses: {
listen: ['/ip4/127.0.0.1/tcp/0']
Expand All @@ -30,20 +36,57 @@ describe('circuit-relay discovery', () => {
connectionEncryption: [
plaintext()
],
contentRouters: [
mockContentRouting()
],
services: {
relay: circuitRelayServer({
advertise: {
bootDelay: 10
reservations: {
maxReservations: Infinity
}
}),
identify: identify(),
kadDht: kadDHT({
protocol: DHT_PROTOCOL,
peerInfoMapper: passthroughMapper,
clientMode: false
})
}
})

bootstrapper = await createLibp2p({
addresses: {
listen: ['/ip4/127.0.0.1/tcp/0']
},
transports: [
tcp()
],
streamMuxers: [
yamux()
],
connectionEncryption: [
plaintext()
],
services: {
identify: identify(),
kadDht: kadDHT({
protocol: DHT_PROTOCOL,
peerInfoMapper: passthroughMapper,
clientMode: false
})
}
})

// wait for relay to advertise service successfully
await pEvent(relay.services.relay, 'relay:advert:success')
// connect the bootstrapper to the relay
await bootstrapper.dial(relay.getMultiaddrs())

// bootstrapper should be able to locate relay via DHT
const foundRelay = pDefer()
void Promise.resolve().then(async () => {
for await (const event of bootstrapper.services.kadDht.findPeer(relay.peerId)) {
if (event.name === 'FINAL_PEER') {
foundRelay.resolve()
}
}
})
await foundRelay.promise

// now create client nodes
;[local, remote] = await Promise.all([
Expand All @@ -63,9 +106,14 @@ describe('circuit-relay discovery', () => {
connectionEncryption: [
plaintext()
],
contentRouters: [
mockContentRouting()
]
services: {
identify: identify(),
kadDht: kadDHT({
protocol: DHT_PROTOCOL,
peerInfoMapper: passthroughMapper,
clientMode: true
})
}
}),
createLibp2p({
addresses: {
Expand All @@ -83,25 +131,35 @@ describe('circuit-relay discovery', () => {
connectionEncryption: [
plaintext()
],
contentRouters: [
mockContentRouting()
]
services: {
identify: identify(),
kadDht: kadDHT({
protocol: DHT_PROTOCOL,
peerInfoMapper: passthroughMapper,
clientMode: true
})
}
})
])

// connect both nodes to the bootstrapper
await Promise.all([
local.dial(bootstrapper.getMultiaddrs()),
remote.dial(bootstrapper.getMultiaddrs())
])
})

afterEach(async () => {
MockContentRouting.reset()

// Stop each node
return Promise.all([local, remote, relay].map(async libp2p => {
if (libp2p != null) {
await libp2p.stop()
}
}))
await stop(
local,
remote,
bootstrapper,
relay
)
})

it('should find provider for relay and add it as listen relay', async () => {
it('should discover relay and add it as listen relay', async () => {
// both nodes should discover the relay - they have no direct connection
// so it will be via content routing
const localRelayPeerId = await hasRelay(local)
Expand Down
99 changes: 99 additions & 0 deletions packages/integration-tests/test/circuit-relay-discovery.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* eslint-env mocha */
/* eslint max-nested-callbacks: ['error', 6] */

import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
import { identify } from '@libp2p/identify'
import { stop } from '@libp2p/interface'
import { mplex } from '@libp2p/mplex'
import { plaintext } from '@libp2p/plaintext'
import { webSockets } from '@libp2p/websockets'
import * as filters from '@libp2p/websockets/filters'
import { webTransport } from '@libp2p/webtransport'
import { multiaddr } from '@multiformats/multiaddr'
import { WebSockets, WebTransport } from '@multiformats/multiaddr-matcher'
import { createLibp2p } from 'libp2p'
import { hasRelay, isFirefox } from './fixtures/utils.js'
import type { Libp2p } from '@libp2p/interface'

describe('circuit-relay discovery', () => {
let node: Libp2p

beforeEach(async () => {
node = await createLibp2p({
transports: [
webSockets({
filter: filters.all
}),
circuitRelayTransport({
discoverRelays: 1
}),
webTransport()
],
streamMuxers: [
yamux(),
mplex()
],
connectionEncryption: [
plaintext(),
noise()
],
connectionGater: {
denyDialMultiaddr: () => false
},
services: {
identify: identify()
}
})
})

afterEach(async () => {
await stop(node)
})

it('should reserve slot on go relay via WebSockets', async () => {
const ma = (process.env.GO_RELAY_MULTIADDRS ?? '')
.split(',')
.map(ma => multiaddr(ma))
.filter(ma => WebSockets.matches(ma))
.pop()

if (ma == null) {
throw new Error('Could not detect go relay WebSocket address')
}

// dial the relay
await node.dial(ma)

// wait for a reservation to be made
await hasRelay(node)
})

it('should reserve slot on go relay via WebTransport', async function () {
if (globalThis.WebTransport == null) {
return this.skip()
}

if (isFirefox) {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1899812
return this.skip()
}

const ma = (process.env.GO_RELAY_MULTIADDRS ?? '')
.split(',')
.map(ma => multiaddr(`${ma}/p2p/${process.env.GO_RELAY_PEER}`))
.filter(ma => WebTransport.matches(ma))
.pop()

if (ma == null) {
throw new Error('Could not detect go relay WebSocket address')
}

// dial the relay
await node.dial(ma)

// wait for a reservation to be made
await hasRelay(node)
})
})
4 changes: 4 additions & 0 deletions packages/integration-tests/test/fixtures/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RELAY_V2_HOP_CODEC } from '@libp2p/circuit-relay-v2'
import { peerIdFromString } from '@libp2p/peer-id'
import { detect } from 'detect-browser'
import pWaitFor from 'p-wait-for'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import type { Libp2p, AbortOptions, ContentRouting, PeerId, PeerInfo } from '@libp2p/interface'
Expand All @@ -8,6 +9,9 @@ import type { Multiaddr } from '@multiformats/multiaddr'
import type { CID, Version } from 'multiformats'
import type { Options as PWaitForOptions } from 'p-wait-for'

const browser = detect()
export const isFirefox = ((browser != null) && browser.name === 'firefox')

export async function usingAsRelay (node: Libp2p, relay: Libp2p, opts?: PWaitForOptions<boolean>): Promise<void> {
// Wait for peer to be used as a relay
await pWaitFor(() => {
Expand Down
Loading

0 comments on commit 440c9b3

Please sign in to comment.