diff --git a/README.md b/README.md index 18e0d38..8b60948 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,115 @@ -# js-libp2p-hop-relay-server -A out of the box libp2p relay server with HOP +# js-libp2p-hop-relay-server + +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) +[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) +[![](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/libp2p/js-libp2p-hop-relay-server/ci?label=ci&style=flat-square)](https://github.com/libp2p/js-libp2p-hop-relay-server/actions?query=branch%3Amaster+workflow%3Aci+) + +> An out of the box libp2p relay server with HOP + +## Lead Maintainer + +[Vasco Santos](https://github.com/vasco-santos) + +## Table of Contents + +- [Background](#background) +- [Usage](#usage) + - [Install](#install) + - [CLI](#cli) + - [Docker](#docker) +- [Contribute](#contribute) +- [License](#license) + +## Background + +Libp2p nodes acting as circuit relay aim to establish connectivity between libp2p nodes (e.g. IPFS nodes) that wouldn't otherwise be able to establish a direct connection to each other. + +A relay is needed in situations where nodes are behind NAT, reverse proxies, firewalls and/or simply don't support the same transports (e.g. go-libp2p vs. browser-libp2p). The circuit relay protocol exists to overcome those scenarios. Nodes with the `auto-relay` feature enabled can automatically bind themselves on a relay to listen for connections on their behalf. + +You can read more in its [SPEC](https://github.com/libp2p/specs/tree/master/relay). + +## Usage + +### Install + +```bash +> npm install --global libp2p-hop-relay-server +``` + +Now you can use the cli command `libp2p-hop-relay-server` to spawn an out of the box libp2p hop relay server. + +### CLI + +After installing the relay server, you can use its binary. It accepts several arguments: `--peerId`, `--listenMultiaddrs`, `--announceMultiaddrs`, `--metricsMultiaddr`, `--disableMetrics`, `--delegateMultiaddr` and `--disableAdvertise`. + +```sh +libp2p-hop-relay-server [--peerId ] [--listenMultiaddrs ... ] [--announceMultiaddrs ... ] [--metricsMultiaddr ] [--disableMetrics] [--delegateMultiaddr ] [--disableAdvertise] +``` + +#### PeerId + +You can create a [PeerId](https://github.com/libp2p/js-peer-id) via its [CLI](https://github.com/libp2p/js-peer-id#cli). + +```sh +libp2p-hop-relay-server --peerId id.json +``` + +#### Multiaddrs + +You can specify the libp2p rendezvous server listen and announce multiaddrs. This server is configured with [libp2p-tcp](https://github.com/libp2p/js-libp2p-tcp) and [libp2p-websockets](https://github.com/libp2p/js-libp2p-websockets) and addresses with this transports should be used. It can always be modified via the API. + +```sh +libp2p-hop-relay-server --peerId id.json --listenMultiaddrs '/ip4/127.0.0.1/tcp/15002/ws' '/ip4/127.0.0.1/tcp/8000' --announceMultiaddrs '/dns4/test.io/tcp/443/wss/p2p/12D3KooWAuEpJKhCAfNcHycKcZCv9Qy69utLAJ3MobjKpsoKbrGA' '/dns6/test.io/tcp/443/wss/p2p/12D3KooWAuEpJKhCAfNcHycKcZCv9Qy69utLAJ3MobjKpsoKbrGA' +``` + +By default it listens on `/ip4/127.0.0.1/tcp/15002/ws` and has no announce multiaddrs specified. + +#### Metrics + +Metrics are enabled by default on `/ip4/127.0.0.1/tcp/8003` via Prometheus. This address can also be modified with: + +```sh +libp2p-hop-relay-server --metricsMultiaddr '/ip4/127.0.0.1/tcp/8000' +``` + +Moreover, metrics can also be disabled with: + +```sh +libp2p-hop-relay-server --disableMetrics +``` + +#### Advertise + +The relay server will advertise its HOP capability by default, using a delegate node on `/dns4/node0.delegate.ipfs.io/tcp/443/https`. This is important for peers that will try to find HOP relays on the network to bind themselves. + +This advertise can be disabled with: + +```sh +libp2p-hop-relay-server --disableAdvertise +``` + +You can also customize the delegate node to use with: + +```sh +libp2p-hop-relay-server --delegateMultiaddr '/dns4/node1.delegate.ipfs.io/tcp/443/https' +``` + +Note: In the future this will leverage libp2p's [DHT](https://github.com/libp2p/js-libp2p-kad-dht). + +### Docker + +TODO + +## Contribute + +Feel free to join in. All welcome. Open an [issue](https://github.com/libp2p/js-libp2p-hop-relay-server/issues)! + +This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md) + +## License + +MIT - Protocol Labs 2020 \ No newline at end of file diff --git a/package.json b/package.json index 6c01cee..92e9e4f 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,58 @@ "name": "libp2p-hop-relay-server", "version": "0.0.0", "description": "A out of the box libp2p relay server with HOP", + "leadMaintainer": "Vasco Santos ", "main": "src/index.js", + "bin": { + "libp2p-hop-relay-server": "src/bin.js" + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "lint": "aegir lint", + "test": "aegir test", + "test:node": "aegir test -t node", + "test:browser": "aegir test -t browser", + "test:types": "aegir ts -p check", + "build": "aegir build", + "release": "aegir release", + "release-minor": "aegir release --type minor", + "release-major": "aegir release --type major", + "docs": "aegir docs", + "size": "aegir build -b" }, + "files": [ + "src", + "dist" + ], "repository": { "type": "git", "url": "git+https://github.com/vasco-santos/js-libp2p-hop-relay-server.git" }, "keywords": [ - "libp2p" + "libp2p", + "relay", + "auto relay", + "hop" ], - "author": "", + "author": "Vasco Santos ", "license": "MIT", "bugs": { "url": "https://github.com/vasco-santos/js-libp2p-hop-relay-server/issues" }, - "homepage": "https://github.com/vasco-santos/js-libp2p-hop-relay-server#readme" + "homepage": "https://github.com/vasco-santos/js-libp2p-hop-relay-server#readme", + "dependencies": { + "ipfs-http-client": "^48.1.2", + "libp2p": "libp2p/js-libp2p#0.30.x", + "libp2p-delegated-content-routing": "^0.8.0", + "libp2p-mplex": "^0.10.1", + "libp2p-noise": "^2.0.1", + "libp2p-tcp": "^0.15.1", + "libp2p-websockets": "^0.14.0", + "menoetius": "0.0.2", + "minimist": "^1.2.5", + "multiaddr": "^8.1.1", + "peer-id": "^0.14.2" + }, + "devDependencies": { + "aegir": "^29.1.0" + } } diff --git a/src/bin.js b/src/bin.js new file mode 100644 index 0000000..ed82289 --- /dev/null +++ b/src/bin.js @@ -0,0 +1,105 @@ +#!/usr/bin/env node + +'use strict' + +// Usage: $0 [--peerId ] [--listenMultiaddrs ... ] [--announceMultiaddrs ... ] +// [--metricsMultiaddr ] [--disableMetrics] [--delegateMultiaddr ] [--disableAdvertise] + +/* eslint-disable no-console */ + +const debug = require('debug') +const log = debug('libp2p:hop-relay:bin') + +const fs = require('fs') +const http = require('http') +const menoetius = require('menoetius') +const argv = require('minimist')(process.argv.slice(2)) + +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') + +const { getAnnounceAddresses, getListenAddresses } = require('./utils') +const createRelay = require('./index') + +async function main () { + // Metrics + let metricsServer + const metrics = !(argv.disableMetrics) + const metricsMa = multiaddr(argv.metricsMultiaddr || argv.ma || '/ip4/127.0.0.1/tcp/8003') + const metricsAddr = metricsMa.nodeAddress() + + // multiaddrs + const listenAddresses = getListenAddresses(argv) + const announceAddresses = getAnnounceAddresses(argv) + + // Should advertise + const shouldAdvertise = !(argv.disableAdvertise) + + // Delegate + let delegateOptions + if (argv.delegateMultiaddr || argv.dm) { + const delegateAddr = multiaddr(argv.delegateMultiaddr || argv.dm).toOptions() + delegateOptions = { + host: delegateAddr.host, + protocol: delegateAddr.port === '443' ? 'https' : 'http', + port: delegateAddr.port + } + } + + // PeerId + let peerId + if (argv.peerId) { + const peerData = fs.readFileSync(argv.peerId) + peerId = await PeerId.createFromJSON(JSON.parse(peerData)) + } else { + peerId = await PeerId.create() + log('You are using an automatically generated peer.') + log('If you want to keep the same address for the server you should provide a peerId with --peerId ') + } + + // Create Relay + const relay = await createRelay({ + peerId, + listenAddresses, + announceAddresses, + shouldAdvertise, + delegateOptions + }) + + relay.peerStore.on('change:multiaddrs', ({ peerId: changedPeerId, multiaddrs }) => { + if (peerId.equals(changedPeerId)) { + console.log('Relay server listening on:') + multiaddrs.forEach((m) => console.log(m)) + } + }) + + await relay.start() + + if (metrics) { + log('enabling metrics') + metricsServer = http.createServer((req, res) => { + if (req.url !== '/metrics') { + res.statusCode = 200 + res.end() + } + }) + + menoetius.instrument(metricsServer) + + metricsServer.listen(metricsAddr.port, metricsAddr.address, () => { + console.log(`metrics server listening on ${metricsAddr.port}`) + }) + } + + const stop = async () => { + console.log('Stopping...') + await relay.stop() + metricsServer && await metricsServer.close() + process.exit(0) + } + + process.on('SIGTERM', stop) + process.on('SIGINT', stop) +} + +main() diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..5e4cfec --- /dev/null +++ b/src/index.js @@ -0,0 +1,79 @@ +'use strict' + +const Libp2p = require('libp2p') +const TCP = require('libp2p-tcp') +const Websockets = require('libp2p-websockets') +const Muxer = require('libp2p-mplex') +const { NOISE: Crypto } = require('libp2p-noise') +const DelegatedContentRouting = require('libp2p-delegated-content-routing') +const ipfsHttpClient = require('ipfs-http-client') + +/** + * @typedef {import('peer-id')} PeerId + */ + +/** + * @typedef {Object} DelegateOptions + * @property {string} host + * @property {string} protocol + * @property {number} port + */ + + const defaulDelegateOptions = { + host: 'node0.delegate.ipfs.io', + protocol: 'https', + port: 443 + } + +/** + * @typedef {Object} HopRelayOptions + * @property {PeerId} peerId + * @property {DelegateOptions} [delegateOptions = defaulDelegateOptions] + * @property {string[]} [listenAddresses = []] + * @property {string[]} [announceAddresses = []] + * @property {boolean} [shouldAdvertise = true] + */ + +/** + * Create a Libp2p Relay with HOP service + * + * @param {HopRelayOptions} options + * @returns {Promise} + */ +function create ({ peerId, delegateOptions = defaulDelegateOptions, listenAddresses = [], announceAddresses = [], shouldAdvertise = true }) { + let contentRouting = [] + + if (shouldAdvertise) { + const httpClient = ipfsHttpClient(delegateOptions) + contentRouting.push(new DelegatedContentRouting(peerId, httpClient)) + } + + return Libp2p.create({ + peerId, + modules: { + transport: [Websockets, TCP], + streamMuxer: [Muxer], + connEncryption: [Crypto], + contentRouting + }, + peerId, + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + config: { + relay: { + enabled: true, // Allows you to dial and accept relayed connections. Does not make you a relay. + hop: { + enabled: true, // Allows you to be a relay for other peers + active: true // You will attempt to dial destination peers if you are not connected to them + }, + advertise: { + enabled: shouldAdvertise // Allows you to advertise the Hop service + } + } + } + }) +} + +module.exports = create diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..d7f8d40 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,41 @@ +'use strict' + +const multiaddr = require('multiaddr') + +function getAnnounceAddresses(argv) { + const announceAddr = argv.announceMultiaddrs || argv.am + const announceAddresses = announceAddr ? [multiaddr(announceAddr)] : [] + + if (argv.announceMultiaddrs || argv.am) { + const flagIndex = process.argv.findIndex((e) => e === '--announceMultiaddrs' || e === '--am') + const tmpEndIndex = process.argv.slice(flagIndex + 1).findIndex((e) => e.startsWith('--')) + const endIndex = tmpEndIndex !== -1 ? tmpEndIndex : process.argv.length - flagIndex - 1 + + for (let i = flagIndex + 1; i < flagIndex + endIndex; i++) { + announceAddresses.push(multiaddr(process.argv[i + 1])) + } + } + + return announceAddresses +} + +module.exports.getAnnounceAddresses = getAnnounceAddresses + +function getListenAddresses(argv) { + const listenAddr = argv.listenMultiaddrs || argv.lm || '/ip4/127.0.0.1/tcp/0/ws' + const listenAddresses = [multiaddr(listenAddr)] + + if (argv.listenMultiaddrs || argv.lm) { + const flagIndex = process.argv.findIndex((e) => e === '--listenMultiaddrs' || e === '--lm') + const tmpEndIndex = process.argv.slice(flagIndex + 1).findIndex((e) => e.startsWith('--')) + const endIndex = tmpEndIndex !== -1 ? tmpEndIndex : process.argv.length - flagIndex - 1 + + for (let i = flagIndex + 1; i < flagIndex + endIndex; i++) { + listenAddresses.push(multiaddr(process.argv[i + 1])) + } + } + + return listenAddresses +} + +module.exports.getListenAddresses = getListenAddresses