Skip to content

Commit

Permalink
feat: add heath check and docker file
Browse files Browse the repository at this point in the history
  • Loading branch information
achingbrain committed Sep 29, 2023
1 parent d030766 commit 3e55899
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 12 deletions.
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM node:18

WORKDIR /app
ENV CONFIG_FLAGS=""

COPY package*.json .
RUN npm ci --quiet
RUN npm run build
COPY . .
ENTRYPOINT [ "node", "dist/index.js", ${CONFIG_FLAGS} ]
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,43 @@
## Table of contents <!-- omit in toc -->

- [Bootstrap details](#bootstrap-details)
- [Requirements of a bootstrap node](#requirements-of-a-bootstrap-node)
- [Install](#install)
- [Browser `<script>` tag](#browser-script-tag)
- [License](#license)
- [Contribution](#contribution)

## Install
## Bootstrap details

```console
$ npm i @libp2p/amino-dht-bootstrapper
```
EPIC tracking issue: https://github.com/protocol/bifrost-infra/issues/2778

Find more info at https://github.com/protocol/bifrost-infra/blob/master/docs/bootstrap.md

Rust bootstrapper: https://github.com/libp2p/rust-libp2p/tree/master/misc/server

### Browser `<script>` tag
### Requirements of a bootstrap node

Loading this module through a script tag will make it's exports available as `Libp2pAminoDhtBootstrapper` in the global namespace.
* The Peer IDs of the default bootstrap nodes are set explicitly, to assert that you only trust a specific peer at a given ip or dns entry.
* This makes it harder for attackers to spoof or otherwise MITM attack, as they would also have to compromise our infrastructure to steal the private key behind the Peer ID.
* Defaults
* The default addresses for the bootstrap nodes are set in the ipfs/go-ipfs-config configuration

```html
<script src="https://unpkg.com/@libp2p/amino-dht-bootstrapper/dist/index.min.js"></script>
## Start the bootstrapper

```console
$ npx @libp2p/amino-dht-bootstrapper
```

### Configuring bootstrapper options

Options:
--config <CONFIG> Path to IPFS config file
--metrics-path <METRICS_PATH> Metric endpoint path [default: /metrics]
--enable-kademlia Whether to run the libp2p Kademlia protocol and join the IPFS DHT
--enable-autonat Whether to run the libp2p Autonat protocol
-h, --help Print help

## License

Licensed under either of
Expand Down
17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"url": "https://github.com/libp2p/js-libp2p-amino-dht-bootstrapper/issues"
},
"bin": {
"amino": "./dist/src/index.js"
"amino": "./dist/src/index.js",
"health-check": "./dist/src/health-check.js"
},
"type": "module",
"types": "./dist/src/index.d.ts",
Expand Down Expand Up @@ -122,7 +123,7 @@
"scripts": {
"clean": "aegir clean",
"lint": "aegir lint",
"build": "aegir build",
"build": "aegir build --bundle false",
"test": "aegir test",
"test:node": "aegir test -t node --cov",
"test:chrome": "aegir test -t browser --cov",
Expand All @@ -133,7 +134,17 @@
"dep-check": "aegir dep-check"
},
"dependencies": {
"libp2p": "^0.46.11"
"@chainsafe/libp2p-noise": "^13.0.1",
"@chainsafe/libp2p-yamux": "^5.0.0",
"@libp2p/kad-dht": "^10.0.7",
"@libp2p/mplex": "^9.0.6",
"@libp2p/peer-id": "^3.0.2",
"@libp2p/prometheus-metrics": "^2.0.6",
"@libp2p/tcp": "^8.0.7",
"@libp2p/websockets": "^7.0.7",
"@multiformats/multiaddr": "^12.1.7",
"libp2p": "^0.46.11",
"prom-client": "^14.2.0"
},
"devDependencies": {
"aegir": "^40.0.13"
Expand Down
93 changes: 93 additions & 0 deletions src/health-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#! /usr/bin/env node --trace-warnings
/* eslint-disable no-console */

/**
* @packageDoc
* Health-check that ensures the bootstrapper is running and dialable.
*
* This binary can be executed via the docker --health-cmd option, but we automatically execute it
* via the health-check DockerFile instruction
*
* See https://github.com/libp2p/rust-libp2p/tree/master/misc/server for more information
*
* - need to execute functionality similar to https://github.com/mxinden/libp2p-lookup/
* as used by https://github.com/libp2p/rust-libp2p/tree/master/misc/server
*/
import { parseArgs } from 'node:util'
import { createLibp2p } from 'libp2p'
import { webSockets } from '@libp2p/websockets'
import { tcp } from '@libp2p/tcp'
import { yamux } from '@chainsafe/libp2p-yamux'
import { mplex } from '@libp2p/mplex'
import { noise } from '@chainsafe/libp2p-noise'
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'
import { peerIdFromString } from '@libp2p/peer-id'
import { kadDHT } from '@libp2p/kad-dht'

// // @ts-expect-error unused
const { positionals } = parseArgs({
allowPositionals: true,
})

if (positionals.length > 1) {
throw new Error('Do not support multiple peerIds nor multiaddrs')
}

const multiaddrOrPeerId = positionals[0]
let multiaddrs: Multiaddr[] = []
let peerId = null

try {
multiaddrs = [
multiaddr(multiaddrOrPeerId)
]
} catch (e) {
// don't care about error, try peerId
console.error('Could not convert input into multiaddr')
}

try {
peerId = peerIdFromString(multiaddrOrPeerId)
} catch {
// peer id failed, maybe multiaddr didn't?
console.error('Could not convert input into PeerId')
}

if (multiaddr == null && peerId == null) {
throw new Error('Could not convert input into valid dialable artifact.')
}

const node = await createLibp2p({
transports: [
webSockets(),
tcp()
],
streamMuxers: [
yamux(),
mplex()
],
connectionEncryption: [
noise()
],
services: {
dht: kadDHT()
}
})

if (peerId != null) {
// if we receive a PeerId (which would be a string because its from command line) we need to do:
// * dht lookup
// * dial
try {
const resolvedPeer = await node.peerRouting.findPeer(peerId)
multiaddrs = resolvedPeer.multiaddrs
} catch (e) {
console.error('Could not find peer via dht lookup')
}
}

// if we receive a Multiaddr, dial it immediately
await node.dial(multiaddrs)

// it works!
process.exit(0)
85 changes: 84 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@
/* eslint-disable no-console */

import { parseArgs } from 'node:util'
import { createServer } from 'node:http'
import { createLibp2p } from 'libp2p'
import { circuitRelayServer } from 'libp2p/circuit-relay'
import { webSockets } from '@libp2p/websockets'
import { tcp } from '@libp2p/tcp'
import { kadDHT } from '@libp2p/kad-dht'
import { autoNATService } from 'libp2p/autonat'
import { yamux } from '@chainsafe/libp2p-yamux'
import { mplex } from '@libp2p/mplex'
import { noise } from '@chainsafe/libp2p-noise'
import { prometheusMetrics } from '@libp2p/prometheus-metrics'
import { register } from 'prom-client'

async function main (): Promise<void> {
const args = parseArgs({
Expand All @@ -12,11 +23,83 @@ async function main (): Promise<void> {
help: {
description: 'Show help text',
type: 'boolean'
},
config: {
description: 'Path to IPFS config file',
type: 'string'
},
metricsPath: {
description: 'Metric endpoint path',
default: '/metrics',
type: 'string'
},
metricsPort: {
description: 'Port to serve metrics',
default: '8888',
type: 'string'
},
enableKademlia: {
description: 'Whether to run the libp2p Kademlia protocol and join the IPFS DHT',
type: 'boolean'
},
enableAutonat: {
description: 'Whether to run the libp2p Autonat protocol',
type: 'boolean'
}
}
})

const node = await createLibp2p({})
if (args.values.help) {
console.info('Help!')
return
}

const services: Record<string, any> = {
relay: circuitRelayServer({
advertise: true
})
}

if (args.values.enableKademlia) {
services.dht = kadDHT()
}

if (args.values.enableAutonat) {
services.autonat = autoNATService()
}

const node = await createLibp2p({
transports: [
webSockets(),
tcp()
],
streamMuxers: [
yamux(),
mplex()
],
connectionEncryption: [
noise()
],
metrics: prometheusMetrics(),
services
})

console.info('libp2p is running')
console.info('PeerId', node.peerId.toString())

const metricsServer = createServer((req, res) => {
if (req.url === args.values.metricsPath && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/plain' })
register.metrics().then((metrics) => res.end(metrics))
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' })
res.end('Not Found')
}
})
await new Promise<void>((resolve) => metricsServer.listen(parseInt(args.values.metricsPort ?? '8888', 10), '0.0.0.0', resolve))

console.info('Metrics server listening', `0.0.0.0:${args.values.metricsPort}/${args.values.metricsPath}`)

}

main().catch(err => {
Expand Down

0 comments on commit 3e55899

Please sign in to comment.