fix: multi-repo local dev (file:../ deps, package exports, missing deps)

This commit is contained in:
Raven Scott
2026-05-21 20:14:54 -04:00
parent 76e3272ced
commit b63275c6cc
802 changed files with 94636 additions and 4 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018-2019 Mathias Buus, David Mark Clements & Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+334
View File
@@ -0,0 +1,334 @@
# hyperdht
### [See the full API docs at docs.pears.com](https://docs.pears.com/building-blocks/hyperdht)
The DHT powering Hyperswarm
```
npm install hyperdht
```
Built on top of [dht-rpc](https://github.com/mafintosh/dht-rpc).
The Hyperswarm DHT uses a series of holepunching techniques to make sure connectivity works on most networks,
and is mainly used to facilitate finding and connecting to peers using end to end encrypted Noise streams.
## Usage
To try it out, first instantiate a DHT instance
```js
import DHT from 'hyperdht'
const node = new DHT()
```
Then on one computer listen for connections
```js
// create a server to listen for secure connections
const server = node.createServer()
server.on('connection', function (socket) {
// socket is E2E encrypted between you and the other peer
console.log('Remote public key', socket.remotePublicKey)
// pipe it somewhere like any duplex stream
process.stdin.pipe(socket).pipe(process.stdout)
})
// make a ed25519 keypair to listen on
const keyPair = DHT.keyPair()
// this makes the server accept connections on this keypair
await server.listen(keyPair)
```
Then on another connect to the computer using the public key of the key-pair it is listening on
```js
// publicKey here is keyPair.publicKey from above
const socket = anotherNode.connect(publicKey)
socket.on('open', function () {
// socket fully open with the other peer
})
// pipe it somewhere like any duplex stream
process.stdin.pipe(socket).pipe(process.stdout)
```
## API
#### `const node = new DHT([options])`
Create a new DHT node.
Options include:
```js
{
// Optionally overwrite the default bootstrap servers, just need to be an array of any known dht node(s)
// Defaults to Pear.config.dht.bootstrap in a Pear app or ['[email protected]:49737', '[email protected]:49737', '[email protected]:49737'] elsewhere
// Supports suggested-IP to avoid DNS calls: [suggested-IP@]<host>:<port>
bootstrap: ['host:port'],
keyPair, // set the default key pair to use for server.listen and connect
connectionKeepAlive, // set a default keep-alive (in ms) on all opened sockets. Defaults to 5000. Set false to turn off (advanced usage).
randomPunchInterval: 20000 // set a default time for interval between punches (in ms). Defaults to 20000.
}
```
See [dht-rpc](https://github.com/mafintosh/dht-rpc) for more options as HyperDHT inherits from that.
_Note:_ The default bootstrap servers are publicly served on behalf of the commons. To run a fully isolated DHT, start one or more dht nodes with an empty bootstrap array (`new DHT({bootstrap:[]})`) and then use the addresses of those nodes as the `bootstrap` option in all other dht nodes. You'll need at least one persistent node for the network to be completely operational.
#### `keyPair = DHT.keyPair([seed])`
Use this method to generate the required keypair for DHT operations.
Returns an object with `{publicKey, secretKey}`. `publicKey` holds a public key buffer, `secretKey` holds a private key buffer.
If you pass any options they are forwarded to dht-rpc.
#### `await node.destroy([options])`
Fully destroy this DHT node.
This will also unannounce any running servers.
If you want to force close the node without waiting for the servers to unannounce pass `{ force: true }`.
#### `node = DHT.bootstrapper(port, host, [options])`
If you want to run your own Hyperswarm network use this method to easily create a bootstrap node.
## Creating P2P servers
#### `const server = node.createServer([options], [onconnection])`
Create a new server for accepting incoming encrypted P2P connections.
Options include:
```js
{
firewall (remotePublicKey, remoteHandshakePayload) {
// validate if you want a connection from remotePublicKey
// if you do return false, else return true
// remoteHandshakePayload contains their ip and some more info
return true
}
}
```
You can run servers on normal home computers, as the DHT will UDP holepunch connections for you.
#### `await server.listen(keyPair)`
Make the server listen on a keyPair.
To connect to this server use keyPair.publicKey as the connect address.
#### `server.refresh()`
Refresh the server, causing it to reannounce its address. This is automatically called on network changes.
#### `server.on('connection', socket)`
Emitted when a new encrypted connection has passed the firewall check.
`socket` is a [NoiseSecretStream](https://github.com/holepunchto/hyperswarm-secret-stream) instance.
You can check who you are connected to using `socket.remotePublicKey` and `socket.handshakeHash` contains a unique hash representing this crypto session (same on both sides).
#### `server.on('listening')`
Emitted when the server is fully listening on a keyPair.
#### `server.address()`
Returns an object containing the address of the server:
```js
{
;(host, // external IP of the server,
port, // external port of the server if predictable,
publicKey) // public key of the server
}
```
You can also get this info from `node.remoteAddress()` minus the public key.
#### `await server.close()`
Stop listening.
#### `server.on('close')`
Emitted when the server is fully closed.
## Connecting to P2P servers
#### `const socket = node.connect(remotePublicKey, [options])`
Connect to a remote server. Similar to `createServer` this performs UDP holepunching for P2P connectivity.
The remote public key can be encoded as either a buffer, a hex string or a z-base32 string.
Options include:
```js
{
nodes: [...], // optional array of close dht nodes to speed up connecting
keyPair // optional key pair to use when connection (defaults to node.defaultKeyPair)
}
```
#### `socket.on('open')`
Emitted when the encrypted connection has been fully established with the server.
#### `socket.remotePublicKey`
The public key of the remote peer.
#### `socket.publicKey`
The public key of the local socket.
## Additional peer discovery
#### `const stream = node.lookup(topic, [options])`
Look for peers in the DHT on the given topic. Topic should be a 32 byte buffer (normally a hash of something).
The returned stream looks like this
```js
{
// Who sent the response?
from: { id, host, port },
// What address they responded to (i.e. your address)
to: { host, port },
// List of peers announcing under this topic
peers: [ { publicKey, nodes: [{ host, port }, ...] } ]
}
```
To connect to the peers you should afterwards call `connect` with those public keys.
If you pass any options they are forwarded to dht-rpc.
#### `const stream = node.announce(topic, keyPair, [relayAddresses], [options])`
Announce that you are listening on a key-pair to the DHT under a specific topic.
When announcing you'll send a signed proof to peers that you own the key-pair and wish to announce under the specific topic. Optionally you can provide up to 3 nodes, indicating which DHT nodes can relay messages to you - this speeds up connects later on for other users.
An announce does a parallel lookup so the stream returned looks like the lookup stream.
Creating a server using `dht.createServer` automatically announces itself periodically on the key-pair it is listening on. When announcing the server under a specific topic, you can access the nodes it is close to using `server.nodes`.
If you pass any options they are forwarded to dht-rpc.
#### `await node.unannounce(topic, keyPair, [options])`
Unannounce a key-pair.
If you pass any options they are forwarded to dht-rpc.
## Mutable/immutable records
#### `const { hash, closestNodes } = await node.immutablePut(value, [options])`
Store an immutable value in the DHT. When successful, the hash of the value is returned.
If you pass any options they are forwarded to dht-rpc.
#### `const { value, from } = await node.immutableGet(hash, [options])`
Fetch an immutable value from the DHT. When successful, it returns the value corresponding to the hash.
If you pass any options they are forwarded to dht-rpc.
#### `const { publicKey, closestNodes, seq, signature } = await node.mutablePut(keyPair, value, [options])`
Store a mutable value in the DHT.
If you pass any options they are forwarded to dht-rpc.
#### `const { value, from, seq, signature } = await node.mutableGet(publicKey, [options])`
Fetch a mutable value from the DHT.
Options:
- `seq` - OPTIONAL, default `0`, a number which will only return values with corresponding `seq` values that are greater than or equal to the supplied `seq` option.
- `latest` - OPTIONAL - default `false`, a boolean indicating whether the query should try to find the highest seq before returning, or just the first verified value larger than `options.seq` it sees.
Any additional options you pass are forwarded to dht-rpc.
## Additional API
See [dht-rpc](https://github.com/mafintosh/dht-rpc) for the additional APIs the DHT exposes.
## CLI
You can start a DHT node in the command line:
```sh
npm install -g hyperdht
```
Run a DHT node:
```sh
hyperdht # [--port 0] [--host 0.0.0.0] [--bootstrap <comma separated list of ip:port>]
```
Or run multiple nodes:
```sh
hyperdht --nodes 5 # [--host 0.0.0.0] [--bootstrap <list>]
```
Note: by default it uses the [mainnet bootstrap nodes](lib/constants.js).
#### Isolated DHT network
To create your own DHT network is as follows:
1. Run your first bootstrap node:
```sh
hyperdht --bootstrap --host (server-ip) # [--port 49737]
```
Important: it requires the port to be open.
Now your bootstrap node is ready to use at `(server-ip):49737`, for example:
```js
const dht = new DHT({ bootstrap: ['(server-ip):49737'] })
```
Note: You could configure some DNS for the bootstrap IP addresses.
For the network to be fully operational it needs at least one persistent node.
2. Provide the first node by using your own bootstrap values:
```sh
hyperdht --port 49738 --bootstrap (server-ip):49737
```
Important: it requires the port to be open too.
You need to wait ~30 mins for the node to become persistent.
Having persistent nodes in different places makes the network more decentralized and resilient!
For more information: [`examples/isolated-dht.mjs`](examples/isolated-dht.mjs)
## License
MIT
Generated Vendored Executable
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env node
const HyperDHT = require('./')
const bootstrap = arg('bootstrap')
const nodes = arg('node') ? '' : arg('nodes')
const isBootstrap = bootstrap === '' || (bootstrap !== null && bootstrap.startsWith('--'))
if (isBootstrap) {
const port = Number(arg('port') || '0') || 49737
const host = arg('host')
if (!host) throw new Error('You need to specify --host <node ip>')
startBootstrapNode(port, host)
} else {
startNodes(Number(nodes) || 1, bootstrap ? bootstrap.split(',') : undefined)
}
function arg(name) {
const i = process.argv.indexOf('--' + name)
if (i === -1) return null
return i < process.argv.length - 1 ? process.argv[i + 1] : ''
}
async function startBootstrapNode(port, host) {
console.log('Starting DHT bootstrap node...')
const node = HyperDHT.bootstrapper(port, host)
await node.ready()
node.on('close', function () {
console.log('Bootstrap node closed')
})
console.log('Bootstrap node bound to', node.address())
console.log('Fully started Hyperswarm DHT bootstrap node')
process.once('SIGINT', function () {
node.destroy()
})
}
async function startNodes(cnt, bootstrap) {
console.log('Booting DHT nodes...')
const port = Number(arg('port') || '0') || 0
const host = arg('host') || undefined
const all = []
if (port && cnt !== 1) throw new Error('--port is only valid when running a single node')
while (all.length < cnt) {
const node = new HyperDHT({ host, port, anyPort: !port, bootstrap })
await node.ready()
all.push(node)
const id = all.push(node) - 1
console.log('Node #' + id + ' bound to', node.address())
node.on('ephemeral', function () {
console.log('Node #' + id + ' is ephemeral', node.address())
})
node.on('persistent', function () {
console.log('Node #' + id + ' is persistent, joining remote routing tables', node.address())
})
node.on('close', function () {
console.log('Node #' + id + ' closed')
})
}
console.log('Fully started ' + cnt + ' Hyperswarm DHT node' + (cnt === 1 ? '' : 's'))
process.once('SIGINT', function () {
console.log('Shutting down nodes...')
for (const node of all) {
node.destroy()
}
})
}
+15
View File
@@ -0,0 +1,15 @@
const { hash, createKeyPair } = require('./lib/crypto')
module.exports = class Stub {
constructor() {
throw new Error('hyperdht is not supported in browsers')
}
static keyPair(seed) {
return createKeyPair(seed)
}
static hash(data) {
return hash(data)
}
}
+631
View File
@@ -0,0 +1,631 @@
const DHT = require('dht-rpc')
const sodium = require('sodium-universal')
const c = require('compact-encoding')
const b4a = require('b4a')
const safetyCatch = require('safety-catch')
const m = require('./lib/messages')
const SocketPool = require('./lib/socket-pool')
const Persistent = require('./lib/persistent')
const Router = require('./lib/router')
const Cache = require('xache')
const Server = require('./lib/server')
const connect = require('./lib/connect')
const { FIREWALL, BOOTSTRAP_NODES, KNOWN_NODES, COMMANDS } = require('./lib/constants')
const { hash, createKeyPair } = require('./lib/crypto')
const RawStreamSet = require('./lib/raw-stream-set')
const ConnectionPool = require('./lib/connection-pool')
const { STREAM_NOT_CONNECTED } = require('./lib/errors')
const DEFAULTS = {
...DHT.DEFAULTS,
connectionKeepAlive: 5000,
randomPunchInterval: 20000
}
class HyperDHT extends DHT {
constructor(opts = {}) {
const port = opts.port || 49737
const bootstrap = opts.bootstrap || BOOTSTRAP_NODES
const nodes = opts.nodes || KNOWN_NODES
super({ ...opts, port, bootstrap, nodes, filterNode })
const { router, relayAddresses, persistent } = defaultCacheOpts(opts)
this.defaultKeyPair = opts.keyPair || createKeyPair(opts.seed)
this.listening = new Set()
this.connectionKeepAlive =
opts.connectionKeepAlive === false
? 0
: opts.connectionKeepAlive || DEFAULTS.connectionKeepAlive
// stats is inherited from dht-rpc so fwd the ones from there
this.stats = {
punches: { consistent: 0, random: 0, open: 0 },
relaying: { attempts: 0, successes: 0, aborts: 0 },
...this.stats
}
this.rawStreams = new RawStreamSet(this)
this.plugins = new Map()
this._router = new Router(this, router)
this._socketPool = new SocketPool(this, opts.host || '0.0.0.0')
this._persistent = null
this._validatedLocalAddresses = new Map()
this._relayAddressesCache = new Cache(relayAddresses)
this._deferRandomPunch = !!opts.deferRandomPunch
this._lastRandomPunch = this._deferRandomPunch ? Date.now() : 0
this._connectable = true
this._randomPunchInterval = opts.randomPunchInterval || DEFAULTS.randomPunchInterval // min 20s between random punches...
this._randomPunches = 0
this._randomPunchLimit = 1 // set to one for extra safety for now
this.once('persistent', () => {
this._persistent = new Persistent(this, persistent)
for (const plugin of this.plugins.values()) plugin.onpersistent()
})
this.on('network-change', () => {
for (const server of this.listening) server.refresh()
})
this.on('network-update', () => {
if (!this.online) return
for (const server of this.listening) server.notifyOnline()
})
}
static DEFAULTS = DEFAULTS
connect(remotePublicKey, opts) {
return connect(this, remotePublicKey, opts)
}
createServer(opts, onconnection) {
if (typeof opts === 'function') return this.createServer({}, opts)
if (opts && opts.onconnection) onconnection = opts.onconnection
const s = new Server(this, opts)
if (onconnection) s.on('connection', onconnection)
return s
}
pool() {
return new ConnectionPool(this)
}
async resume({ log = noop } = {}) {
if (this._deferRandomPunch) this._lastRandomPunch = Date.now()
await super.resume({ log })
const resuming = []
for (const server of this.listening) resuming.push(server.resume())
log('Resuming hyperdht servers')
await Promise.allSettled(resuming)
log('Done, hyperdht fully resumed')
}
async suspend({ log = noop } = {}) {
this._connectable = false // just so nothing gets connected during suspension
const suspending = []
for (const server of this.listening) suspending.push(server.suspend())
log('Suspending all hyperdht servers')
await Promise.allSettled(suspending)
log('Done, clearing all raw streams')
await this.rawStreams.clear()
log('Done, suspending dht-rpc')
await super.suspend({ log })
log('Done, clearing raw streams again')
await this.rawStreams.clear()
log('Done, hyperdht fully suspended')
this._connectable = true
}
async destroy({ force = false } = {}) {
if (!force) {
const closing = []
for (const server of this.listening) closing.push(server.close())
await Promise.allSettled(closing)
}
this._router.destroy()
if (this._persistent) this._persistent.destroy()
for (const plugin of this.plugins.values()) plugin.destroy()
await this.rawStreams.clear()
await this._socketPool.destroy()
await super.destroy()
}
async validateLocalAddresses(addresses) {
const list = []
const socks = []
const waiting = []
for (const addr of addresses) {
const { host } = addr
if (this._validatedLocalAddresses.has(host)) {
if (await this._validatedLocalAddresses.get(host)) {
list.push(addr)
}
continue
}
const sock = this.udx.createSocket()
try {
sock.bind(0, host)
} catch {
this._validatedLocalAddresses.set(host, Promise.resolve(false))
continue
}
socks.push(sock)
// semi terrible heuristic until we proper fix local connections by racing them to the remote...
const promise = new Promise((resolve) => {
sock.on('message', () => resolve(true))
setTimeout(() => resolve(false), 500)
sock.trySend(b4a.alloc(1), sock.address().port, addr.host)
})
this._validatedLocalAddresses.set(host, promise)
waiting.push(addr)
}
for (const addr of waiting) {
const { host } = addr
if (this._validatedLocalAddresses.has(host)) {
if (await this._validatedLocalAddresses.get(host)) {
list.push(addr)
}
continue
}
}
for (const sock of socks) await sock.close()
return list
}
findPeer(publicKey, opts = {}) {
const target = opts.hash === false ? publicKey : hash(publicKey)
opts = { ...opts, map: mapFindPeer }
return this.query({ target, command: COMMANDS.FIND_PEER, value: null }, opts)
}
lookup(target, opts = {}) {
opts = { ...opts, map: mapLookup }
return this.query({ target, command: COMMANDS.LOOKUP, value: null }, opts)
}
lookupAndUnannounce(target, keyPair, opts = {}) {
const unannounces = []
const dht = this
const userCommit = opts.commit || noop
const signUnannounce = opts.signUnannounce || Persistent.signUnannounce
if (this._persistent !== null) {
// unlink self
this._persistent.unannounce(target, keyPair.publicKey)
}
opts = { ...opts, map, commit }
return this.query({ target, command: COMMANDS.LOOKUP, value: null }, opts)
async function commit(reply, dht, query) {
await Promise.all(unannounces) // can never fail, caught below
return userCommit(reply, dht, query)
}
function map(reply) {
const data = mapLookup(reply)
if (!data || !data.token) return data
let found = data.peers.length >= 20
for (let i = 0; !found && i < data.peers.length; i++) {
found = b4a.equals(data.peers[i].publicKey, keyPair.publicKey)
}
if (!found) return data
if (!data.from.id) return data
unannounces.push(
dht
._requestUnannounce(keyPair, dht, target, data.token, data.from, signUnannounce)
.catch(safetyCatch)
)
return data
}
}
unannounce(target, keyPair, opts = {}) {
return this.lookupAndUnannounce(target, keyPair, opts).finished()
}
announce(target, keyPair, relayAddresses, opts = {}) {
const signAnnounce = opts.signAnnounce || Persistent.signAnnounce
const bump = opts.bump || 0
opts = { ...opts, commit }
return opts.clear ? this.lookupAndUnannounce(target, keyPair, opts) : this.lookup(target, opts)
function commit(reply, dht) {
return dht._requestAnnounce(
keyPair,
dht,
target,
reply.token,
reply.from,
relayAddresses,
signAnnounce,
bump
)
}
}
async immutableGet(target, opts = {}) {
opts = { ...opts, map: mapImmutable }
const query = this.query({ target, command: COMMANDS.IMMUTABLE_GET, value: null }, opts)
const check = b4a.allocUnsafe(32)
for await (const node of query) {
const { value } = node
sodium.crypto_generichash(check, value)
if (b4a.equals(check, target)) return node
}
return null
}
async immutablePut(value, opts = {}) {
const target = b4a.allocUnsafe(32)
sodium.crypto_generichash(target, value)
opts = {
...opts,
map: mapImmutable,
commit(reply, dht) {
return dht.request(
{ token: reply.token, target, command: COMMANDS.IMMUTABLE_PUT, value },
reply.from
)
}
}
const query = this.query({ target, command: COMMANDS.IMMUTABLE_GET, value: null }, opts)
await query.finished()
return { hash: target, closestNodes: query.closestNodes }
}
async mutableGet(publicKey, opts = {}) {
let refresh = opts.refresh || null
let signed = null
let result = null
opts = { ...opts, map: mapMutable, commit: refresh ? commit : null }
const target = b4a.allocUnsafe(32)
sodium.crypto_generichash(target, publicKey)
const userSeq = opts.seq || 0
const query = this.query(
{ target, command: COMMANDS.MUTABLE_GET, value: c.encode(c.uint, userSeq) },
opts
)
const latest = opts.latest !== false
for await (const node of query) {
if (result && node.seq <= result.seq) continue
if (
node.seq < userSeq ||
!Persistent.verifyMutable(node.signature, node.seq, node.value, publicKey)
)
continue
if (!latest) return node
if (!result || node.seq > result.seq) result = node
}
return result
function commit(reply, dht) {
if (!signed && result && refresh) {
if (refresh(result)) {
signed = c.encode(m.mutablePutRequest, {
publicKey,
seq: result.seq,
value: result.value,
signature: result.signature
})
} else {
refresh = null
}
}
return signed
? dht.request(
{ token: reply.token, target, command: COMMANDS.MUTABLE_PUT, value: signed },
reply.from
)
: Promise.resolve(null)
}
}
async mutablePut(keyPair, value, opts = {}) {
const signMutable = opts.signMutable || Persistent.signMutable
const target = b4a.allocUnsafe(32)
sodium.crypto_generichash(target, keyPair.publicKey)
const seq = opts.seq || 0
const signature = await signMutable(seq, value, keyPair)
const signed = c.encode(m.mutablePutRequest, {
publicKey: keyPair.publicKey,
seq,
value,
signature
})
opts = {
...opts,
map: mapMutable,
commit(reply, dht) {
return dht.request(
{ token: reply.token, target, command: COMMANDS.MUTABLE_PUT, value: signed },
reply.from
)
}
}
// use seq = 0, for the query part here, as we don't care about the actual values
const query = this.query(
{ target, command: COMMANDS.MUTABLE_GET, value: c.encode(c.uint, 0) },
opts
)
await query.finished()
return { publicKey: keyPair.publicKey, closestNodes: query.closestNodes, seq, signature }
}
onrequest(req) {
switch (req.command) {
case COMMANDS.PEER_HANDSHAKE: {
this._router.onpeerhandshake(req)
return true
}
case COMMANDS.PEER_HOLEPUNCH: {
this._router.onpeerholepunch(req)
return true
}
}
if (this._persistent === null || this.id === null) return false
switch (req.command) {
case COMMANDS.FIND_PEER: {
this._persistent.onfindpeer(req)
return true
}
case COMMANDS.LOOKUP: {
this._persistent.onlookup(req)
return true
}
case COMMANDS.ANNOUNCE: {
this._persistent.onannounce(req)
return true
}
case COMMANDS.UNANNOUNCE: {
this._persistent.onunannounce(req)
return true
}
case COMMANDS.MUTABLE_PUT: {
this._persistent.onmutableput(req)
return true
}
case COMMANDS.MUTABLE_GET: {
this._persistent.onmutableget(req)
return true
}
case COMMANDS.IMMUTABLE_PUT: {
this._persistent.onimmutableput(req)
return true
}
case COMMANDS.IMMUTABLE_GET: {
this._persistent.onimmutableget(req)
return true
}
case COMMANDS.PLUGIN: {
this._persistent.onplugin(req)
return true
}
}
return false
}
static keyPair(seed) {
return createKeyPair(seed)
}
static hash(data) {
return hash(data)
}
static connectRawStream(encryptedStream, rawStream, remoteId) {
const stream = encryptedStream.rawStream
if (!stream.connected) throw STREAM_NOT_CONNECTED()
rawStream.connect(stream.socket, remoteId, stream.remotePort, stream.remoteHost)
}
createRawStream(opts) {
return this.rawStreams.add(opts)
}
async _requestAnnounce(keyPair, dht, target, token, from, relayAddresses, sign, bump) {
const ann = {
peer: {
publicKey: keyPair.publicKey,
relayAddresses: relayAddresses || []
},
refresh: null,
signature: null,
bump
}
ann.signature = await sign(target, token, from.id, ann, keyPair)
const value = c.encode(m.announce, ann)
return dht.request(
{
token,
target,
command: COMMANDS.ANNOUNCE,
value
},
from
)
}
async _requestUnannounce(keyPair, dht, target, token, from, sign) {
const unann = {
peer: {
publicKey: keyPair.publicKey,
relayAddresses: []
},
signature: null
}
unann.signature = await sign(target, token, from.id, unann, keyPair)
const value = c.encode(m.announce, unann)
return dht.request(
{
token,
target,
command: COMMANDS.UNANNOUNCE,
value
},
from
)
}
register(name, plugin) {
this.plugins.set(name, plugin)
plugin.onregister(this)
}
}
HyperDHT.BOOTSTRAP = BOOTSTRAP_NODES
HyperDHT.FIREWALL = FIREWALL
module.exports = HyperDHT
function mapLookup(node) {
if (!node.value) return null
try {
const l = c.decode(m.lookupRawReply, node.value)
return {
token: node.token,
from: node.from,
to: node.to,
peers: l.peers,
bump: l.bump
}
} catch {
return null
}
}
function mapFindPeer(node) {
if (!node.value) return null
try {
return {
token: node.token,
from: node.from,
to: node.to,
peer: c.decode(m.peer, node.value)
}
} catch {
return null
}
}
function mapImmutable(node) {
if (!node.value) return null
return {
token: node.token,
from: node.from,
to: node.to,
value: node.value
}
}
function mapMutable(node) {
if (!node.value) return null
try {
const { seq, value, signature } = c.decode(m.mutableGetResponse, node.value)
return {
token: node.token,
from: node.from,
to: node.to,
seq,
value,
signature
}
} catch {
return null
}
}
function noop() {}
function filterNode(node) {
// always skip these testnet nodes that got mixed in by accident, until they get updated
return (
!(node.port === 49738 && (node.host === '134.209.28.98' || node.host === '167.99.142.185')) &&
!(node.port === 9400 && node.host === '35.233.47.252') &&
!(node.host === '150.136.142.116')
)
}
const defaultMaxSize = 65536
const defaultMaxAge = 20 * 60 * 1000 // 20 minutes
function defaultCacheOpts(opts) {
const maxSize = opts.maxSize || defaultMaxSize
const maxAge = opts.maxAge || defaultMaxAge
return {
router: {
forwards: { maxSize, maxAge }
},
relayAddresses: { maxSize: Math.min(maxSize, 512), maxAge: 0 },
persistent: {
records: { maxSize, maxAge },
refreshes: { maxSize, maxAge },
mutables: {
maxSize: (maxSize / 2) | 0,
maxAge: opts.maxAge || 48 * 60 * 60 * 1000 // 48 hours
},
immutables: {
maxSize: (maxSize / 2) | 0,
maxAge: opts.maxAge || 48 * 60 * 60 * 1000 // 48 hours
},
bumps: { maxSize, maxAge }
}
}
}
+303
View File
@@ -0,0 +1,303 @@
const safetyCatch = require('safety-catch')
const c = require('compact-encoding')
const Signal = require('signal-promise')
const { encodeUnslab } = require('./encode')
const Sleeper = require('./sleeper')
const m = require('./messages')
const Persistent = require('./persistent')
const { COMMANDS } = require('./constants')
const MIN_ACTIVE = 3
module.exports = class Announcer {
constructor(dht, keyPair, target, opts = {}) {
this.dht = dht
this.keyPair = keyPair
this.target = target
this.relays = []
this.relayAddresses = []
this.stopped = false
this.suspended = false
this.record = encodeUnslab(m.peer, { publicKey: keyPair.publicKey, relayAddresses: [] })
this.online = new Signal()
this._refreshing = false
this._closestNodes = null
this._active = null
this._sleeper = new Sleeper()
this._resumed = new Signal()
this._signAnnounce = opts.signAnnounce || Persistent.signAnnounce
this._signUnannounce = opts.signUnannounce || Persistent.signUnannounce
this._updating = null
this._activeQuery = null
this._unannouncing = null
this._serverRelays = [new Map(), new Map(), new Map()]
}
isRelay(addr) {
const id = addr.host + ':' + addr.port
const [a, b, c] = this._serverRelays
return a.has(id) || b.has(id) || c.has(id)
}
async suspend({ log = noop } = {}) {
if (this.suspended) return
this.suspended = true
log('Suspending announcer')
// Suspend has its own sleep logic
// so we don't want to hang on this one
this.online.notify()
if (this._activeQuery) this._activeQuery.destroy()
this._sleeper.resume()
if (this._updating) await this._updating
log('Suspending announcer (post update)')
if (this.suspended === false || this.stopped) return
log('Suspending announcer (pre unannounce)')
await this._unannounceCurrent()
log('Suspending announcer (post unannounce)')
}
resume() {
if (!this.suspended) return
this.suspended = false
this.refresh()
this._sleeper.resume()
this._resumed.notify()
}
refresh() {
if (this.stopped) return
this._refreshing = true
}
async start() {
if (this.stopped) return
this._active = this._runUpdate()
await this._active
if (this.stopped) return
this._active = this._background()
}
async stop() {
this.stopped = true
this.online.notify() // Break out of the _background loop if we're offline
this._sleeper.resume()
this._resumed.notify()
await this._active
await this._unannounceCurrent()
}
async _unannounceCurrent() {
while (this._unannouncing !== null) await this._unannouncing
const un = (this._unannouncing = this._unannounceAll(this._serverRelays[2].values()))
await this._unannouncing
if (un === this._unannouncing) this._unannouncing = null
}
async _background() {
while (!this.dht.destroyed && !this.stopped) {
try {
this._refreshing = false
// ~5min +-
for (let i = 0; i < 100 && !this.stopped && !this._refreshing && !this.suspended; i++) {
const pings = []
for (const node of this._serverRelays[2].values()) {
pings.push(this.dht.ping(node))
}
const active = await resolved(pings)
if (active < Math.min(pings.length, MIN_ACTIVE)) {
this.refresh() // we lost too many relay nodes, retry all
}
if (this.stopped) return
if (!this.suspended && !this._refreshing) await this._sleeper.pause(3000)
}
while (!this.stopped && this.suspended) await this._resumed.wait()
if (!this.stopped) await this._runUpdate()
while (!this.dht.online && !this.stopped && !this.suspended) {
// Being offline can make _background repeat very quickly
// So wait until we're back online
await this.online.wait()
}
} catch (err) {
safetyCatch(err)
}
}
}
async _runUpdate() {
this._updating = this._update()
await this._updating
this._updating = null
}
async _update() {
while (this._unannouncing) await this._unannouncing
this._cycle()
const q = (this._activeQuery = this.dht.findPeer(this.target, {
hash: false,
nodes: this._closestNodes
}))
try {
await q.finished()
} catch {
// ignore failures...
}
this._activeQuery = null
if (this.stopped || this.suspended) return
const ann = []
const replies = pickBest(q.closestReplies)
const relays = []
const relayAddresses = []
if (!this.dht.firewalled) {
const addr = this.dht.remoteAddress()
if (addr) relayAddresses.push(addr)
}
for (const msg of replies) {
ann.push(this._commit(msg, relays, relayAddresses))
}
await Promise.allSettled(ann)
if (this.stopped || this.suspended) return
this._closestNodes = q.closestNodes
this.relays = relays
this.relayAddresses = relayAddresses
const removed = []
for (const [key, value] of this._serverRelays[1]) {
if (!this._serverRelays[2].has(key)) removed.push(value)
}
await this._unannounceAll(removed)
}
_unannounceAll(relays) {
const unann = []
for (const r of relays) unann.push(this._unannounce(r))
return Promise.allSettled(unann)
}
async _unannounce(to) {
const unann = {
peer: {
publicKey: this.keyPair.publicKey,
relayAddresses: []
},
refresh: null,
signature: null
}
const { from, token, value } = await this.dht.request(
{
token: null,
command: COMMANDS.FIND_PEER,
target: this.target,
value: null
},
to
)
if (!token || !from.id || !value) return
unann.signature = await this._signUnannounce(this.target, token, from.id, unann, this.keyPair)
await this.dht.request(
{
token,
command: COMMANDS.UNANNOUNCE,
target: this.target,
value: c.encode(m.announce, unann)
},
to
)
}
async _commit(msg, relays, relayAddresses) {
const ann = {
peer: {
publicKey: this.keyPair.publicKey,
relayAddresses: []
},
refresh: null,
signature: null
}
ann.signature = await this._signAnnounce(this.target, msg.token, msg.from.id, ann, this.keyPair)
const res = await this.dht.request(
{
token: msg.token,
command: COMMANDS.ANNOUNCE,
target: this.target,
value: c.encode(m.announce, ann)
},
msg.from
)
if (res.error !== 0) return
if (relayAddresses.length < 3) relayAddresses.push({ host: msg.from.host, port: msg.from.port })
relays.push({ relayAddress: msg.from, peerAddress: msg.to })
this._serverRelays[2].set(msg.from.host + ':' + msg.from.port, msg.from)
}
_cycle() {
const tmp = this._serverRelays[0]
this._serverRelays[0] = this._serverRelays[1]
this._serverRelays[1] = this._serverRelays[2]
this._serverRelays[2] = tmp
tmp.clear()
}
}
function resolved(ps) {
let replied = 0
let ticks = ps.length + 1
return new Promise((resolve) => {
for (const p of ps) p.then(push, tick)
tick()
function push(v) {
replied++
tick()
}
function tick() {
if (--ticks === 0) resolve(replied)
}
})
}
function pickBest(replies) {
// TODO: pick the ones closest to us RTT wise
return replies.slice(0, 3)
}
function noop() {}
+889
View File
@@ -0,0 +1,889 @@
const NoiseSecretStream = require('@hyperswarm/secret-stream')
const b4a = require('b4a')
const relay = require('blind-relay')
const { isReserved, isBogon } = require('bogon')
const safetyCatch = require('safety-catch')
const unslab = require('unslab')
const Semaphore = require('./semaphore')
const NoiseWrap = require('./noise-wrap')
const SecurePayload = require('./secure-payload')
const Holepuncher = require('./holepuncher')
const Sleeper = require('./sleeper')
const { FIREWALL, ERROR } = require('./constants')
const { unslabbedHash } = require('./crypto')
const {
CANNOT_HOLEPUNCH,
HANDSHAKE_INVALID,
HOLEPUNCH_ABORTED,
HOLEPUNCH_INVALID,
HOLEPUNCH_PROBE_TIMEOUT,
HOLEPUNCH_DOUBLE_RANDOMIZED_NATS,
PEER_CONNECTION_FAILED,
PEER_NOT_FOUND,
REMOTE_ABORTED,
REMOTE_NOT_HOLEPUNCHABLE,
REMOTE_NOT_HOLEPUNCHING,
SERVER_ERROR,
SERVER_INCOMPATIBLE,
RELAY_ABORTED,
SUSPENDED
} = require('./errors')
const { decode } = require('hypercore-id-encoding')
const HyperDHTAddress = require('hyperdht-address')
module.exports = function connect(dht, publicKey, opts = {}) {
const pool = opts.pool || null
const { key, nodes: providedNodes } = HyperDHTAddress.decode(
b4a.isBuffer(publicKey) ? publicKey : decode(publicKey)
)
publicKey = key
if (pool && pool.has(publicKey)) return pool.get(publicKey)
publicKey = unslab(publicKey)
opts.relayAddresses = opts.relayAddresses || providedNodes || []
const keyPair = opts.keyPair || dht.defaultKeyPair
const relayThrough = selectRelay(opts.relayThrough || null)
const encryptedSocket = (opts.createSecretStream || defaultCreateSecretStream)(true, null, {
publicKey: keyPair.publicKey,
remotePublicKey: publicKey,
autoStart: false,
keepAlive: dht.connectionKeepAlive
})
// in case a socket is made during suspended state, destroy it immediately
if (dht.suspended || !dht._connectable) {
encryptedSocket.destroy(SUSPENDED())
return encryptedSocket
}
if (pool) pool._attachStream(encryptedSocket, false)
const id = b4a.toString(publicKey, 'hex')
const c = {
id,
dht,
session: dht.session(),
relayAddresses: opts.relayAddresses,
remoteRelayAddresses: [],
pool,
round: 0,
target: unslabbedHash(publicKey),
remotePublicKey: publicKey,
reusableSocket: !!opts.reusableSocket,
handshake: (opts.createHandshake || defaultCreateHandshake)(keyPair, publicKey),
request: null,
requesting: false,
lan: opts.localConnection !== false,
firewall: FIREWALL.UNKNOWN,
rawStream: dht.createRawStream({ framed: true, firewall }),
connect: null,
query: null,
puncher: null,
payload: null,
passiveConnectTimeout: null,
serverSocket: null,
serverAddress: null,
onsocket: null,
sleeper: new Sleeper(),
encryptedSocket,
// Relay state
relayTimeout: null,
relayThrough,
relayToken: relayThrough ? relay.token() : null,
relaySocket: null,
relayClient: null,
relayPaired: false,
relayKeepAlive: opts.relayKeepAlive || 5000
}
// If the raw stream receives an error signal pre connect (ie from the firewall hook), make sure
// to forward that to the encrypted socket for proper teardown
c.rawStream.on('error', autoDestroy)
c.rawStream.once('connect', () => {
c.rawStream.removeListener('error', autoDestroy)
})
encryptedSocket.on('close', function () {
if (c.passiveConnectTimeout) clearPassiveConnectTimeout(c)
if (c.query) c.query.destroy()
if (c.puncher) c.puncher.destroy()
if (c.rawStream) c.rawStream.destroy()
c.session.destroy()
c.sleeper.resume()
})
// Safe to run in the background - never throws
if (dht.suspended) encryptedSocket.destroy(SUSPENDED())
else connectAndHolepunch(c, opts)
return encryptedSocket
function autoDestroy(err) {
maybeDestroyEncryptedSocket(c, err)
}
function firewall(socket, port, host) {
// Check if the traffic originated from the socket on which we're expecting relay traffic. If so,
// we haven't hole punched yet and the other side is just sending us traffic through the relay.
if (c.relaySocket && isRelay(c.relaySocket, socket, port, host)) {
return false
}
if (c.onsocket) {
c.onsocket(socket, port, host)
} else {
c.serverSocket = socket
c.serverAddress = { port, host }
}
return false
}
}
function isDone(c) {
// we are destroying or the puncher is connected - done
if (c.encryptedSocket.destroying || !!(c.puncher && c.puncher.connected)) {
return true
}
// not destroying, but no raw stream - def not done
if (c.encryptedSocket.rawStream === null) {
return false
}
// we are relayed, but the puncher is not done yet
if (c.relaySocket && !!(c.puncher && !c.puncher.connected && !c.puncher.destroyed)) {
return false
}
// we are done
return true
}
async function retryRoute(c, route) {
const ref = c.dht._socketPool.lookup(route.socket)
if (!ref) {
if (route.socket === c.dht.socket) {
await connectThroughNode(c, route.address, c.dht.socket)
}
return
}
ref.active()
try {
await connectThroughNode(c, route.address, route.socket)
} catch {
// if error, just ignore, and continue through the existing strat
}
ref.inactive()
}
async function connectAndHolepunch(c, opts) {
const route = c.reusableSocket ? c.dht._socketPool.routes.get(c.remotePublicKey) : null
if (route) {
await retryRoute(c, route)
if (isDone(c)) return
}
await findAndConnect(c, opts)
if (isDone(c)) return
if (!c.connect) {
// TODO: just a quick fix for now, should retry prob
maybeDestroyEncryptedSocket(c, HANDSHAKE_INVALID())
return
}
await holepunch(c, opts)
}
function getFirstRemoteAddress(addrs, serverAddress) {
for (const addr of addrs) {
if (isBogon(addr.host)) continue
return addr
}
return serverAddress
}
async function holepunch(c, opts) {
let { relayAddress, serverAddress, clientAddress, payload } = c.connect
const remoteHolepunchable = !!(payload.holepunch && payload.holepunch.relays.length)
const relayed = diffAddress(serverAddress, relayAddress)
if (payload.firewall === FIREWALL.OPEN || (relayed && !remoteHolepunchable)) {
const addr = getFirstRemoteAddress(payload.addresses4, serverAddress)
if (addr) {
const socket = c.dht.socket
c.dht.stats.punches.open++
c.onsocket(socket, addr.port, addr.host)
return
}
// TODO: check all addresses also obvs
}
const onabort = () => {
c.session.destroy()
maybeDestroyEncryptedSocket(c, HOLEPUNCH_ABORTED())
}
if (c.firewall === FIREWALL.OPEN) {
c.passiveConnectTimeout = setTimeout(onabort, 10000)
return
}
// TODO: would be better to just try local addrs in the background whilst continuing with other strategies...
if (c.lan && relayed && clientAddress.host === serverAddress.host) {
const serverAddresses = payload.addresses4.filter(onlyNonReserved)
if (serverAddresses.length > 0) {
const myAddresses = Holepuncher.localAddresses(c.dht.io.serverSocket)
const addr = Holepuncher.matchAddress(myAddresses, serverAddresses) || serverAddresses[0]
const socket = c.dht.io.serverSocket
try {
await c.dht.ping(addr)
} catch {
maybeDestroyEncryptedSocket(c, HOLEPUNCH_ABORTED())
return
}
c.onsocket(socket, addr.port, addr.host)
return
}
}
if (!remoteHolepunchable) {
maybeDestroyEncryptedSocket(c, CANNOT_HOLEPUNCH())
return
}
c.puncher = new Holepuncher(c.dht, c.session, true, payload.firewall)
c.puncher.onconnect = c.onsocket
c.puncher.onabort = onabort
const serverRelay = pickServerRelay(payload.holepunch.relays, relayAddress)
// Begin holepunching!
let probe
try {
probe = await probeRound(c, opts.fastOpen === false ? null : serverAddress, serverRelay, true)
} catch (err) {
destroyPuncher(c)
// TODO: we should retry here with some of the other relays, bail for now
maybeDestroyEncryptedSocket(c, err)
return
}
if (isDone(c) || !probe) return
const { token, peerAddress } = probe
// If the relay the server picked is the same as the relay the client picked,
// then we can use the peerAddress that round one indicates the server wants to use.
// This shaves off a roundtrip if the server chose to reroll its socket due to some NAT
// issue with the first one it picked (ie mobile nat inconsistencies...).
// If the relays were different, then the server would not have a UDP session open on this address
// to the client relay, which round2 uses.
if (
!diffAddress(serverRelay.relayAddress, relayAddress) &&
diffAddress(serverAddress, peerAddress)
) {
serverAddress = peerAddress
await c.puncher.openSession(serverAddress)
if (isDone(c)) return
}
// TODO: still continue here if a local connection might work, but then do not holepunch...
if (
opts.holepunch &&
!opts.holepunch(
c.puncher.remoteFirewall,
c.puncher.nat.firewall,
c.puncher.remoteAddresses,
c.puncher.nat.addresses
)
) {
await abort(c, serverRelay, HOLEPUNCH_ABORTED('Client aborted holepunch'))
return
}
try {
await roundPunch(c, serverAddress, token, relayAddress, serverRelay, false)
} catch (err) {
destroyPuncher(c)
// TODO: retry with another relay?
maybeDestroyEncryptedSocket(c, err)
}
}
async function connectThroughNodes(c, addresses, socket) {
for (const address of addresses) {
if (isDone(c) || c.connect) return
c.remoteRelayAddresses.push(address)
await connectThroughNode(c, address, socket)
}
}
async function findAndConnect(c, opts) {
let attempts = 0
let relayAddresses =
opts.relayAddresses && opts.relayAddresses.length ? opts.relayAddresses : null
if (!relayAddresses) {
const cachedRelayAddresses = c.dht._relayAddressesCache.get(c.id)
if (cachedRelayAddresses) relayAddresses = cachedRelayAddresses
}
if (c.dht._persistent) {
// check if we know the route ourself...
const route = c.dht._router.get(c.target)
if (route && route.relay !== null) {
relayAddresses = [{ host: route.relay.host, port: route.relay.port }]
}
}
// 2 is how many parallel connect attempts we want to do, we can make this configurable
const preConnect = relayAddresses !== null && relayAddresses.length > 0
const sem = new Semaphore(preConnect ? 3 : 2)
const signal = sem.signal.bind(sem)
const tries = relayAddresses !== null ? 2 : 1
if (preConnect) {
await sem.wait()
connectThroughNodes(c, relayAddresses, null).then(signal, signal)
}
try {
for (let i = 0; i < tries && !isDone(c) && !c.connect; i++) {
c.query = c.dht.findPeer(c.target, {
hash: false,
session: c.session,
nodes: relayAddresses,
retries: 3
})
for await (const data of c.query) {
await sem.wait()
if (isDone(c)) return
if (c.connect) {
sem.signal()
break
}
// Skip node already run via preConnect
if (preConnect && relayAddresses && isRelayAddress(relayAddresses, data)) {
sem.signal()
continue
}
c.remoteRelayAddresses.push(data.from)
attempts++
connectThroughNode(c, data.from, null).then(signal, signal)
}
relayAddresses = null
if (attempts > 0) await sem.flush()
}
c.query = null
if (isDone(c)) return
// flush the semaphore
await sem.flush()
if (isDone(c)) return
} catch (err) {
c.query = null
maybeDestroyEncryptedSocket(c, err)
return
}
if (!c.connect) {
maybeDestroyEncryptedSocket(c, attempts ? PEER_CONNECTION_FAILED() : PEER_NOT_FOUND())
}
}
async function connectThroughNode(c, address, socket) {
if (!c.requesting) {
// If we have a stable server address, send it over now
const addr = c.dht.remoteAddress()
const localAddrs = c.lan ? Holepuncher.localAddresses(c.dht.io.serverSocket) : null
const addresses4 = []
if (addr) addresses4.push(addr)
if (localAddrs) addresses4.push(...localAddrs)
c.firewall = addr ? FIREWALL.OPEN : FIREWALL.UNKNOWN
c.requesting = true
c.request = await c.handshake.send({
error: ERROR.NONE,
firewall: c.firewall,
holepunch: null,
addresses4,
addresses6: [],
udx: {
reusableSocket: c.reusableSocket,
id: c.rawStream.id,
seq: 0
},
secretStream: {},
relayThrough: c.relayThrough ? { publicKey: c.relayThrough, token: c.relayToken } : null
})
if (isDone(c)) return
}
const { serverAddress, clientAddress, relayed, noise } = await c.dht._router.peerHandshake(
c.target,
{ noise: c.request, socket, session: c.session },
address
)
if (isDone(c) || c.connect) return
const payload = await c.handshake.recv(noise)
if (isDone(c) || !payload) return
if (payload.version !== 1) {
maybeDestroyEncryptedSocket(c, SERVER_INCOMPATIBLE())
return
}
if (payload.error !== ERROR.NONE) {
maybeDestroyEncryptedSocket(c, SERVER_ERROR())
return
}
if (!payload.udx) {
maybeDestroyEncryptedSocket(c, SERVER_ERROR('Server did not send UDX data'))
return
}
const hs = c.handshake.final()
c.handshake = null
c.request = null
c.requesting = false
c.connect = {
relayed,
relayAddress: address,
clientAddress,
serverAddress,
payload
}
c.payload = new SecurePayload(hs.holepunchSecret)
c.onsocket = function (socket, port, host) {
if (c.rawStream === null) return // Already hole punched
if (c.rawStream.connected) {
const remoteChanging = c.rawStream.changeRemote(socket, c.connect.payload.udx.id, port, host)
if (remoteChanging) remoteChanging.catch(safetyCatch)
} else {
// cache the relay addrs for a future reconnect, we prefer the remote one so they
// can give us the correct ones from their pov
if (payload.relayAddresses && payload.relayAddresses.length) {
c.dht._relayAddressesCache.set(c.id, payload.relayAddresses)
} else if (c.remoteRelayAddresses.length) {
c.dht._relayAddressesCache.set(c.id, c.remoteRelayAddresses)
}
c.rawStream.connect(socket, c.connect.payload.udx.id, port, host)
c.encryptedSocket.start(c.rawStream, { handshake: hs })
}
if (c.reusableSocket && payload.udx.reusableSocket) {
c.dht._socketPool.routes.add(c.remotePublicKey, c.rawStream)
}
if (c.puncher) {
c.puncher.onabort = noop
c.puncher.destroy()
}
if (c.passiveConnectTimeout) {
clearPassiveConnectTimeout(c)
}
c.rawStream = null
}
if (payload.relayThrough || c.relayThrough) {
relayConnection(c, c.relayThrough, payload, hs)
}
if (c.serverSocket) {
c.onsocket(c.serverSocket, c.serverAddress.port, c.serverAddress.host)
return
}
if (!relayed) {
c.onsocket(socket || c.dht.socket, address.port, address.host)
}
c.session.destroy()
}
async function updateHolepunch(c, peerAddress, relayAddr, payload) {
const holepunch = await c.dht._router.peerHolepunch(
c.target,
{
id: c.connect.payload.holepunch.id,
payload: c.payload.encrypt(payload),
peerAddress,
socket: c.puncher.socket,
session: c.session
},
relayAddr
)
if (isDone(c)) return null
const remotePayload = c.payload.decrypt(holepunch.payload)
if (!remotePayload) {
throw HOLEPUNCH_INVALID()
}
const { error, firewall, punching, addresses, remoteToken } = remotePayload
if (error === ERROR.TRY_LATER && c.relayToken && payload.punching) {
return {
tryLater: true,
...holepunch,
payload: remotePayload
}
}
if (error !== ERROR.NONE) {
throw REMOTE_ABORTED('Remote aborted with error code ' + error)
}
const echoed = !!(remoteToken && payload.token && b4a.equals(remoteToken, payload.token))
c.puncher.updateRemote({
punching,
firewall,
addresses,
verified: echoed ? peerAddress.host : null
})
return {
tryLater: false,
...holepunch,
payload: remotePayload
}
}
async function probeRound(c, serverAddress, serverRelay, retry) {
// Open a quick low ttl session against what we think is the server
if (serverAddress) await c.puncher.openSession(serverAddress)
if (isDone(c)) return null
const reply = await updateHolepunch(c, serverRelay.peerAddress, serverRelay.relayAddress, {
error: ERROR.NONE,
firewall: c.puncher.nat.firewall,
round: c.round++,
connected: false,
punching: false,
addresses: c.puncher.nat.addresses,
remoteAddress: serverAddress,
token: null,
remoteToken: null
})
if (isDone(c) || !reply) return null
const { peerAddress } = reply
const { address, token } = reply.payload
c.puncher.nat.add(reply.to, reply.from)
// Open another quick low ttl session against what the server says their address is,
// if they haven't said they are random yet
if (
c.puncher.remoteFirewall < FIREWALL.RANDOM &&
address &&
address.host &&
address.port &&
diffAddress(address, serverAddress)
) {
await c.puncher.openSession(address)
if (isDone(c)) return null
}
// If the remote told us they didn't know their nat firewall yet, give them a chance to figure it out
// They might say this to see if the "fast mode" punch comes through first.
if (c.puncher.remoteFirewall === FIREWALL.UNKNOWN) {
await c.sleeper.pause(1000)
if (isDone(c)) return null
}
let stable = await c.puncher.analyze(false)
if (isDone(c)) return null
// If the socket seems unstable, try to make it stable by setting the "allowReopen" flag
// Mostly relevant for mobile networks
if (!stable) {
stable = await c.puncher.analyze(true)
if (isDone(c)) return null
if (stable) return probeRound(c, serverAddress, serverRelay, false)
}
if ((c.puncher.remoteFirewall === FIREWALL.UNKNOWN || !token) && retry) {
return probeRound(c, serverAddress, serverRelay, false)
}
if (
c.puncher.remoteFirewall === FIREWALL.UNKNOWN ||
c.puncher.nat.firewall === FIREWALL.UNKNOWN
) {
await abort(c, serverRelay, HOLEPUNCH_PROBE_TIMEOUT())
return null
}
if (c.puncher.remoteFirewall >= FIREWALL.RANDOM && c.puncher.nat.firewall >= FIREWALL.RANDOM) {
await abort(c, serverRelay, HOLEPUNCH_DOUBLE_RANDOMIZED_NATS())
return null
}
return { token, peerAddress }
}
async function roundPunch(c, serverAddress, remoteToken, clientRelay, serverRelay, delayed) {
// We are gossiping our final NAT status to the other peer now
// so make sure we don't update our local view for now as that can make things weird
c.puncher.nat.freeze()
const isRandom =
c.puncher.remoteFirewall >= FIREWALL.RANDOM || c.puncher.nat.firewall >= FIREWALL.RANDOM
if (isRandom) {
while (
c.dht._randomPunches >= c.dht._randomPunchLimit ||
Date.now() - c.dht._lastRandomPunch < c.dht._randomPunchInterval
) {
// if no relay can help, bail
if (!c.relayToken) throw HOLEPUNCH_ABORTED()
if (!delayed) {
delayed = true
await updateHolepunch(c, serverAddress, clientRelay, {
error: ERROR.NONE,
firewall: c.puncher.nat.firewall,
round: c.round++,
connected: false,
punching: false,
addresses: c.puncher.nat.addresses,
remoteAddress: null,
token: c.payload.token(serverAddress),
remoteToken
})
if (isDone(c)) return
}
await tryLater(c)
if (isDone(c)) return
}
}
// increment now, so we can commit to punching
if (isRandom) c.dht._randomPunches++
let reply
try {
// if delayed switch to the servers chosen relay - we validated anyway
reply = await updateHolepunch(
c,
delayed ? serverRelay.peerAddress : serverAddress,
delayed ? serverRelay.relayAddress : clientRelay,
{
error: ERROR.NONE,
firewall: c.puncher.nat.firewall,
round: c.round++,
connected: false,
punching: true,
addresses: c.puncher.nat.addresses,
remoteAddress: null,
token: delayed ? null : c.payload.token(serverAddress),
remoteToken
}
)
} finally {
// decrement as punch increments for us
if (isRandom) c.dht._randomPunches--
}
if (isDone(c)) return
if (!reply) return
if (reply.tryLater) {
await tryLater(c)
if (isDone(c)) return
return roundPunch(c, serverAddress, remoteToken, clientRelay, serverRelay, true)
}
if (!c.puncher.remoteHolepunching) {
throw REMOTE_NOT_HOLEPUNCHING()
}
if (!(await c.puncher.punch())) {
throw REMOTE_NOT_HOLEPUNCHABLE()
}
}
async function tryLater(c) {
if (!c.relayToken) throw HOLEPUNCH_ABORTED()
await c.sleeper.pause(10000 + Math.round(Math.random() * 10000))
}
function maybeDestroyEncryptedSocket(c, err) {
if (isDone(c)) return
if (c.encryptedSocket.rawStream) return
if (c.relaySocket) return // waiting for the relay
if (c.puncher && !c.puncher.destroyed) return // waiting for the puncher
c.session.destroy()
c.encryptedSocket.destroy(err)
}
async function abort(c, { peerAddress, relayAddress }, err) {
try {
await updateHolepunch(c, peerAddress, relayAddress, {
error: ERROR.ABORTED,
firewall: FIREWALL.UNKNOWN,
round: c.round++,
connected: false,
punching: false,
addresses: null,
remoteAddress: null,
token: null,
remoteToken: null
})
} catch (err) {
safetyCatch(err)
}
destroyPuncher(c)
maybeDestroyEncryptedSocket(c, err)
}
function relayConnection(c, relayThrough, payload, hs) {
let isInitiator
let publicKey
let token
if (payload.relayThrough) {
isInitiator = false
publicKey = payload.relayThrough.publicKey
token = payload.relayThrough.token
} else {
isInitiator = true
publicKey = relayThrough
token = c.relayToken
}
c.relayToken = token
c.relaySocket = c.dht.connect(publicKey)
c.relaySocket.setKeepAlive(c.relayKeepAlive)
c.relayClient = relay.Client.from(c.relaySocket, { id: c.relaySocket.publicKey })
c.relayTimeout = setTimeout(onabort, 15000, null)
c.relayClient.pair(isInitiator, token, c.rawStream).on('error', onabort).on('data', ondata)
function ondata(remoteId) {
if (c.relayTimeout) clearRelayTimeout(c)
if (c.rawStream === null) {
onabort(null)
return
}
c.relayPaired = true
const { remotePort, remoteHost, socket } = c.relaySocket.rawStream
c.rawStream
.on('close', () => c.relaySocket.destroy())
.connect(socket, remoteId, remotePort, remoteHost)
c.encryptedSocket.start(c.rawStream, { handshake: hs })
}
function onabort(err) {
if (c.relayTimeout) clearRelayTimeout(c)
const socket = c.relaySocket
c.relayToken = null
c.relaySocket = null
if (socket) socket.destroy()
maybeDestroyEncryptedSocket(c, err || RELAY_ABORTED())
}
}
function clearPassiveConnectTimeout(c) {
clearTimeout(c.passiveConnectTimeout)
c.passiveConnectTimeout = null
}
function clearRelayTimeout(c) {
clearTimeout(c.relayTimeout)
c.relayTimeout = null
}
function destroyPuncher(c) {
if (c.puncher) c.puncher.destroy()
c.session.destroy()
}
function pickServerRelay(relays, clientRelay) {
for (const r of relays) {
if (!diffAddress(r.relayAddress, clientRelay)) return r
}
return relays[0]
}
function diffAddress(a, b) {
return a.host !== b.host || a.port !== b.port
}
function defaultCreateHandshake(keyPair, remotePublicKey) {
return new NoiseWrap(keyPair, remotePublicKey)
}
function defaultCreateSecretStream(isInitiator, rawStream, opts) {
return new NoiseSecretStream(isInitiator, rawStream, opts)
}
function onlyNonReserved(addr) {
return !isReserved(addr.host)
}
function isRelay(relaySocket, socket, port, host) {
const stream = relaySocket.rawStream
if (!stream) return false
if (stream.socket !== socket) return false
return port === stream.remotePort && host === stream.remoteHost
}
function selectRelay(relayThrough) {
if (typeof relayThrough === 'function') relayThrough = relayThrough()
if (relayThrough === null) return null
if (Array.isArray(relayThrough))
return relayThrough[Math.floor(Math.random() * relayThrough.length)]
return relayThrough
}
function isRelayAddress(relayAddresses, data) {
for (const node of relayAddresses) {
if (node.host === data.from.host && node.port === data.from.port) return true
}
return false
}
function noop() {}
+142
View File
@@ -0,0 +1,142 @@
const EventEmitter = require('events')
const b4a = require('b4a')
const errors = require('./errors')
module.exports = class ConnectionPool extends EventEmitter {
constructor(dht) {
super()
this._dht = dht
this._servers = new Map()
this._connecting = new Map()
this._connections = new Map()
}
_attachServer(server) {
const keyString = b4a.toString(server.publicKey, 'hex')
this._servers.set(keyString, server)
server
.on('close', () => {
this._servers.delete(keyString)
})
.on('connection', (socket) => {
this._attachStream(socket, true)
})
}
_attachStream(stream, opened) {
const existing = this.get(stream.remotePublicKey)
if (existing) {
const keepNew =
stream.isInitiator === existing.isInitiator ||
b4a.compare(stream.publicKey, stream.remotePublicKey) > 0
if (keepNew) {
let closed = false
const onclose = () => {
closed = true
}
existing
.on('error', noop)
.on('close', () => {
if (closed) return
stream.off('error', noop).off('close', onclose)
this._attachStream(stream, opened)
})
.destroy(errors.DUPLICATE_CONNECTION())
stream.on('error', noop).on('close', onclose)
} else {
stream.on('error', noop).destroy(errors.DUPLICATE_CONNECTION())
}
return
}
const session = new ConnectionRef(this, stream)
const keyString = b4a.toString(stream.remotePublicKey, 'hex')
if (opened) {
this._connections.set(keyString, session)
stream.on('close', () => {
this._connections.delete(keyString)
})
this.emit('connection', stream, session)
} else {
this._connecting.set(keyString, session)
stream
.on('error', noop)
.on('close', () => {
if (opened) this._connections.delete(keyString)
else this._connecting.delete(keyString)
})
.on('open', () => {
opened = true
this._connecting.delete(keyString)
this._connections.set(keyString, session)
stream.off('error', noop)
this.emit('connection', stream, session)
})
}
return session
}
get connecting() {
return this._connecting.size
}
get connections() {
return this._connections.values()
}
has(publicKey) {
const keyString = b4a.toString(publicKey, 'hex')
return this._connections.has(keyString) || this._connecting.has(keyString)
}
get(publicKey) {
const keyString = b4a.toString(publicKey, 'hex')
const existing = this._connections.get(keyString) || this._connecting.get(keyString)
return existing?._stream || null
}
}
class ConnectionRef {
constructor(pool, stream) {
this._pool = pool
this._stream = stream
this._refs = 0
}
active() {
this._refs++
}
inactive() {
this._refs--
}
release() {
this._stream.destroy()
}
}
function noop() {}
+66
View File
@@ -0,0 +1,66 @@
const crypto = require('hypercore-crypto')
const COMMANDS = (exports.COMMANDS = {
PEER_HANDSHAKE: 0,
PEER_HOLEPUNCH: 1,
FIND_PEER: 2,
LOOKUP: 3,
ANNOUNCE: 4,
UNANNOUNCE: 5,
MUTABLE_PUT: 6,
MUTABLE_GET: 7,
IMMUTABLE_PUT: 8,
IMMUTABLE_GET: 9,
PLUGIN: 10
})
exports.BOOTSTRAP_NODES = global.Pear?.config.dht?.bootstrap || [
'[email protected]:49737',
'[email protected]:49737',
'[email protected]:49737'
]
exports.KNOWN_NODES = global.Pear?.config.dht?.nodes || []
exports.FIREWALL = {
UNKNOWN: 0,
OPEN: 1,
CONSISTENT: 2,
RANDOM: 3
}
exports.ERROR = {
// noise / connection related
NONE: 0,
ABORTED: 1,
VERSION_MISMATCH: 2,
TRY_LATER: 3,
// dht related
SEQ_REUSED: 16,
SEQ_TOO_LOW: 17
}
const [
NS_ANNOUNCE,
NS_UNANNOUNCE,
NS_MUTABLE_PUT,
NS_PEER_HANDSHAKE,
NS_PEER_HOLEPUNCH,
NS_PLUGIN
] = crypto.namespace('hyperswarm/dht', [
COMMANDS.ANNOUNCE,
COMMANDS.UNANNOUNCE,
COMMANDS.MUTABLE_PUT,
COMMANDS.PEER_HANDSHAKE,
COMMANDS.PEER_HOLEPUNCH,
COMMANDS.PLUGIN
])
exports.NS = {
ANNOUNCE: NS_ANNOUNCE,
UNANNOUNCE: NS_UNANNOUNCE,
MUTABLE_PUT: NS_MUTABLE_PUT,
PEER_HANDSHAKE: NS_PEER_HANDSHAKE,
PEER_HOLEPUNCH: NS_PEER_HOLEPUNCH,
PLUGIN: NS_PLUGIN
}
+28
View File
@@ -0,0 +1,28 @@
const sodium = require('sodium-universal')
const b4a = require('b4a')
function hash(data) {
const out = b4a.allocUnsafe(32)
sodium.crypto_generichash(out, data)
return out
}
function unslabbedHash(data) {
const out = b4a.allocUnsafeSlow(32)
sodium.crypto_generichash(out, data)
return out
}
function createKeyPair(seed) {
const publicKey = b4a.alloc(32)
const secretKey = b4a.alloc(64)
if (seed) sodium.crypto_sign_seed_keypair(publicKey, secretKey, seed)
else sodium.crypto_sign_keypair(publicKey, secretKey)
return { publicKey, secretKey }
}
module.exports = {
hash,
unslabbedHash,
createKeyPair
}
+16
View File
@@ -0,0 +1,16 @@
const b4a = require('b4a')
const cenc = require('compact-encoding')
function encodeUnslab(enc, m) {
// Faster than unslab(c.encode(enc, data)) because it avoids the mem copy.
// Makes sense to put in compact-encoding when we need it in other modules too
const state = cenc.state()
enc.preencode(state, m)
state.buffer = b4a.allocUnsafeSlow(state.end)
enc.encode(state, m)
return state.buffer
}
module.exports = {
encodeUnslab
}
+110
View File
@@ -0,0 +1,110 @@
module.exports = class DHTError extends Error {
constructor(msg, code, fn = DHTError) {
super(`${code}: ${msg}`)
this.code = code
if (Error.captureStackTrace) {
Error.captureStackTrace(this, fn)
}
}
get name() {
return 'DHTError'
}
static BAD_HANDSHAKE_REPLY(msg = 'Bad handshake reply') {
return new DHTError(msg, 'BAD_HANDSHAKE_REPLY', DHTError.BAD_HANDSHAKE_REPLY)
}
static BAD_HOLEPUNCH_REPLY(msg = 'Bad holepunch reply') {
return new DHTError(msg, 'BAD_HOLEPUNCH_REPLY', DHTError.BAD_HOLEPUNCH_REPLY)
}
static HOLEPUNCH_ABORTED(msg = 'Holepunch aborted') {
return new DHTError(msg, 'HOLEPUNCH_ABORTED', DHTError.HOLEPUNCH_ABORTED)
}
static HOLEPUNCH_INVALID(msg = 'Invalid holepunch payload') {
return new DHTError(msg, 'HOLEPUNCH_INVALID', DHTError.HOLEPUNCH_INVALID)
}
static HOLEPUNCH_PROBE_TIMEOUT(msg = 'Holepunching probe did not finish in time') {
return new DHTError(msg, 'HOLEPUNCH_PROBE_TIMEOUT', DHTError.HOLEPUNCH_PROBE_TIMEOUT)
}
static HOLEPUNCH_DOUBLE_RANDOMIZED_NATS(msg = 'Both remote and local NATs are randomized') {
return new DHTError(
msg,
'HOLEPUNCH_DOUBLE_RANDOMIZED_NATS',
DHTError.HOLEPUNCH_DOUBLE_RANDOMIZED_NATS
)
}
static CANNOT_HOLEPUNCH(msg = 'Cannot holepunch to remote') {
return new DHTError(msg, 'CANNOT_HOLEPUNCH', DHTError.CANNOT_HOLEPUNCH)
}
static REMOTE_NOT_HOLEPUNCHING(msg = 'Remote is not holepunching') {
return new DHTError(msg, 'REMOTE_NOT_HOLEPUNCHING', DHTError.REMOTE_NOT_HOLEPUNCHING)
}
static REMOTE_NOT_HOLEPUNCHABLE(msg = 'Remote is not holepunchable') {
return new DHTError(msg, 'REMOTE_NOT_HOLEPUNCHABLE', DHTError.REMOTE_NOT_HOLEPUNCHABLE)
}
static REMOTE_ABORTED(msg = 'Remote aborted') {
return new DHTError(msg, 'REMOTE_ABORTED', DHTError.REMOTE_ABORTED)
}
static HANDSHAKE_UNFINISHED(msg = 'Handshake did not finish') {
return new DHTError(msg, 'HANDSHAKE_UNFINISHED', DHTError.HANDSHAKE_UNFINISHED)
}
static HANDSHAKE_INVALID(msg = 'Received invalid handshake') {
return new DHTError(msg, 'HANDSHAKE_INVALID', DHTError.HANDSHAKE_INVALID)
}
static ALREADY_LISTENING(msg = 'Already listening') {
return new DHTError(msg, 'ALREADY_LISTENING', DHTError.ALREADY_LISTENING)
}
static KEYPAIR_ALREADY_USED(msg = 'Keypair already used') {
return new DHTError(msg, 'KEYPAIR_ALREADY_USED', DHTError.KEYPAIR_ALREADY_USED)
}
static NODE_DESTROYED(msg = 'Node destroyed') {
return new DHTError(msg, 'NODE_DESTROYED', DHTError.NODE_DESTROYED)
}
static PEER_CONNECTION_FAILED(msg = 'Could not connect to peer') {
return new DHTError(msg, 'PEER_CONNECTION_FAILED', DHTError.PEER_CONNECTION_FAILED)
}
static PEER_NOT_FOUND(msg = 'Peer not found') {
return new DHTError(msg, 'PEER_NOT_FOUND', DHTError.PEER_NOT_FOUND)
}
static STREAM_NOT_CONNECTED(msg = 'Stream is not connected') {
return new DHTError(msg, 'STREAM_NOT_CONNECTED', DHTError.STREAM_DISCONNECTED)
}
static SERVER_INCOMPATIBLE(msg = 'Server is using an incompatible version') {
return new DHTError(msg, 'SERVER_INCOMPATIBLE', DHTError.SERVER_INCOMPATIBLE)
}
static SERVER_ERROR(msg = 'Server returned an error') {
return new DHTError(msg, 'SERVER_ERROR', DHTError.SERVER_ERROR)
}
static DUPLICATE_CONNECTION(msg = 'Duplicate connection') {
return new DHTError(msg, 'DUPLICATE_CONNECTION', DHTError.DUPLICATE_CONNECTION)
}
static RELAY_ABORTED(msg = 'Relay aborted') {
return new DHTError(msg, 'RELAY_ABORTED', DHTError.RELAY_ABORTED)
}
static SUSPENDED(msg = 'Suspended') {
return new DHTError(msg, 'SUSPENDED', DHTError.SUSPENDED)
}
}
+388
View File
@@ -0,0 +1,388 @@
const b4a = require('b4a')
const Nat = require('./nat')
const Sleeper = require('./sleeper')
const { FIREWALL } = require('./constants')
const BIRTHDAY_SOCKETS = 256
const HOLEPUNCH = b4a.from([0])
const HOLEPUNCH_TTL = 5
const DEFAULT_TTL = 64
const MAX_REOPENS = 3
module.exports = class Holepuncher {
constructor(dht, session, isInitiator, remoteFirewall = FIREWALL.UNKNOWN) {
const holder = dht._socketPool.acquire()
this.dht = dht
this.session = session
this.nat = new Nat(dht, session, holder.socket)
this.nat.autoSample()
this.isInitiator = isInitiator
// events
this.onconnect = noop
this.onabort = noop
this.punching = false
this.connected = false
this.destroyed = false
this.randomized = false
// track remote state
this.remoteFirewall = remoteFirewall
this.remoteAddresses = []
this.remoteHolepunching = false
this._sleeper = new Sleeper()
this._reopening = null
this._timeout = null
this._punching = null
this._allHolders = []
this._holder = this._addRef(holder)
}
get socket() {
return this._holder.socket
}
updateRemote({ punching, firewall, addresses, verified }) {
const remoteAddresses = []
if (addresses) {
for (const addr of addresses) {
remoteAddresses.push({
host: addr.host,
port: addr.port,
verified: verified === addr.host || this._isVerified(addr.host)
})
}
}
this.remoteFirewall = firewall
this.remoteAddresses = remoteAddresses
this.remoteHolepunching = punching
}
_isVerified(host) {
for (const addr of this.remoteAddresses) {
if (addr.verified && addr.host === host) {
return true
}
}
return false
}
ping(addr, socket = this._holder.socket) {
return holepunch(socket, addr, false)
}
openSession(addr, socket = this._holder.socket) {
return holepunch(socket, addr, true)
}
async analyze(allowReopen) {
await this.nat.analyzing
if (this._unstable()) {
if (!allowReopen) return false
if (!this._reopening) this._reopening = this._reopen()
return this._reopening
}
return true
}
_unstable() {
// TODO!!: We need an additional heuristic here... If we were NOT random in the past we should also do this.
const firewall = this.nat.firewall
return (
(this.remoteFirewall >= FIREWALL.RANDOM && firewall >= FIREWALL.RANDOM) ||
firewall === FIREWALL.UNKNOWN
)
}
_reset() {
const prev = this._holder
this._allHolders.pop()
this._holder = this._addRef(this.dht._socketPool.acquire())
prev.release()
this.nat.destroy()
this.nat = new Nat(this.dht, this.session, this._holder.socket)
// TODO: maybe make auto sampling configurable somehow?
this.nat.autoSample()
}
_addRef(ref) {
this._allHolders.push(ref)
ref.onholepunchmessage = (msg, rinfo) => this._onholepunchmessage(msg, rinfo, ref)
return ref
}
_onholepunchmessage(_, addr, ref) {
if (!this.isInitiator) {
// TODO: we don't need this if we had a way to connect a socket to many hosts
holepunch(ref.socket, addr, false) // never fails
return
}
if (this.connected) return
this.connected = true
this.punching = false
for (const r of this._allHolders) {
if (r === ref) continue
r.release()
}
this._allHolders[0] = ref
while (this._allHolders.length > 1) this._allHolders.pop()
this._decrementRandomized()
this.onconnect(ref.socket, addr.port, addr.host)
}
_done() {
return this.destroyed || this.connected
}
async _reopen() {
for (let i = 0; this._unstable() && i < MAX_REOPENS && !this._done() && !this.punching; i++) {
this._reset()
await this.nat.analyzing
}
return coerceFirewall(this.nat.firewall) === FIREWALL.CONSISTENT
}
punch() {
if (!this._punching) this._punching = this._punch()
return this._punching
}
async _punch() {
if (this._done() || !this.remoteAddresses.length) return false
this.punching = true
// Coerce into consistency for now. Obvs we could make this this more efficient if we use that info
// but that's seldomly used since those will just use tcp most of the time.
const local = coerceFirewall(this.nat.firewall)
const remote = coerceFirewall(this.remoteFirewall)
// Note that most of these async functions are meant to run in the background
// which is why we don't await them here and why they are not allowed to throw
let remoteVerifiedAddress = null
for (const addr of this.remoteAddresses) {
if (addr.verified) {
remoteVerifiedAddress = addr
break
}
}
if (local === FIREWALL.CONSISTENT && remote === FIREWALL.CONSISTENT) {
this.dht.stats.punches.consistent++
this._consistentProbe()
return true
}
if (!remoteVerifiedAddress) return false
if (local === FIREWALL.CONSISTENT && remote >= FIREWALL.RANDOM) {
this.dht.stats.punches.random++
this._incrementRandomized()
this._randomProbes(remoteVerifiedAddress)
return true
}
if (local >= FIREWALL.RANDOM && remote === FIREWALL.CONSISTENT) {
this.dht.stats.punches.random++
this._incrementRandomized()
await this._openBirthdaySockets(remoteVerifiedAddress)
if (this.punching) this._keepAliveRandomNat(remoteVerifiedAddress)
return true
}
return false
}
// Note that this never throws so it is safe to run in the background
async _consistentProbe() {
// Here we do the sleep first because the "fast open" mode in the server just fired a ping
if (!this.isInitiator) await this._sleeper.pause(1000)
let tries = 0
while (this.punching && tries++ < 10) {
for (const addr of this.remoteAddresses) {
// only try unverified addresses every 4 ticks
if (!addr.verified && (tries & 3) !== 0) continue
await holepunch(this._holder.socket, addr, false)
}
if (this.punching) await this._sleeper.pause(1000)
}
this._autoDestroy()
}
// Note that this never throws so it is safe to run in the background
async _randomProbes(remoteAddr) {
let tries = 1750 // ~35s
while (this.punching && tries-- > 0) {
const addr = { host: remoteAddr.host, port: randomPort() }
await holepunch(this._holder.socket, addr, false)
if (this.punching) await this._sleeper.pause(20)
}
this._autoDestroy()
}
// Note that this never throws so it is safe to run in the background
async _keepAliveRandomNat(remoteAddr) {
let i = 0
let lowTTLRounds = 1
// TODO: experiment with this here. We just bursted all the messages in
// openOtherSockets to ensure the sockets are open, so it's potentially
// a good idea to slow down for a bit.
await this._sleeper.pause(100)
let tries = 1750 // ~35s
while (this.punching && tries-- > 0) {
if (i === this._allHolders.length) {
i = 0
if (lowTTLRounds > 0) lowTTLRounds--
}
await holepunch(this._allHolders[i++].socket, remoteAddr, lowTTLRounds > 0)
if (this.punching) await this._sleeper.pause(20)
}
this._autoDestroy()
}
async _openBirthdaySockets(remoteAddr) {
while (this.punching && this._allHolders.length < BIRTHDAY_SOCKETS) {
const ref = this._addRef(this.dht._socketPool.acquire())
await holepunch(ref.socket, remoteAddr, HOLEPUNCH_TTL)
}
}
_autoDestroy() {
if (!this.connected) this.destroy()
}
_incrementRandomized() {
if (!this.randomized) {
this.randomized = true
this.dht._randomPunches++
}
}
_decrementRandomized() {
if (this.randomized) {
this.dht._lastRandomPunch = Date.now()
this.randomized = false
this.dht._randomPunches--
}
}
destroy() {
if (this.destroyed) return
this.destroyed = true
this.punching = false
for (const ref of this._allHolders) ref.release()
this._allHolders = []
this.nat.destroy()
if (!this.connected) {
this._decrementRandomized()
this.onabort()
}
}
static ping(socket, addr) {
return holepunch(socket, addr, false)
}
static localAddresses(socket) {
return localAddresses(socket)
}
static matchAddress(myAddresses, externalAddresses) {
return matchAddress(myAddresses, externalAddresses)
}
}
function holepunch(socket, addr, lowTTL) {
return socket.send(HOLEPUNCH, addr.port, addr.host, lowTTL ? HOLEPUNCH_TTL : DEFAULT_TTL)
}
function randomPort() {
return (1000 + Math.random() * 64536) | 0
}
function coerceFirewall(fw) {
return fw === FIREWALL.OPEN ? FIREWALL.CONSISTENT : fw
}
function localAddresses(socket) {
const addrs = []
const { host, port } = socket.address()
if (host === '127.0.0.1') return [{ host, port }]
for (const n of socket.udx.networkInterfaces()) {
if (n.family !== 4 || n.internal) continue
addrs.push({ host: n.host, port })
}
if (addrs.length === 0) {
addrs.push({ host: '127.0.0.1', port })
}
return addrs
}
function matchAddress(localAddresses, remoteLocalAddresses) {
if (remoteLocalAddresses.length === 0) return null
let best = { segment: 1, addr: null }
for (const localAddress of localAddresses) {
// => 192.168.122.238
const a = localAddress.host.split('.')
for (const remoteAddress of remoteLocalAddresses) {
// => 192.168.0.23
// => 192.168.122.1
const b = remoteAddress.host.split('.')
// Matches 192.*.*.*
if (a[0] === b[0]) {
if (best.segment === 1) best = { segment: 2, addr: remoteAddress }
// Matches 192.168.*.*
if (a[1] === b[1]) {
if (best.segment === 2) best = { segment: 3, addr: remoteAddress }
// Matches 192.168.122.*
if (a[2] === b[2]) return remoteAddress
}
}
}
}
return best.addr
}
function noop() {}
+455
View File
@@ -0,0 +1,455 @@
const c = require('compact-encoding')
const ipv4 = {
...c.ipv4Address,
decode(state) {
const ip = c.ipv4Address.decode(state)
return {
host: ip.host,
port: ip.port
}
}
}
const ipv4Array = c.array(ipv4)
const ipv6 = {
...c.ipv6Address,
decode(state) {
const ip = c.ipv6Address.decode(state)
return {
host: ip.host,
port: ip.port
}
}
}
const ipv6Array = c.array(ipv6)
exports.handshake = {
preencode(state, m) {
state.end += 1 + 1 + (m.peerAddress ? 6 : 0) + (m.relayAddress ? 6 : 0)
c.buffer.preencode(state, m.noise)
},
encode(state, m) {
const flags = (m.peerAddress ? 1 : 0) | (m.relayAddress ? 2 : 0)
c.uint.encode(state, flags)
c.uint.encode(state, m.mode)
c.buffer.encode(state, m.noise)
if (m.peerAddress) ipv4.encode(state, m.peerAddress)
if (m.relayAddress) ipv4.encode(state, m.relayAddress)
},
decode(state) {
const flags = c.uint.decode(state)
return {
mode: c.uint.decode(state),
noise: c.buffer.decode(state),
peerAddress: flags & 1 ? ipv4.decode(state) : null,
relayAddress: flags & 2 ? ipv4.decode(state) : null
}
}
}
const relayInfo = {
preencode(state, m) {
state.end += 12
},
encode(state, m) {
ipv4.encode(state, m.relayAddress)
ipv4.encode(state, m.peerAddress)
},
decode(state) {
return {
relayAddress: ipv4.decode(state),
peerAddress: ipv4.decode(state)
}
}
}
const relayInfoArray = c.array(relayInfo)
const holepunchInfo = {
preencode(state, m) {
c.uint.preencode(state, m.id)
relayInfoArray.preencode(state, m.relays)
},
encode(state, m) {
c.uint.encode(state, m.id)
relayInfoArray.encode(state, m.relays)
},
decode(state) {
return {
id: c.uint.decode(state),
relays: relayInfoArray.decode(state)
}
}
}
const udxInfo = {
preencode(state, m) {
state.end += 2 // version + features
c.uint.preencode(state, m.id)
c.uint.preencode(state, m.seq)
},
encode(state, m) {
c.uint.encode(state, 1)
c.uint.encode(state, m.reusableSocket ? 1 : 0)
c.uint.encode(state, m.id)
c.uint.encode(state, m.seq)
},
decode(state) {
const version = c.uint.decode(state)
const features = c.uint.decode(state)
return {
version,
reusableSocket: (features & 1) !== 0,
id: c.uint.decode(state),
seq: c.uint.decode(state)
}
}
}
const secretStreamInfo = {
preencode(state, m) {
c.uint.preencode(state, 1)
},
encode(state, m) {
c.uint.encode(state, 1)
},
decode(state) {
return {
version: c.uint.decode(state)
}
}
}
const relayThroughInfo = {
preencode(state, m) {
c.uint.preencode(state, 1) // version
c.uint.preencode(state, 0) // flags
c.fixed32.preencode(state, m.publicKey)
c.fixed32.preencode(state, m.token)
},
encode(state, m) {
c.uint.encode(state, 1)
c.uint.encode(state, 0)
c.fixed32.encode(state, m.publicKey)
c.fixed32.encode(state, m.token)
},
decode(state) {
const version = c.uint.decode(state)
c.uint.decode(state)
return {
version,
publicKey: c.fixed32.decode(state),
token: c.fixed32.decode(state)
}
}
}
exports.noisePayload = {
preencode(state, m) {
state.end += 4 // version + flags + error + firewall
if (m.holepunch) holepunchInfo.preencode(state, m.holepunch)
if (m.addresses4 && m.addresses4.length) ipv4Array.preencode(state, m.addresses4)
if (m.addresses6 && m.addresses6.length) ipv6Array.preencode(state, m.addresses6)
if (m.udx) udxInfo.preencode(state, m.udx)
if (m.secretStream) secretStreamInfo.preencode(state, m.secretStream)
if (m.relayThrough) relayThroughInfo.preencode(state, m.relayThrough)
if (m.relayAddresses) ipv4Array.preencode(state, m.relayAddresses)
},
encode(state, m) {
let flags = 0
if (m.holepunch) flags |= 1
if (m.addresses4 && m.addresses4.length) flags |= 2
if (m.addresses6 && m.addresses6.length) flags |= 4
if (m.udx) flags |= 8
if (m.secretStream) flags |= 16
if (m.relayThrough) flags |= 32
if (m.relayAddresses) flags |= 64
c.uint.encode(state, 1) // version
c.uint.encode(state, flags)
c.uint.encode(state, m.error)
c.uint.encode(state, m.firewall)
if (m.holepunch) holepunchInfo.encode(state, m.holepunch)
if (m.addresses4 && m.addresses4.length) ipv4Array.encode(state, m.addresses4)
if (m.addresses6 && m.addresses6.length) ipv6Array.encode(state, m.addresses6)
if (m.udx) udxInfo.encode(state, m.udx)
if (m.secretStream) secretStreamInfo.encode(state, m.secretStream)
if (m.relayThrough) relayThroughInfo.encode(state, m.relayThrough)
if (m.relayAddresses) ipv4Array.encode(state, m.relayAddresses)
},
decode(state) {
const version = c.uint.decode(state)
if (version !== 1) {
// Do not attempt to decode but return this back to the user so they can
// actually handle it
return {
version,
error: 0,
firewall: 0,
holepunch: null,
addresses4: [],
addresses6: [],
udx: null,
secretStream: null,
relayThrough: null,
relayAddresses: null
}
}
const flags = c.uint.decode(state)
return {
version,
error: c.uint.decode(state),
firewall: c.uint.decode(state),
holepunch: (flags & 1) !== 0 ? holepunchInfo.decode(state) : null,
addresses4: (flags & 2) !== 0 ? ipv4Array.decode(state) : [],
addresses6: (flags & 4) !== 0 ? ipv6Array.decode(state) : [],
udx: (flags & 8) !== 0 ? udxInfo.decode(state) : null,
secretStream: (flags & 16) !== 0 ? secretStreamInfo.decode(state) : null,
relayThrough: (flags & 32) !== 0 ? relayThroughInfo.decode(state) : null,
relayAddresses: (flags & 64) !== 0 ? ipv4Array.decode(state) : null
}
}
}
exports.holepunch = {
preencode(state, m) {
state.end += 2
c.uint.preencode(state, m.id)
c.buffer.preencode(state, m.payload)
if (m.peerAddress) ipv4.preencode(state, m.peerAddress)
},
encode(state, m) {
const flags = m.peerAddress ? 1 : 0
c.uint.encode(state, flags)
c.uint.encode(state, m.mode)
c.uint.encode(state, m.id)
c.buffer.encode(state, m.payload)
if (m.peerAddress) ipv4.encode(state, m.peerAddress)
},
decode(state) {
const flags = c.uint.decode(state)
return {
mode: c.uint.decode(state),
id: c.uint.decode(state),
payload: c.buffer.decode(state),
peerAddress: flags & 1 ? ipv4.decode(state) : null
}
}
}
exports.holepunchPayload = {
preencode(state, m) {
state.end += 4 // flags + error + firewall + round
if (m.addresses) ipv4Array.preencode(state, m.addresses)
if (m.remoteAddress) state.end += 6
if (m.token) state.end += 32
if (m.remoteToken) state.end += 32
},
encode(state, m) {
const flags =
(m.connected ? 1 : 0) |
(m.punching ? 2 : 0) |
(m.addresses ? 4 : 0) |
(m.remoteAddress ? 8 : 0) |
(m.token ? 16 : 0) |
(m.remoteToken ? 32 : 0)
c.uint.encode(state, flags)
c.uint.encode(state, m.error)
c.uint.encode(state, m.firewall)
c.uint.encode(state, m.round)
if (m.addresses) ipv4Array.encode(state, m.addresses)
if (m.remoteAddress) ipv4.encode(state, m.remoteAddress)
if (m.token) c.fixed32.encode(state, m.token)
if (m.remoteToken) c.fixed32.encode(state, m.remoteToken)
},
decode(state) {
const flags = c.uint.decode(state)
return {
error: c.uint.decode(state),
firewall: c.uint.decode(state),
round: c.uint.decode(state),
connected: (flags & 1) !== 0,
punching: (flags & 2) !== 0,
addresses: (flags & 4) !== 0 ? ipv4Array.decode(state) : null,
remoteAddress: (flags & 8) !== 0 ? ipv4.decode(state) : null,
token: (flags & 16) !== 0 ? c.fixed32.decode(state) : null,
remoteToken: (flags & 32) !== 0 ? c.fixed32.decode(state) : null
}
}
}
const peer = (exports.peer = {
preencode(state, m) {
state.end += 32
ipv4Array.preencode(state, m.relayAddresses)
},
encode(state, m) {
c.fixed32.encode(state, m.publicKey)
ipv4Array.encode(state, m.relayAddresses)
},
decode(state) {
return {
publicKey: c.fixed32.decode(state),
relayAddresses: ipv4Array.decode(state)
}
}
})
const peers = (exports.peers = c.array(peer))
const rawPeers = c.array(c.raw)
exports.lookupRawReply = {
preencode(state, m) {
rawPeers.preencode(state, m.peers)
c.uint.preencode(state, m.bump)
},
encode(state, m) {
rawPeers.encode(state, m.peers)
c.uint.encode(state, m.bump)
},
decode(state) {
return {
peers: peers.decode(state),
bump: state.start < state.end ? c.uint.decode(state) : 0
}
}
}
exports.announce = {
preencode(state, m) {
state.end++ // flags
if (m.peer) peer.preencode(state, m.peer)
if (m.refresh) state.end += 32
if (m.signature) state.end += 64
if (m.bump) c.uint.preencode(state, m.bump)
},
encode(state, m) {
const flags = (m.peer ? 1 : 0) | (m.refresh ? 2 : 0) | (m.signature ? 4 : 0) | (m.bump ? 8 : 0)
c.uint.encode(state, flags)
if (m.peer) peer.encode(state, m.peer)
if (m.refresh) c.fixed32.encode(state, m.refresh)
if (m.signature) c.fixed64.encode(state, m.signature)
if (m.bump) c.uint.encode(state, m.bump)
},
decode(state) {
const flags = c.uint.decode(state)
return {
peer: (flags & 1) !== 0 ? peer.decode(state) : null,
refresh: (flags & 2) !== 0 ? c.fixed32.decode(state) : null,
signature: (flags & 4) !== 0 ? c.fixed64.decode(state) : null,
bump: (flags & 8) !== 0 ? c.uint.decode(state) : 0
}
}
}
exports.mutableSignable = {
preencode(state, m) {
c.uint.preencode(state, m.seq)
c.buffer.preencode(state, m.value)
},
encode(state, m) {
c.uint.encode(state, m.seq)
c.buffer.encode(state, m.value)
},
decode(state) {
return {
seq: c.uint.decode(state),
value: c.buffer.decode(state)
}
}
}
exports.mutablePutRequest = {
preencode(state, m) {
c.fixed32.preencode(state, m.publicKey)
c.uint.preencode(state, m.seq)
c.buffer.preencode(state, m.value)
c.fixed64.preencode(state, m.signature)
},
encode(state, m) {
c.fixed32.encode(state, m.publicKey)
c.uint.encode(state, m.seq)
c.buffer.encode(state, m.value)
c.fixed64.encode(state, m.signature)
},
decode(state) {
return {
publicKey: c.fixed32.decode(state),
seq: c.uint.decode(state),
value: c.buffer.decode(state),
signature: c.fixed64.decode(state)
}
}
}
exports.mutableGetResponse = {
preencode(state, m) {
c.uint.preencode(state, m.seq)
c.buffer.preencode(state, m.value)
c.fixed64.preencode(state, m.signature)
},
encode(state, m) {
c.uint.encode(state, m.seq)
c.buffer.encode(state, m.value)
c.fixed64.encode(state, m.signature)
},
decode(state) {
return {
seq: c.uint.decode(state),
value: c.buffer.decode(state),
signature: c.fixed64.decode(state)
}
}
}
exports.pluginRequest = {
preencode(state, m) {
c.string.preencode(state, m.plugin)
c.uint.preencode(state, m.version)
c.uint.preencode(state, m.command)
state.end++ // max flag is 1 so always one byte
if (m.value) c.buffer.preencode(state, m.value)
},
encode(state, m) {
const flags = m.value ? 1 : 0
c.string.encode(state, m.plugin)
c.uint.encode(state, m.version)
c.uint.encode(state, m.command)
c.uint.encode(state, flags)
if (m.value) c.buffer.encode(state, m.value)
},
decode(state) {
const r0 = c.string.decode(state)
const r1 = c.uint.decode(state)
const r2 = c.uint.decode(state)
const flags = c.uint.decode(state)
return {
plugin: r0,
version: r1,
command: r2,
value: (flags & 1) !== 0 ? c.buffer.decode(state) : null
}
}
}
+207
View File
@@ -0,0 +1,207 @@
const { FIREWALL } = require('../lib/constants')
module.exports = class Nat {
constructor(dht, session, socket) {
this._samplesHost = []
this._samplesFull = []
this._visited = new Map()
this._resolve = null
this._minSamples = 4
this._autoSampling = false
this.dht = dht
this.session = session
this.socket = socket
this.sampled = 0
this.firewall = dht.firewalled ? FIREWALL.UNKNOWN : FIREWALL.OPEN
this.addresses = null
this.analyzing = new Promise((resolve) => {
this._resolve = resolve
})
}
autoSample(retry = true) {
if (this._autoSampling) return
this._autoSampling = true
const self = this
const socket = this.socket
const maxPings = this._minSamples
let skip = this.dht.nodes.length >= 8 ? 5 : 0
let pending = 0
// TODO: it would be best to pick the nodes to help us based on latency to us
// That should reduce connect latency in general. We should investigate tracking that later on.
// TODO 2: try to pick nodes with different IPs as well, as that'll help multi IP cell connections...
// If we expose this from the nat sampler then the DHT should be able to help us filter out scams as well...
for (
let node = this.dht.nodes.latest;
node && this.sampled + pending < maxPings;
node = node.prev
) {
if (skip > 0) {
skip--
continue
}
const ref = node.host + ':' + node.port
if (this._visited.has(ref)) continue
this._visited.set(ref, 1)
pending++
this.session.ping(node, { socket, retry: false }).then(onpong, onskip)
}
pending++
onskip()
function onpong(res) {
self.add(res.to, res.from)
onskip()
}
function onskip() {
if (--pending === 0 && self.sampled < self._minSamples) {
if (retry) {
self._autoSampling = false
self.autoSample(false)
return
}
self._resolve()
}
}
}
destroy() {
this._autoSampling = true
this._minSamples = 0
this._resolve()
}
unfreeze() {
this.frozen = false
this._updateFirewall()
this._updateAddresses()
}
freeze() {
this.frozen = true
}
_updateFirewall() {
if (!this.dht.firewalled) {
this.firewall = FIREWALL.OPEN
return
}
if (this.sampled < 3) return
const max = this._samplesFull[0].hits
if (max >= 3) {
this.firewall = FIREWALL.CONSISTENT
return
}
if (max === 1) {
this.firewall = FIREWALL.RANDOM
return
}
// else max === 2
// 1 host, >= 4 total samples ie, 2 bad ones -> random
if (this._samplesHost.length === 1 && this.sampled > 3) {
this.firewall = FIREWALL.RANDOM
return
}
// double hit on two different ips -> assume consistent
if (this._samplesHost.length > 1 && this._samplesFull[1].hits > 1) {
this.firewall = FIREWALL.CONSISTENT
return
}
// (4 is just means - all the samples we expect) - no decision - assume random
if (this.sampled > 4) {
this.firewall = FIREWALL.RANDOM
}
}
_updateAddresses() {
if (this.firewall === FIREWALL.UNKNOWN) {
this.addresses = null
return
}
if (this.firewall === FIREWALL.RANDOM) {
this.addresses = [this._samplesHost[0]]
return
}
if (this.firewall === FIREWALL.CONSISTENT) {
this.addresses = []
for (const addr of this._samplesFull) {
if (addr.hits >= 2 || this.addresses.length < 2) this.addresses.push(addr)
}
}
}
update() {
if (this.dht.firewalled && this.firewall === FIREWALL.OPEN) {
this.firewall = FIREWALL.UNKNOWN
}
this._updateFirewall()
this._updateAddresses()
}
add(addr, from) {
const ref = from.host + ':' + from.port
if (this._visited.get(ref) === 2) return
this._visited.set(ref, 2)
addSample(this._samplesHost, addr.host, 0)
addSample(this._samplesFull, addr.host, addr.port)
if ((++this.sampled >= 3 || !this.dht.firewalled) && !this.frozen) {
this.update()
}
if (this.firewall === FIREWALL.CONSISTENT || this.firewall === FIREWALL.OPEN) {
this._resolve()
} else if (this.sampled >= this._minSamples) {
this._resolve()
}
}
}
function addSample(samples, host, port) {
for (let i = 0; i < samples.length; i++) {
const s = samples[i]
if (s.port !== port || s.host !== host) continue
s.hits++
for (; i > 0; i--) {
const prev = samples[i - 1]
if (prev.hits >= s.hits) return
samples[i - 1] = s
samples[i] = prev
}
return
}
samples.push({
host,
port,
hits: 1
})
}
+52
View File
@@ -0,0 +1,52 @@
const NoiseSecretStream = require('@hyperswarm/secret-stream')
const NoiseHandshake = require('noise-handshake')
const curve = require('noise-curve-ed')
const c = require('compact-encoding')
const b4a = require('b4a')
const sodium = require('sodium-universal')
const m = require('./messages')
const { NS } = require('./constants')
const { HANDSHAKE_UNFINISHED } = require('./errors')
const NOISE_PROLOUGE = NS.PEER_HANDSHAKE
module.exports = class NoiseWrap {
constructor(keyPair, remotePublicKey) {
this.isInitiator = !!remotePublicKey
this.remotePublicKey = remotePublicKey
this.keyPair = keyPair
this.handshake = new NoiseHandshake('IK', this.isInitiator, keyPair, { curve })
this.handshake.initialise(NOISE_PROLOUGE, remotePublicKey)
}
send(payload) {
const buf = c.encode(m.noisePayload, payload)
return this.handshake.send(buf)
}
recv(buf) {
const payload = c.decode(m.noisePayload, this.handshake.recv(buf))
this.remotePublicKey = b4a.toBuffer(this.handshake.rs)
return payload
}
final() {
if (!this.handshake.complete) throw HANDSHAKE_UNFINISHED()
const holepunchSecret = b4a.allocUnsafe(32)
sodium.crypto_generichash(holepunchSecret, NS.PEER_HOLEPUNCH, this.handshake.hash)
return {
isInitiator: this.isInitiator,
publicKey: this.keyPair.publicKey,
streamId: this.streamId,
remotePublicKey: this.remotePublicKey,
remoteId: NoiseSecretStream.id(this.handshake.hash, !this.isInitiator),
holepunchSecret,
hash: b4a.toBuffer(this.handshake.hash),
rx: b4a.toBuffer(this.handshake.rx),
tx: b4a.toBuffer(this.handshake.tx)
}
}
}
+315
View File
@@ -0,0 +1,315 @@
const c = require('compact-encoding')
const sodium = require('sodium-universal')
const RecordCache = require('record-cache')
const Cache = require('xache')
const b4a = require('b4a')
const unslab = require('unslab')
const { encodeUnslab } = require('./encode')
const m = require('./messages')
const { NS, ERROR } = require('./constants')
const EMPTY = b4a.alloc(0)
const TMP = b4a.allocUnsafe(32)
const MAX_BUMP_DRIFT = 60_000
module.exports = class Persistent {
constructor(dht, opts) {
this.dht = dht
this.records = new RecordCache(opts.records)
this.bumps = new Cache(opts.bumps)
this.refreshes = new Cache(opts.refreshes)
this.mutables = new Cache(opts.mutables)
this.immutables = new Cache(opts.immutables)
}
onlookup(req) {
if (!req.target) return
const k = b4a.toString(req.target, 'hex')
const records = this.records.get(k, 20)
const bump = this.bumps.get(k) || 0
const fwd = this.dht._router.get(k)
if (fwd && records.length < 20) records.push(fwd.record)
req.reply(records.length ? c.encode(m.lookupRawReply, { peers: records, bump }) : null)
}
onfindpeer(req) {
if (!req.target) return
const fwd = this.dht._router.get(req.target)
req.reply(fwd ? fwd.record : null)
}
unannounce(target, publicKey) {
const k = b4a.toString(target, 'hex')
sodium.crypto_generichash(TMP, publicKey)
if (b4a.equals(TMP, target)) this.dht._router.delete(k)
this.records.remove(k, publicKey)
}
onunannounce(req) {
if (!req.target || !req.token) return
const unann = decode(m.announce, req.value)
if (unann === null) return
const { peer, signature } = unann
if (!peer || !signature) return
const signable = annSignable(req.target, req.token, this.dht.id, unann, NS.UNANNOUNCE)
if (!sodium.crypto_sign_verify_detached(signature, signable, peer.publicKey)) {
return
}
this.unannounce(req.target, peer.publicKey)
req.reply(null, { token: false, closerNodes: false })
}
_onrefresh(token, req) {
sodium.crypto_generichash(TMP, token)
const activeRefresh = b4a.toString(TMP, 'hex')
const r = this.refreshes.get(activeRefresh)
if (!r) return
const { announceSelf, k, record } = r
const publicKey = record.subarray(0, 32)
if (announceSelf) {
this.dht._router.set(k, {
relay: req.from,
record,
onconnect: null,
onholepunch: null
})
this.records.remove(k, publicKey)
} else {
this.records.add(k, publicKey, record)
}
this.refreshes.delete(activeRefresh)
this.refreshes.set(b4a.toString(token, 'hex'), r)
req.reply(null, { token: false, closerNodes: false })
}
onannounce(req) {
if (!req.target || !req.token || !this.dht.id) return
const ann = decode(m.announce, req.value)
if (ann === null) return
const { peer, refresh, signature, bump } = ann
if (!peer) {
if (!refresh) return
this._onrefresh(refresh, req)
return
}
const signable = annSignable(req.target, req.token, this.dht.id, ann, NS.ANNOUNCE)
if (!signature || !sodium.crypto_sign_verify_detached(signature, signable, peer.publicKey)) {
return
}
// TODO: it would be potentially be more optimal to allow more than 3 addresses here for a findPeer response
// and only use max 3 for a lookup reply
if (peer.relayAddresses.length > 3) {
peer.relayAddresses = peer.relayAddresses.slice(0, 3)
}
sodium.crypto_generichash(TMP, peer.publicKey)
const k = b4a.toString(req.target, 'hex')
const announceSelf = b4a.equals(TMP, req.target)
const record = encodeUnslab(m.peer, peer)
if (announceSelf) {
this.dht._router.set(k, {
relay: req.from,
record,
onconnect: null,
onholepunch: null
})
this.records.remove(k, peer.publicKey)
} else {
const currentBump = this.bumps.get(k) || 0
if (bump > currentBump && bump <= Date.now() + MAX_BUMP_DRIFT) this.bumps.set(k, bump)
this.records.add(k, peer.publicKey, record)
}
if (refresh) {
this.refreshes.set(b4a.toString(refresh, 'hex'), { k, record, announceSelf })
}
req.reply(null, { token: false, closerNodes: false })
}
onmutableget(req) {
if (!req.target || !req.value) return
let seq = 0
try {
seq = c.decode(c.uint, req.value)
} catch {
return
}
const k = b4a.toString(req.target, 'hex')
const value = this.mutables.get(k)
if (!value) {
req.reply(null)
return
}
const localSeq = c.decode(c.uint, value)
req.reply(localSeq < seq ? null : value)
}
onmutableput(req) {
if (!req.target || !req.token || !req.value) return
const p = decode(m.mutablePutRequest, req.value)
if (!p) return
const { publicKey, seq, value, signature } = p
const hash = b4a.allocUnsafe(32)
sodium.crypto_generichash(hash, publicKey)
if (!b4a.equals(hash, req.target)) return
if (!value || !verifyMutable(signature, seq, value, publicKey)) return
const k = b4a.toString(hash, 'hex')
const local = this.mutables.get(k)
if (local) {
const existing = c.decode(m.mutableGetResponse, local)
if (existing.value && existing.seq === seq && b4a.compare(value, existing.value) !== 0) {
req.error(ERROR.SEQ_REUSED)
return
}
if (seq < existing.seq) {
req.error(ERROR.SEQ_TOO_LOW)
return
}
}
this.mutables.set(k, encodeUnslab(m.mutableGetResponse, { seq, value, signature }))
req.reply(null)
}
onimmutableget(req) {
if (!req.target) return
const k = b4a.toString(req.target, 'hex')
const value = this.immutables.get(k)
req.reply(value || null)
}
onimmutableput(req) {
if (!req.target || !req.token || !req.value) return
const hash = b4a.alloc(32)
sodium.crypto_generichash(hash, req.value)
if (!b4a.equals(hash, req.target)) return
const k = b4a.toString(hash, 'hex')
this.immutables.set(k, unslab(req.value))
req.reply(null)
}
onplugin(req) {
if (!req.value) return
const plugreq = decode(m.pluginRequest, req.value)
if (plugreq === null) return
const p = this.dht.plugins.get(plugreq.plugin)
if (!p || p.version !== plugreq.version) return
p.onrequest(plugreq, req)
}
destroy() {
this.records.destroy()
this.refreshes.destroy()
this.mutables.destroy()
this.immutables.destroy()
}
static signMutable(seq, value, keyPair) {
const signable = b4a.allocUnsafe(32 + 32)
const hash = signable.subarray(32)
signable.set(NS.MUTABLE_PUT, 0)
sodium.crypto_generichash(hash, c.encode(m.mutableSignable, { seq, value }))
return sign(signable, keyPair)
}
static verifyMutable(signature, seq, value, publicKey) {
return verifyMutable(signature, seq, value, publicKey)
}
static signAnnounce(target, token, id, ann, keyPair) {
return sign(annSignable(target, token, id, ann, NS.ANNOUNCE), keyPair)
}
static signUnannounce(target, token, id, ann, keyPair) {
return sign(annSignable(target, token, id, ann, NS.UNANNOUNCE), keyPair)
}
}
function verifyMutable(signature, seq, value, publicKey) {
const signable = b4a.allocUnsafe(32 + 32)
const hash = signable.subarray(32)
signable.set(NS.MUTABLE_PUT, 0)
sodium.crypto_generichash(hash, c.encode(m.mutableSignable, { seq, value }))
return sodium.crypto_sign_verify_detached(signature, signable, publicKey)
}
function annSignable(target, token, id, ann, ns) {
const signable = b4a.allocUnsafe(32 + 32)
const hash = signable.subarray(32)
signable.set(ns, 0)
sodium.crypto_generichash_batch(hash, [
target,
id,
token,
c.encode(m.peer, ann.peer), // note that this is the partial encoding of the announce message so we could just use that for perf
ann.refresh || EMPTY
])
return signable
}
function sign(signable, keyPair) {
if (keyPair.sign) {
return keyPair.sign(signable)
}
const secretKey = keyPair.secretKey ? keyPair.secretKey : keyPair
const signature = b4a.allocUnsafe(64)
sodium.crypto_sign_detached(signature, signable, secretKey)
return signature
}
function decode(enc, val) {
try {
return val && c.decode(enc, val)
} catch (err) {
return null
}
}
+65
View File
@@ -0,0 +1,65 @@
const c = require('compact-encoding')
const m = require('./messages')
const { COMMANDS: HYPERDHT_COMMANDS } = require('./constants')
module.exports = class Plugin {
constructor(name, version) {
this.name = name
this.version = version
this.dht = null
}
onregister(dht) {
this.dht = dht
}
onrequest(req, outerReq) {
throw new Error('onrequest() must be implemented')
}
onpersistent() {
throw new Error('onpersistent() must be implemented')
}
destroy() {
throw new Error('destroy() must be implemented')
}
request({ token = null, command, target = null, value = null }, to, opts) {
const req = c.encode(m.pluginRequest, {
plugin: this.name,
version: this.version,
command,
value
})
return this.dht.request(
{
token,
target,
command: HYPERDHT_COMMANDS.PLUGIN,
value: req
},
to,
opts
)
}
query({ command, target = null, value = null }, opts) {
const req = c.encode(m.pluginRequest, {
plugin: this.name,
version: this.version,
command,
value
})
return this.dht.query(
{
target,
command: HYPERDHT_COMMANDS.PLUGIN,
value: req
},
opts
)
}
}
+66
View File
@@ -0,0 +1,66 @@
module.exports = class RawStreamSet {
constructor(dht) {
this._dht = dht
this._prefix = 16 - 1 // 16 is the default stream-set side in udx
this._streams = new Map()
}
get size() {
return this._streams.size
}
[Symbol.iterator]() {
return this._streams.values()
}
add(opts) {
const self = this
// TODO: we should prob have a udx helper for id generation, given the slight complexity
// of the below. requires a PRNG in udx tho.
let id = 0
while (true) {
id = (Math.random() * 0x100000000) >>> 0
if (this._streams.has(id & this._prefix)) continue
break
}
// always have ~50% change of rolling a free one
if (2 * this._streams.size >= this._prefix) {
// ie 0b11111 = 0b1111 + 1 + 0b1111
this._prefix = 2 * this._prefix + 1
// move the prefixes over
const next = new Map()
for (const stream of this._streams.values()) {
next.set(stream.id & this._prefix, stream)
}
this._streams = next
}
const stream = this._dht.udx.createStream(id, opts)
this._streams.set(id & this._prefix, stream)
stream.on('close', onclose)
return stream
function onclose() {
self._streams.delete(id & self._prefix)
}
}
async clear() {
const destroying = []
for (const stream of this._streams.values()) {
destroying.push(new Promise((resolve) => stream.once('close', resolve).destroy()))
}
await Promise.allSettled(destroying)
}
}
+20
View File
@@ -0,0 +1,20 @@
const sodium = require('sodium-universal')
const b4a = require('b4a')
module.exports = function createRefreshChain(cnt) {
const blocks = new Array(cnt)
if (!blocks.length) return blocks
const all = b4a.allocUnsafe(cnt * 32 + 32)
let prev = all.subarray(all.byteLength - 32)
sodium.randombytes_buf(prev)
for (let i = cnt - 1; i >= 0; i--) {
blocks[i] = all.subarray(32 * i, 32 * i + 32)
sodium.crypto_generichash(blocks[i], prev)
prev = blocks[i]
}
return blocks
}
+261
View File
@@ -0,0 +1,261 @@
const c = require('compact-encoding')
const Cache = require('xache')
const safetyCatch = require('safety-catch')
const b4a = require('b4a')
const { handshake, holepunch } = require('./messages')
const { COMMANDS } = require('./constants')
const { BAD_HANDSHAKE_REPLY, BAD_HOLEPUNCH_REPLY } = require('./errors')
const FROM_CLIENT = 0
const FROM_SERVER = 1
const FROM_RELAY = 2
const FROM_SECOND_RELAY = 3
const REPLY = 4
// TODO: While the current design is very trustless in regards to clients/servers trusting the DHT,
// we should add a bunch of rate limits everywhere, especially including here to avoid bad users
// using a DHT node to relay traffic indiscriminately using the connect/holepunch messages.
// That's mostly from an abuse POV as none of the messsages do amplication.
module.exports = class Router {
constructor(dht, opts) {
this.dht = dht
this.forwards = new Cache(opts.forwards)
}
set(target, state) {
if (state.onpeerhandshake) {
this.forwards.retain(toString(target), state)
} else {
this.forwards.set(toString(target), state)
}
}
get(target) {
return this.forwards.get(toString(target))
}
delete(target) {
this.forwards.delete(toString(target))
}
destroy() {
this.forwards.destroy()
}
async peerHandshake(target, { noise, peerAddress, relayAddress, socket, session }, to) {
const dht = this.dht
const requestValue = c.encode(handshake, {
mode: FROM_CLIENT,
noise,
peerAddress,
relayAddress
})
const res = await dht.request(
{ command: COMMANDS.PEER_HANDSHAKE, target, value: requestValue },
to,
{ socket, session }
)
const hs = decode(handshake, res.value)
if (
!hs ||
hs.mode !== REPLY ||
to.host !== res.from.host ||
to.port !== res.from.port ||
!hs.noise
) {
throw BAD_HANDSHAKE_REPLY()
}
return {
noise: hs.noise,
relayed: !!hs.peerAddress,
serverAddress: hs.peerAddress || to,
clientAddress: res.to
}
}
async onpeerhandshake(req) {
const hs = req.value && decode(handshake, req.value)
if (!hs) return
const { mode, noise, peerAddress, relayAddress } = hs
const state = req.target && this.get(req.target)
const isServer = !!(state && state.onpeerhandshake)
const relay = state && state.relay
if (isServer) {
let reply = null
try {
reply = noise && (await state.onpeerhandshake({ noise, peerAddress }, req))
} catch (e) {
safetyCatch(e)
return
}
if (!reply || !reply.noise) return
const opts = { socket: reply.socket, closerNodes: false, token: false }
switch (mode) {
case FROM_CLIENT: {
req.reply(
c.encode(handshake, { mode: REPLY, noise: reply.noise, peerAddress: null }),
opts
)
return
}
case FROM_RELAY: {
req.relay(
c.encode(handshake, { mode: FROM_SERVER, noise: reply.noise, peerAddress }),
req.from,
opts
)
return
}
case FROM_SECOND_RELAY: {
if (!relayAddress) return
req.relay(
c.encode(handshake, { mode: FROM_SERVER, noise: reply.noise, peerAddress }),
relayAddress,
opts
)
return // eslint-disable-line
}
}
} else {
switch (mode) {
case FROM_CLIENT: {
// TODO: if no relay is known route closer to the target instead of timing out
if (!noise) return
if (!relay && !relayAddress) {
// help the user route
req.reply(null, { token: false, closerNodes: true })
return
}
req.relay(
c.encode(handshake, {
mode: FROM_RELAY,
noise,
peerAddress: req.from,
relayAddress: null
}),
relayAddress || relay
)
return
}
case FROM_RELAY: {
if (!relay || !noise) return
req.relay(
c.encode(handshake, {
mode: FROM_SECOND_RELAY,
noise,
peerAddress,
relayAddress: req.from
}),
relay
)
return
}
case FROM_SERVER: {
if (!peerAddress || !noise) return
req.reply(
c.encode(handshake, { mode: REPLY, noise, peerAddress: req.from, relayAddress: null }),
{ to: peerAddress, closerNodes: false, token: false }
)
return // eslint-disable-line
}
}
}
}
async peerHolepunch(target, { id, payload, peerAddress, socket, session }, to) {
const dht = this.dht
const requestValue = c.encode(holepunch, {
mode: FROM_CLIENT,
id,
payload,
peerAddress
})
const res = await dht.request(
{ command: COMMANDS.PEER_HOLEPUNCH, target, value: requestValue },
to,
{ socket, session }
)
const hp = decode(holepunch, res.value)
if (!hp || hp.mode !== REPLY || to.host !== res.from.host || to.port !== res.from.port) {
throw BAD_HOLEPUNCH_REPLY()
}
return {
from: res.from,
to: res.to,
payload: hp.payload,
peerAddress: hp.peerAddress || to
}
}
async onpeerholepunch(req) {
const hp = req.value && decode(holepunch, req.value)
if (!hp) return
const { mode, id, payload, peerAddress } = hp
const state = req.target && this.get(req.target)
const isServer = !!(state && state.onpeerholepunch)
const relay = state && state.relay
switch (mode) {
case FROM_CLIENT: {
if (!peerAddress && !relay) return
req.relay(
c.encode(holepunch, { mode: FROM_RELAY, id, payload, peerAddress: req.from }),
peerAddress || relay
)
return
}
case FROM_RELAY: {
if (!isServer || !peerAddress) return
let reply = null
try {
reply = await state.onpeerholepunch({ id, payload, peerAddress }, req)
} catch (e) {
safetyCatch(e)
return
}
if (!reply) return
const opts = { socket: reply.socket, closerNodes: false, token: false }
req.relay(
c.encode(holepunch, { mode: FROM_SERVER, id: 0, payload: reply.payload, peerAddress }),
req.from,
opts
)
return
}
case FROM_SERVER: {
req.reply(c.encode(holepunch, { mode: REPLY, id, payload, peerAddress: req.from }), {
to: peerAddress,
closerNodes: false,
token: false
})
return // eslint-disable-line
}
}
}
}
function decode(enc, val) {
try {
return c.decode(enc, val)
} catch {
return null
}
}
function toString(t) {
return typeof t === 'string' ? t : b4a.toString(t, 'hex')
}
+52
View File
@@ -0,0 +1,52 @@
const sodium = require('sodium-universal')
const b4a = require('b4a')
const { holepunchPayload } = require('./messages')
module.exports = class HolepunchPayload {
constructor(holepunchSecret) {
this._sharedSecret = holepunchSecret
this._localSecret = b4a.allocUnsafe(32)
sodium.randombytes_buf(this._localSecret)
}
decrypt(buffer) {
const state = { start: 24, end: buffer.byteLength - 16, buffer }
if (state.end <= state.start) return null
const nonce = buffer.subarray(0, 24)
const msg = state.buffer.subarray(state.start, state.end)
const cipher = state.buffer.subarray(state.start)
if (!sodium.crypto_secretbox_open_easy(msg, cipher, nonce, this._sharedSecret)) return null
try {
return holepunchPayload.decode(state)
} catch {
return null
}
}
encrypt(payload) {
const state = { start: 24, end: 24, buffer: null }
holepunchPayload.preencode(state, payload)
state.buffer = b4a.allocUnsafe(state.end + 16)
const nonce = state.buffer.subarray(0, 24)
const msg = state.buffer.subarray(state.start, state.end)
const cipher = state.buffer.subarray(state.start)
holepunchPayload.encode(state, payload)
sodium.randombytes_buf(nonce)
sodium.crypto_secretbox_easy(cipher, msg, nonce, this._sharedSecret)
return state.buffer
}
token(addr) {
const out = b4a.allocUnsafe(32)
sodium.crypto_generichash(out, b4a.from(addr.host), this._localSecret)
return out
}
}
+69
View File
@@ -0,0 +1,69 @@
const DONE = Promise.resolve(true)
const DESTROYED = Promise.resolve(false)
module.exports = class Semaphore {
constructor(limit = 1) {
this.limit = limit
this.active = 0
this.waiting = []
this.flushedPromise = null
this.flushedResolve = null
this.destroyed = false
this._onwait = this._queueWaiting.bind(this)
this._onflush = this._queueFlushed.bind(this)
}
_queueWaiting(resolve) {
this.waiting.push(resolve)
}
_queueFlushed(resolve) {
this.flushedResolve = resolve
}
wait() {
if (this.destroyed === true) return DESTROYED
if (this.active < this.limit && this.waiting.length === 0) {
this.active++
return DONE
}
return new Promise(this._onwait)
}
signal() {
if (this.destroyed === true) return
this.active--
while (this.active < this.limit && this.waiting.length > 0 && this.destroyed === false) {
this.active++
this.waiting.shift()(true)
}
if (this.active === 0 && this.flushedResolve) {
const resolve = this.flushedResolve
this.flushedResolve = null
this.flushedPromise = null
resolve(true)
}
}
async flush() {
if (this.destroyed === true) return
if (this.active === 0) return
if (this.flushedPromise) return this.flushedPromise
this.flushedPromise = new Promise(this._onflush)
return this.flushedPromise
}
destroy() {
this.destroyed = true
this.active = 0
while (this.waiting.length) this.waiting.pop()(false)
if (this.flushedResolve) this.flushedResolve(false)
}
}
+738
View File
@@ -0,0 +1,738 @@
const { EventEmitter } = require('events')
const safetyCatch = require('safety-catch')
const NoiseSecretStream = require('@hyperswarm/secret-stream')
const b4a = require('b4a')
const relay = require('blind-relay')
const NoiseWrap = require('./noise-wrap')
const Announcer = require('./announcer')
const { FIREWALL, ERROR } = require('./constants')
const { unslabbedHash } = require('./crypto')
const SecurePayload = require('./secure-payload')
const Holepuncher = require('./holepuncher')
const { isPrivate } = require('bogon')
const { ALREADY_LISTENING, NODE_DESTROYED, KEYPAIR_ALREADY_USED } = require('./errors')
const HANDSHAKE_CLEAR_WAIT = 10000
const HANDSHAKE_INITIAL_TIMEOUT = 10000
module.exports = class Server extends EventEmitter {
constructor(dht, opts = {}) {
super()
this.dht = dht
this.target = null
this.closed = false
this.firewall = opts.firewall || (() => false)
this.holepunch = opts.holepunch || (() => true)
this.relayThrough = opts.relayThrough || null
this.relayKeepAlive = opts.relayKeepAlive || 5000
this.pool = opts.pool || null
this.createHandshake = opts.createHandshake || defaultCreateHandshake
this.createSecretStream = opts.createSecretStream || defaultCreateSecretStream
this.suspended = false
this.handshakeClearWait = opts.handshakeClearWait || HANDSHAKE_CLEAR_WAIT
this._shareLocalAddress = opts.shareLocalAddress !== false
this._reusableSocket = !!opts.reusableSocket
this._neverPunch = opts.holepunch === false // useful for fully disabling punching
this._keyPair = null
this._announcer = null
this._connects = new Map()
this._holepunches = []
this._listening = null
this._closing = null
}
get listening() {
return this._listening !== null
}
get publicKey() {
return this._keyPair && this._keyPair.publicKey
}
get relayAddresses() {
return this._announcer ? this._announcer.relayAddresses : []
}
onconnection(encryptedSocket) {
this.emit('connection', encryptedSocket)
}
async suspend({ log = noop } = {}) {
log('Suspending hyperdht server')
if (this._listening !== null) await this._listening
log('Suspending hyperdht server (post listening)')
this.suspended = true
this._clearAll()
return this._announcer ? this._announcer.suspend({ log }) : Promise.resolve()
}
async resume() {
if (this._listening !== null) await this._listening
this.suspended = false
return this._announcer ? this._announcer.resume() : Promise.resolve()
}
address() {
if (!this._keyPair) return null
return {
publicKey: this._keyPair.publicKey,
host: this.dht.host,
port: this.dht.port
}
}
close() {
if (this._closing) return this._closing
this._closing = this._close()
return this._closing
}
_gc() {
this.dht.listening.delete(this)
if (this.target) this.dht._router.delete(this.target)
}
async _stopListening() {
try {
if (this._announcer) await this._announcer.stop()
} catch {
// ignore
}
this._announcer = null
this._listening = null
this._keyPair = null
}
async _close() {
if (this._listening === null) {
this.closed = true
this.emit('close')
return
}
try {
await this._listening
} catch (err) {
safetyCatch(err)
}
this._gc()
this._clearAll()
await this._stopListening()
this.closed = true
this.emit('close')
}
_clearAll() {
while (this._holepunches.length > 0) {
const h = this._holepunches.pop()
if (h && h.puncher) h.puncher.destroy()
if (h && h.clearing) clearTimeout(h.clearing)
if (h && h.prepunching) clearTimeout(h.prepunching)
if (h && h.rawStream) h.rawStream.destroy()
}
this._connects.clear()
}
async listen(keyPair = this.dht.defaultKeyPair, opts = {}) {
if (this._listening !== null) throw ALREADY_LISTENING()
if (this.dht.destroyed) throw NODE_DESTROYED()
this._listening = this._listen(keyPair, opts)
await this._listening
return this
}
async _listen(keyPair, opts) {
// From now on, the DHT object which created me is responsible for closing me
this.dht.listening.add(this)
try {
await this.dht.bind()
if (this._closing) return
for (const s of this.dht.listening) {
if (s._keyPair && b4a.equals(s._keyPair.publicKey, keyPair.publicKey)) {
throw KEYPAIR_ALREADY_USED()
}
}
this.target = unslabbedHash(keyPair.publicKey)
this._keyPair = keyPair
this._announcer = new Announcer(this.dht, keyPair, this.target, opts)
this.dht._router.set(this.target, {
relay: null,
record: this._announcer.record,
onpeerhandshake: this._onpeerhandshake.bind(this),
onpeerholepunch: this._onpeerholepunch.bind(this)
})
// warm it up for now
this._localAddresses().catch(safetyCatch)
await this._announcer.start()
} catch (err) {
await this._stopListening()
this._gc()
throw err
}
if (this._closing) return
if (this.suspended) await this._announcer.suspend()
if (this._closing) return
if (this.dht.destroyed) throw NODE_DESTROYED()
if (this.pool) this.pool._attachServer(this)
this.emit('listening')
}
refresh() {
if (this._announcer && !this.suspended) this._announcer.refresh()
}
notifyOnline() {
if (this._announcer) this._announcer.online.notify()
}
_localAddresses() {
return this.dht.validateLocalAddresses(Holepuncher.localAddresses(this.dht.io.serverSocket))
}
async _addHandshake(k, noise, clientAddress, { from, to: serverAddress, socket }, direct) {
let id = this._holepunches.indexOf(null)
if (id === -1) id = this._holepunches.push(null) - 1
const hs = {
round: 0,
reply: null,
puncher: null,
payload: null,
rawStream: null,
encryptedSocket: null,
prepunching: null,
firewalled: true,
clearing: null,
onsocket: null,
aborted: false,
// Relay state
relayTimeout: null,
relayToken: null,
relaySocket: null,
relayClient: null,
relayPaired: false
}
this._holepunches[id] = hs
const handshake = this.createHandshake(this._keyPair, null)
let remotePayload
try {
remotePayload = await handshake.recv(noise)
} catch (err) {
safetyCatch(err)
this._clearLater(hs, id, k)
return null
}
if (this._closing || this.suspended) return null
try {
hs.firewalled = await this.firewall(handshake.remotePublicKey, remotePayload, clientAddress)
} catch (err) {
safetyCatch(err)
}
if (this._closing || this.suspended) return null
if (hs.firewalled) {
this._clearLater(hs, id, k)
return null
}
const error =
remotePayload.version === 1
? remotePayload.udx
? ERROR.NONE
: ERROR.ABORTED
: ERROR.VERSION_MISMATCH
const addresses = []
const ourRemoteAddr = this.dht.remoteAddress()
const ourLocalAddrs = this._shareLocalAddress ? await this._localAddresses() : null
if (this._closing || this.suspended) return null
if (ourRemoteAddr) addresses.push(ourRemoteAddr)
if (ourLocalAddrs) addresses.push(...ourLocalAddrs)
if (error === ERROR.NONE) {
hs.rawStream = this.dht.createRawStream({
framed: true,
firewall(socket, port, host) {
if (!(port > 0 && port < 65536)) return true
// Check if the traffic originated from the socket on which we're expecting relay traffic. If so,
// we haven't hole punched yet and the other side is just sending us traffic through the relay.
if (hs.relaySocket && isRelay(hs.relaySocket, socket, port, host)) {
return false
}
hs.onsocket(socket, port, host)
return false
}
})
hs.rawStream.on('error', autoDestroy)
// Handles the case where onsocket is never called, but the stream got setup
// This can happen on a relayed connection which never connects directly
// (onsocket is called there only when the direct connection is established)
const onrawstreamclose = () => {
if (this._closing) return
this._clearLater(hs, id, k)
}
hs.rawStream.on('close', onrawstreamclose)
hs.onsocket = (socket, port, host) => {
if (hs.rawStream === null) return // Already hole punched
this._clearLater(hs, id, k)
if (hs.prepunching) {
clearTimeout(hs.prepunching)
hs.prepunching = null
}
if (this._reusableSocket && remotePayload.udx.reusableSocket) {
this.dht._socketPool.routes.add(handshake.remotePublicKey, hs.rawStream)
}
hs.rawStream.removeListener('error', autoDestroy)
hs.rawStream.removeListener('close', onrawstreamclose)
if (hs.rawStream.connected) {
const remoteChanging = hs.rawStream.changeRemote(socket, remotePayload.udx.id, port, host)
if (remoteChanging) remoteChanging.catch(safetyCatch)
} else {
hs.rawStream.connect(socket, remotePayload.udx.id, port, host)
hs.encryptedSocket = this.createSecretStream(false, hs.rawStream, {
handshake: h,
keepAlive: this.dht.connectionKeepAlive
})
this.onconnection(hs.encryptedSocket)
}
if (hs.puncher) {
hs.puncher.onabort = noop
hs.puncher.destroy()
}
hs.rawStream = null
}
function autoDestroy() {
if (hs.puncher) hs.puncher.destroy()
}
}
const relayAddresses = this.relayAddresses
const relayThrough = selectRelay(this.relayThrough)
if (relayThrough) hs.relayToken = relay.token()
try {
hs.reply = await handshake.send({
error,
firewall: ourRemoteAddr ? FIREWALL.OPEN : FIREWALL.UNKNOWN,
holepunch: ourRemoteAddr ? null : { id, relays: this._announcer.relays },
addresses4: addresses,
addresses6: null,
udx: {
reusableSocket: this._reusableSocket,
id: hs.rawStream ? hs.rawStream.id : 0,
seq: 0
},
secretStream: {},
relayThrough: relayThrough ? { publicKey: relayThrough, token: hs.relayToken } : null,
relayAddresses: relayAddresses.length ? relayAddresses : null
})
} catch (err) {
safetyCatch(err)
if (hs.rawStream) hs.rawStream.destroy()
this._clearLater(hs, id, k)
return null
}
if (this._closing || this.suspended) {
if (hs.rawStream) hs.rawStream.destroy()
return null
}
const h = handshake.final()
if (error !== ERROR.NONE) {
if (hs.rawStream) hs.rawStream.destroy()
this._clearLater(hs, id, k)
return hs
}
if (remotePayload.firewall === FIREWALL.OPEN || direct) {
const sock = direct ? socket : this.dht.socket
this.dht.stats.punches.open++
hs.onsocket(sock, clientAddress.port, clientAddress.host)
return hs
}
if (relayThrough || remotePayload.relayThrough) {
this._relayConnection(hs, relayThrough, remotePayload, h)
}
const onabort = () => {
hs.aborted = true
if (hs.prepunching) clearTimeout(hs.prepunching)
hs.prepunching = null
if (hs.rawStream.destroyed) {
this._clearLater(hs, id, k)
return
}
hs.rawStream.on('close', () => this._clearLater(hs, id, k))
if (hs.relayToken === null) hs.rawStream.destroy()
}
if (!direct && clientAddress.host === serverAddress.host) {
const clientAddresses = remotePayload.addresses4.filter(onlyPrivateHosts)
if (clientAddresses.length > 0 && this._shareLocalAddress) {
const myAddresses = await this._localAddresses()
const addr = Holepuncher.matchAddress(myAddresses, clientAddresses)
if (addr) {
hs.prepunching = setTimeout(onabort, HANDSHAKE_INITIAL_TIMEOUT)
return hs
}
}
}
if (this._closing || this.suspended) return null
if (ourRemoteAddr || this._neverPunch) {
hs.prepunching = setTimeout(onabort, HANDSHAKE_INITIAL_TIMEOUT)
return hs
}
hs.payload = new SecurePayload(h.holepunchSecret)
hs.puncher = new Holepuncher(this.dht, this.dht.session(), false, remotePayload.firewall)
hs.puncher.onconnect = hs.onsocket
hs.puncher.onabort = onabort
hs.prepunching = setTimeout(hs.puncher.destroy.bind(hs.puncher), HANDSHAKE_INITIAL_TIMEOUT)
return hs
}
_clearLater(hs, id, k) {
if (hs.clearing) return
hs.clearing = setTimeout(() => this._clear(hs, id, k), this.handshakeClearWait)
}
_clear(hs, id, k) {
if (id >= this._holepunches.length || this._holepunches[id] !== hs) return
if (hs.clearing) clearTimeout(hs.clearing)
this._holepunches[id] = null
while (
this._holepunches.length > 0 &&
this._holepunches[this._holepunches.length - 1] === null
) {
this._holepunches.pop()
}
this._connects.delete(k)
}
async _onpeerhandshake({ noise, peerAddress }, req) {
const k = b4a.toString(noise, 'hex')
// The next couple of statements MUST run within the same tick to prevent
// a malicious peer from flooding us with handshakes.
let p = this._connects.get(k)
if (!p) {
p = this._addHandshake(k, noise, peerAddress || req.from, req, !peerAddress)
this._connects.set(k, p)
}
const h = await p
if (!h) return null
if (this._closing !== null || this.suspended) return null
return { socket: h.puncher && h.puncher.socket, noise: h.reply }
}
async _onpeerholepunch({ id, peerAddress, payload }, req) {
const h = id < this._holepunches.length ? this._holepunches[id] : null
if (!h) return null
if (!peerAddress || this._closing !== null || this.suspended) return null
const p = h.puncher
if (!p || !p.socket) return this._abort(h) // not opened
const remotePayload = h.payload.decrypt(payload)
if (!remotePayload) return null
const isServerRelay = this._announcer.isRelay(req.from)
const { error, firewall, round, punching, addresses, remoteAddress, remoteToken } =
remotePayload
if (error !== ERROR.NONE) {
// We actually do not need to set the round here, but just do it for consistency.
if (round >= h.round) h.round = round
return this._abort(h)
}
const token = h.payload.token(peerAddress)
const echoed = isServerRelay && !!remoteToken && b4a.equals(token, remoteToken)
// Update our heuristics here
if (req.socket === p.socket) {
p.nat.add(req.to, req.from)
}
if (round >= h.round) {
h.round = round
p.updateRemote({ punching, firewall, addresses, verified: echoed ? peerAddress.host : null })
}
// Wait for the analyzer to reach a conclusion...
let stable = await p.analyze(false)
if (p.destroyed) return null
if (!p.remoteHolepunching && !stable) {
stable = await p.analyze(true)
if (p.destroyed) return null
if (!stable) return this._abort(h)
}
// Fast mode! If we are consistent and the remote has opened a session to us (remoteAddress)
// then fire a quick punch back. Note the await here just waits for the udp socket to flush.
if (
isConsistent(p.nat.firewall) &&
remoteAddress &&
hasSameAddr(p.nat.addresses, remoteAddress)
) {
await p.ping(peerAddress)
if (p.destroyed) return null
}
// Remote said they are punching (or willing to), so we will punch as well.
// Note that this returns when the punching has STARTED, so no guarantee
// we will have a connection after this promise etc.
if (p.remoteHolepunching) {
// TODO: still continue here if a local connection might work, but then do not holepunch...
if (!this.holepunch(p.remoteFirewall, p.nat.firewall, p.remoteAddresses, p.nat.addresses)) {
return p.destroyed ? null : this._abort(h)
}
if (h.prepunching) {
clearTimeout(h.prepunching)
h.prepunching = null
}
if (p.remoteFirewall >= FIREWALL.RANDOM || p.nat.firewall >= FIREWALL.RANDOM) {
if (
this.dht._randomPunches >= this.dht._randomPunchLimit ||
Date.now() - this.dht._lastRandomPunch < this.dht._randomPunchInterval
) {
if (!h.relayToken) return this._abort(h, ERROR.TRY_LATER)
return {
socket: p.socket,
payload: h.payload.encrypt({
error: ERROR.TRY_LATER,
firewall: p.nat.firewall,
round: h.round,
connected: p.connected,
punching: p.punching,
addresses: p.nat.addresses,
remoteAddress: null,
token: isServerRelay ? token : null,
remoteToken: remotePayload.token
})
}
}
}
const punching = await p.punch()
if (p.destroyed) return null
if (!punching) return this._abort(h)
}
// Freeze that analysis as soon as we have a result we are giving to the other peer
if (p.nat.firewall !== FIREWALL.UNKNOWN) {
p.nat.freeze()
}
return {
socket: p.socket,
payload: h.payload.encrypt({
error: ERROR.NONE,
firewall: p.nat.firewall,
round: h.round,
connected: p.connected,
punching: p.punching,
addresses: p.nat.addresses,
remoteAddress: null,
token: isServerRelay ? token : null,
remoteToken: remotePayload.token
})
}
}
_abort(h, error = ERROR.ABORTED) {
if (!h.payload) {
if (h.puncher) h.puncher.destroy()
return null
}
const payload = h.payload.encrypt({
error,
firewall: FIREWALL.UNKNOWN,
round: h.round,
connected: false,
punching: false,
addresses: null,
remoteAddress: null,
token: null,
remoteToken: null
})
h.puncher.destroy()
return { socket: this.dht.socket, payload }
}
_relayConnection(hs, relayThrough, remotePayload, h) {
this.dht.stats.relaying.attempts++
let isInitiator
let publicKey
let token
if (relayThrough) {
isInitiator = true
publicKey = relayThrough
token = hs.relayToken
} else {
isInitiator = false
publicKey = remotePayload.relayThrough.publicKey
token = remotePayload.relayThrough.token
}
hs.relayToken = token
hs.relaySocket = this.dht.connect(publicKey)
hs.relaySocket.setKeepAlive(this.relayKeepAlive)
hs.relayClient = relay.Client.from(hs.relaySocket, { id: hs.relaySocket.publicKey })
hs.relayTimeout = setTimeout(onabort, 15000)
hs.relayClient
.pair(isInitiator, token, hs.rawStream)
.on('error', onabort)
.on('data', (remoteId) => {
if (hs.relayTimeout) clearRelayTimeout(hs)
if (hs.rawStream === null) {
onabort(null)
return
}
hs.relayPaired = true
this.dht.stats.relaying.successes++
if (hs.prepunching) clearTimeout(hs.prepunching)
hs.prepunching = null
const { remotePort, remoteHost, socket } = hs.relaySocket.rawStream
hs.rawStream
.on('close', () => hs.relaySocket.destroy())
.connect(socket, remoteId, remotePort, remoteHost)
hs.encryptedSocket = this.createSecretStream(false, hs.rawStream, { handshake: h })
this.onconnection(hs.encryptedSocket)
})
const dht = this.dht
function onabort() {
if (!hs.relayPaired) dht.stats.relaying.aborts++
if (hs.relayTimeout) clearRelayTimeout(hs)
const socket = hs.relaySocket
hs.relayToken = null
hs.relaySocket = null
if (socket) socket.destroy()
if (hs.aborted && hs.rawStream) hs.rawStream.destroy()
}
}
}
function clearRelayTimeout(hs) {
clearTimeout(hs.relayTimeout)
hs.relayTimeout = null
}
function isConsistent(fw) {
return fw === FIREWALL.OPEN || fw === FIREWALL.CONSISTENT
}
function hasSameAddr(addrs, other) {
if (addrs === null) return false
for (const addr of addrs) {
if (addr.port === other.port && addr.host === other.host) return true
}
return false
}
function defaultCreateHandshake(keyPair, remotePublicKey) {
return new NoiseWrap(keyPair, remotePublicKey)
}
function defaultCreateSecretStream(isInitiator, rawStream, opts) {
return new NoiseSecretStream(isInitiator, rawStream, opts)
}
function onlyPrivateHosts(addr) {
return isPrivate(addr.host)
}
function isRelay(relaySocket, socket, port, host) {
const stream = relaySocket.rawStream
if (!stream) return false
if (stream.socket !== socket) return false
return port === stream.remotePort && host === stream.remoteHost
}
function selectRelay(relayThrough) {
if (typeof relayThrough === 'function') relayThrough = relayThrough()
if (relayThrough === null) return null
if (Array.isArray(relayThrough)) {
return relayThrough[Math.floor(Math.random() * relayThrough.length)]
}
return relayThrough
}
function noop() {}
+35
View File
@@ -0,0 +1,35 @@
module.exports = class Sleeper {
constructor() {
this._timeout = null
this._resolve = null
this._start = (resolve) => {
this._resolve = resolve
}
this._trigger = () => {
if (this._resolve === null) return
const resolve = this._resolve
this._timeout = null
this._resolve = null
resolve()
}
}
pause(ms) {
const p = new Promise(this._start)
if (this._timeout !== null) {
clearTimeout(this._timeout)
this._trigger()
}
this._timeout = setTimeout(this._trigger, ms)
return p
}
resume() {
if (this._timeout !== null) {
clearTimeout(this._timeout)
this._trigger()
}
}
}
+219
View File
@@ -0,0 +1,219 @@
const b4a = require('b4a')
const LINGER_TIME = 3000
module.exports = class SocketPool {
constructor(dht, host) {
this._dht = dht
this._sockets = new Map()
this._lingering = new Set() // updated by the ref
this._host = host
this.routes = new SocketRoutes(this)
}
_onmessage(ref, data, address) {
this._dht.onmessage(ref.socket, data, address)
}
_add(ref) {
this._sockets.set(ref.socket, ref)
}
_remove(ref) {
this._sockets.delete(ref.socket)
this._lingering.delete(ref)
}
lookup(socket) {
return this._sockets.get(socket) || null
}
setReusable(socket, bool) {
const ref = this.lookup(socket)
if (ref) ref.reusable = bool
}
acquire() {
// TODO: Enable socket reuse
return new SocketRef(this)
}
async destroy() {
const closing = []
for (const ref of this._sockets.values()) {
ref._unlinger()
closing.push(ref.socket.close())
}
await Promise.allSettled(closing)
}
}
class SocketRoutes {
constructor(pool) {
this._pool = pool
this._routes = new Map()
}
add(publicKey, rawStream) {
if (rawStream.socket) this._onconnect(publicKey, rawStream)
else rawStream.on('connect', this._onconnect.bind(this, publicKey, rawStream))
}
get(publicKey) {
const id = b4a.toString(publicKey, 'hex')
const route = this._routes.get(id)
if (!route) return null
return route
}
_onconnect(publicKey, rawStream) {
const id = b4a.toString(publicKey, 'hex')
const socket = rawStream.socket
let route = this._routes.get(id)
if (!route) {
const gc = () => {
if (this._routes.get(id) === route) this._routes.delete(id)
socket.removeListener('close', gc)
}
route = {
socket,
address: { host: rawStream.remoteHost, port: rawStream.remotePort },
gc
}
this._routes.set(id, route)
socket.on('close', gc)
}
this._pool.setReusable(socket, true)
rawStream.on('error', () => {
this._pool.setReusable(socket, false)
if (!route) route = this._routes.get(id)
if (route && route.socket === socket) route.gc()
})
}
}
// TODO: we should just make some "user data" object on udx to allow to attach this info
class SocketRef {
constructor(pool) {
this._pool = pool
// Events
this.onholepunchmessage = noop
// Whether it should teardown immediately or wait a bit
this.reusable = false
this.socket = pool._dht.udx.createSocket()
this.socket
.on('close', this._onclose.bind(this))
.on('message', this._onmessage.bind(this))
.on('idle', this._onidle.bind(this))
.on('busy', this._onbusy.bind(this))
.bind(0, this._pool._host)
this._refs = 1
this._released = false
this._closed = false
this._timeout = null
this._wasBusy = false
this._pool._add(this)
}
_onclose() {
this._pool._remove(this)
}
_onmessage(data, address) {
if (data.byteLength > 1) {
this._pool._onmessage(this, data, address)
} else {
this.onholepunchmessage(data, address, this)
}
}
_onidle() {
this._closeMaybe()
}
_onbusy() {
this._wasBusy = true
this._unlinger()
}
_reset() {
this.onholepunchmessage = noop
}
_closeMaybe() {
if (this._refs === 0 && this.socket.idle && !this._timeout) this._close()
}
_lingeringClose() {
this._pool._lingering.delete(this)
this._timeout = null
this._closeMaybe()
}
_close() {
this._unlinger()
if (this.reusable && this._wasBusy) {
this._wasBusy = false
this._pool._lingering.add(this)
this._timeout = setTimeout(this._lingeringClose.bind(this), LINGER_TIME)
return
}
this._closed = true
this.socket.close()
}
_unlinger() {
if (this._timeout !== null) {
clearTimeout(this._timeout)
this._pool._lingering.delete(this)
this._timeout = null
}
}
get free() {
return this._refs === 0
}
active() {
this._refs++
this._unlinger()
}
inactive() {
this._refs--
this._closeMaybe()
}
address() {
return this.socket.address()
}
release() {
if (this._released) return
this._released = true
this._reset()
this._refs--
this._closeMaybe()
}
}
function noop() {}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+165
View File
@@ -0,0 +1,165 @@
# compact-encoding
A series of compact encoding schemes for building small and fast parsers and serializers
```
npm install compact-encoding
```
## Usage
```js
const cenc = require('compact-encoding')
const state = cenc.state()
// use preencode to figure out how big a buffer is needed
cenc.uint.preencode(state, 42)
cenc.string.preencode(state, 'hi')
console.log(state) // { start: 0, end: 4, buffer: null }
state.buffer = Buffer.allocUnsafe(state.end)
// then use encode to actually encode it to the buffer
cenc.uint.encode(state, 42)
cenc.string.encode(state, 'hi')
// to decode it simply use decode instead
state.start = 0
cenc.uint.decode(state) // 42
cenc.string.decode(state) // 'hi'
```
## Encoder API
#### `state`
Should be an object that looks like this `{ start, end, buffer }`.
You can also get a blank state object using `cenc.state()`.
- `start` is the byte offset to start encoding/decoding at.
- `end` is the byte offset indicating the end of the buffer.
- `buffer` should be either a Node.js Buffer or Uint8Array.
#### `enc.preencode(state, val)`
Does a fast preencode dry-run that only sets state.end.
Use this to figure out how big of a buffer you need.
#### `enc.encode(state, val)`
Encodes `val` into `state.buffer` at position `state.start`.
Updates `state.start` to point after the encoded value when done.
#### `val = enc.decode(state)`
Decodes a value from `state.buffer` as position `state.start`.
Updates `state.start` to point after the decoded value when done in the buffer.
## Helpers
If you are just encoding to a buffer or decoding from one you can use the `encode` and `decode` helpers
to reduce your boilerplate
```js
const buf = cenc.encode(cenc.bool, true)
const bool = cenc.decode(cenc.bool, buf)
```
## Bundled encodings
The following encodings are bundled as they are primitives that can be used
to build others on top. Feel free to PR more that are missing.
- `cenc.raw` - Pass through encodes a buffer, i.e. a basic copy.
- `cenc.uint` - Encodes a uint using the smallest fixed size encoding with a prefix to signal which one. Useful for uints that can be a wide range of values.
- `cenc.uint8` - Encodes a fixed size uint8.
- `cenc.uint16` - Encodes a fixed size uint16. Useful for things like ports.
- `cenc.uint24` - Encodes a fixed size uint24. Useful for message framing.
- `cenc.uint32` - Encodes a fixed size uint32. Useful for very large message framing.
- `cenc.uint40` - Encodes a fixed size uint40.
- `cenc.uint48` - Encodes a fixed size uint48.
- `cenc.uint56` - Encodes a fixed size uint56.
- `cenc.uint64` - Encodes a fixed size uint64.
- `cenc.int` - Encodes an int using `cenc.uint` with ZigZag encoding.
- `cenc.int8` - Encodes a fixed size int8 using `cenc.uint8` with ZigZag encoding.
- `cenc.int16` - Encodes a fixed size int16 using `cenc.uint16` with ZigZag encoding.
- `cenc.int24` - Encodes a fixed size int24 using `cenc.uint24` with ZigZag encoding.
- `cenc.int32` - Encodes a fixed size int32 using `cenc.uint32` with ZigZag encoding.
- `cenc.int40` - Encodes a fixed size int40 using `cenc.uint40` with ZigZag encoding.
- `cenc.int48` - Encodes a fixed size int48 using `cenc.uint48` with ZigZag encoding.
- `cenc.int56` - Encodes a fixed size int56 using `cenc.uint56` with ZigZag encoding.
- `cenc.int64` - Encodes a fixed size int64 using `cenc.uint64` with ZigZag encoding.
- `cenc.biguint64` - Encodes a fixed size biguint64.
- `cenc.bigint64` - Encodes a fixed size bigint64 using `cenc.biguint64` with ZigZag encoding.
- `cenc.biguint` - Encodes a biguint with its word count uint prefixed.
- `cenc.bigint` - Encodes a bigint using `cenc.biguint` with ZigZag encoding.
- `cenc.float32` - Encodes a fixed size float32.
- `cenc.float64` - Encodes a fixed size float64.
- `cenc.buffer` - Encodes a buffer with its length uint prefixed. When decoding an empty buffer, `null` is returned.
- `cenc.raw.buffer` - Encodes a buffer without a length prefixed.
- `cenc.arraybuffer` - Encodes an arraybuffer with its length uint prefixed.
- `cenc.raw.arraybuffer` - Encodes an arraybuffer without a length prefixed.
- `cenc.uint8array` - Encodes a uint8array with its element length uint prefixed.
- `cenc.raw.uint8array` - Encodes a uint8array without a length prefixed.
- `cenc.uint16array` - Encodes a uint16array with its element length uint prefixed.
- `cenc.raw.uint16array` - Encodes a uint16array without a length prefixed.
- `cenc.uint32array` - Encodes a uint32array with its element length uint prefixed.
- `cenc.raw.uint32array` - Encodes a uint32array without a length prefixed.
- `cenc.int8array` - Encodes a int8array with its element length uint prefixed.
- `cenc.raw.int8array` - Encodes a int8array without a length prefixed.
- `cenc.int16array` - Encodes a int16array with its element length uint prefixed.
- `cenc.raw.int16array` - Encodes a int16array without a length prefixed.
- `cenc.int32array` - Encodes a int32array with its element length uint prefixed.
- `cenc.raw.int32array` - Encodes a int32array without a length prefixed.
- `cenc.biguint64array` - Encodes a biguint64array with its element length uint prefixed.
- `cenc.raw.biguint64array` - Encodes a biguint64array without a length prefixed.
- `cenc.bigint64array` - Encodes a bigint64array with its element length uint prefixed.
- `cenc.raw.bigint64array` - Encodes a bigint64array without a length prefixed.
- `cenc.float32array` - Encodes a float32array with its element length uint prefixed.
- `cenc.raw.float32array` - Encodes a float32array without a length prefixed.
- `cenc.float64array` - Encodes a float64array with its element length uint prefixed.
- `cenc.raw.float64array` - Encodes a float64array without a length prefixed.
- `cenc.bool` - Encodes a boolean as 1 or 0.
- `cenc.string`, `cenc.utf8` - Encodes a utf-8 string, similar to buffer.
- `cenc.raw.string`, `cenc.raw.utf8` - Encodes a utf-8 string without a length prefixed.
- `cenc.string.fixed(n)`, `cenc.utf8.fixed(n)` - Encodes a fixed sized utf-8 string.
- `cenc.ascii` - Encodes an ascii string.
- `cenc.raw.ascii` - Encodes an ascii string without a length prefixed.
- `cenc.ascii.fixed(n)` - Encodes a fixed size ascii string.
- `cenc.hex` - Encodes a hex string.
- `cenc.raw.hex` - Encodes a hex string without a length prefixed.
- `cenc.hex.fixed(n)` - Encodes a fixed size hex string.
- `cenc.base64` - Encodes a base64 string.
- `cenc.raw.base64` - Encodes a base64 string without a length prefixed.
- `cenc.base64.fixed(n)` - Encodes a fixed size base64 string.
- `cenc.utf16le`, `cenc.ucs2` - Encodes a utf16le string.
- `cenc.raw.utf16le`, `cenc.raw.ucs2` - Encodes a utf16le string without a length prefixed.
- `cenc.utf16le.fixed(n)`, `cenc.ucs2.fixed(n)` - Encodes a fixed size utf16le string.
- `cenc.fixed32` - Encodes a fixed 32 byte buffer.
- `cenc.fixed64` - Encodes a fixed 64 byte buffer.
- `cenc.fixed(n)` - Makes a fixed sized encoder.
- `cenc.date(d)` - Encodes a date object.
- `cenc.array(enc)` - Makes an array encoder from another encoder. Arrays are uint prefixed with their length.
- `cenc.raw.array(enc)` - Makes an array encoder from another encoder, without a length prefixed.
- `cenc.json` - Encodes a JSON value as utf-8.
- `cenc.raw.json` - Encodes a JSON value as utf-8 without a length prefixed.
- `cenc.ndjson` - Encodes a JSON value as newline delimited utf-8.
- `cenc.raw.ndjson` - Encodes a JSON value as newline delimited utf-8 without a length prefixed.
- `cenc.any` - Encodes any JSON representable value into a self described buffer. Like JSON + buffer, but using compact types. Useful for schemaless codecs.
- `cenc.port` - Encodes a port number for network addresses.
- `cenc.ipv4` - Encodes an IPv4 network address.
- `cenc.ipv4Address` Encodes an IPv4 network address and a port number.
- `cenc.ipv6` - Encodes an IPv6 network address.
- `cenc.ipv6Address` Encodes an IPv6 network address and a port number.
- `cenc.ip` - Encodes a dual IPv4/6 network address.
- `cenc.ipAddress` Encodes a dual IPv4/6 network address and a port number.
- `cenc.from(enc)` - Makes a compact encoder from a [codec](https://github.com/mafintosh/codecs) or [abstract-encoding](https://github.com/mafintosh/abstract-encoding).
- `cenc.none` - Helper for when you want to just express nothing
## License
Apache 2.0
+4
View File
@@ -0,0 +1,4 @@
const LE = (exports.LE =
new Uint8Array(new Uint16Array([0xff]).buffer)[0] === 0xff)
exports.BE = !LE
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
module.exports = {
preencode,
encode,
decode
}
function preencode(state, num) {
if (num < 251) {
state.end++
} else if (num < 256) {
state.end += 2
} else if (num < 0x10000) {
state.end += 3
} else if (num < 0x1000000) {
state.end += 4
} else if (num < 0x100000000) {
state.end += 5
} else {
state.end++
const exp = Math.floor(Math.log(num) / Math.log(2)) - 32
preencode(state, exp)
state.end += 6
}
}
function encode(state, num) {
const max = 251
const x = num - max
if (num < max) {
state.buffer[state.start++] = num
} else if (num < 256) {
state.buffer[state.start++] = max
state.buffer[state.start++] = x
} else if (num < 0x10000) {
state.buffer[state.start++] = max + 1
state.buffer[state.start++] = (x >> 8) & 0xff
state.buffer[state.start++] = x & 0xff
} else if (num < 0x1000000) {
state.buffer[state.start++] = max + 2
state.buffer[state.start++] = x >> 16
state.buffer[state.start++] = (x >> 8) & 0xff
state.buffer[state.start++] = x & 0xff
} else if (num < 0x100000000) {
state.buffer[state.start++] = max + 3
state.buffer[state.start++] = x >> 24
state.buffer[state.start++] = (x >> 16) & 0xff
state.buffer[state.start++] = (x >> 8) & 0xff
state.buffer[state.start++] = x & 0xff
} else {
// need to use Math here as bitwise ops are 32 bit
const exp = Math.floor(Math.log(x) / Math.log(2)) - 32
state.buffer[state.start++] = 0xff
encode(state, exp)
const rem = x / Math.pow(2, exp - 11)
for (let i = 5; i >= 0; i--) {
state.buffer[state.start++] = (rem / Math.pow(2, 8 * i)) & 0xff
}
}
}
function decode(state) {
const max = 251
if (state.end - state.start < 1) throw new Error('Out of bounds')
const flag = state.buffer[state.start++]
if (flag < max) return flag
if (state.end - state.start < flag - max + 1) {
throw new Error('Out of bounds.')
}
if (flag < 252) {
return state.buffer[state.start++] + max
}
if (flag < 253) {
return (
(state.buffer[state.start++] << 8) + state.buffer[state.start++] + max
)
}
if (flag < 254) {
return (
(state.buffer[state.start++] << 16) +
(state.buffer[state.start++] << 8) +
state.buffer[state.start++] +
max
)
}
// << 24 result may be interpreted as negative
if (flag < 255) {
return (
state.buffer[state.start++] * 0x1000000 +
(state.buffer[state.start++] << 16) +
(state.buffer[state.start++] << 8) +
state.buffer[state.start++] +
max
)
}
const exp = decode(state)
if (state.end - state.start < 6) throw new Error('Out of bounds')
let rem = 0
for (let i = 5; i >= 0; i--) {
rem += state.buffer[state.start++] * Math.pow(2, 8 * i)
}
return rem * Math.pow(2, exp - 11) + max
}
+34
View File
@@ -0,0 +1,34 @@
{
"name": "compact-encoding",
"version": "3.1.0",
"description": "A series of compact encoding schemes for building small and fast parsers and serializers",
"main": "index.js",
"files": [
"endian.js",
"index.js",
"lexint.js",
"raw.js"
],
"dependencies": {
"b4a": "^1.3.0"
},
"devDependencies": {
"brittle": "^3.0.0",
"prettier": "^3.6.2",
"prettier-config-holepunch": "^1.0.0"
},
"scripts": {
"format": "prettier . --write",
"test": "prettier . --check && brittle test.js"
},
"repository": {
"type": "git",
"url": "https://github.com/holepunchto/compact-encoding.git"
},
"author": "Mathias Buus (@mafintosh)",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/holepunchto/compact-encoding/issues"
},
"homepage": "https://github.com/holepunchto/compact-encoding"
}
+170
View File
@@ -0,0 +1,170 @@
const b4a = require('b4a')
const { BE } = require('./endian')
exports = module.exports = {
preencode(state, b) {
state.end += b.byteLength
},
encode(state, b) {
state.buffer.set(b, state.start)
state.start += b.byteLength
},
decode(state) {
const b = state.buffer.subarray(state.start, state.end)
state.start = state.end
return b
}
}
const buffer = (exports.buffer = {
preencode(state, b) {
uint8array.preencode(state, b)
},
encode(state, b) {
uint8array.encode(state, b)
},
decode(state) {
const b = state.buffer.subarray(state.start)
state.start = state.end
return b
}
})
exports.binary = {
...buffer,
preencode(state, b) {
if (typeof b === 'string') utf8.preencode(state, b)
else buffer.preencode(state, b)
},
encode(state, b) {
if (typeof b === 'string') utf8.encode(state, b)
else buffer.encode(state, b)
}
}
exports.arraybuffer = {
preencode(state, b) {
state.end += b.byteLength
},
encode(state, b) {
const view = new Uint8Array(b)
state.buffer.set(view, state.start)
state.start += b.byteLength
},
decode(state) {
const b = new ArrayBuffer(state.end - state.start)
const view = new Uint8Array(b)
view.set(state.buffer.subarray(state.start))
state.start = state.end
return b
}
}
function typedarray(TypedArray, swap) {
const n = TypedArray.BYTES_PER_ELEMENT
return {
preencode(state, b) {
state.end += b.byteLength
},
encode(state, b) {
const view = new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
if (BE && swap) swap(view)
state.buffer.set(view, state.start)
state.start += b.byteLength
},
decode(state) {
let b = state.buffer.subarray(state.start)
if (b.byteOffset % n !== 0) b = new Uint8Array(b)
if (BE && swap) swap(b)
state.start = state.end
return new TypedArray(b.buffer, b.byteOffset, b.byteLength / n)
}
}
}
const uint8array = (exports.uint8array = typedarray(Uint8Array))
exports.uint16array = typedarray(Uint16Array, b4a.swap16)
exports.uint32array = typedarray(Uint32Array, b4a.swap32)
exports.int8array = typedarray(Int8Array)
exports.int16array = typedarray(Int16Array, b4a.swap16)
exports.int32array = typedarray(Int32Array, b4a.swap32)
exports.biguint64array = typedarray(BigUint64Array, b4a.swap64)
exports.bigint64array = typedarray(BigInt64Array, b4a.swap64)
exports.float32array = typedarray(Float32Array, b4a.swap32)
exports.float64array = typedarray(Float64Array, b4a.swap64)
function string(encoding) {
return {
preencode(state, s) {
state.end += b4a.byteLength(s, encoding)
},
encode(state, s) {
state.start += b4a.write(state.buffer, s, state.start, encoding)
},
decode(state) {
const s = b4a.toString(state.buffer, encoding, state.start)
state.start = state.end
return s
}
}
}
const utf8 = (exports.string = exports.utf8 = string('utf-8'))
exports.ascii = string('ascii')
exports.hex = string('hex')
exports.base64 = string('base64')
exports.ucs2 = exports.utf16le = string('utf16le')
exports.array = function array(enc) {
return {
preencode(state, list) {
for (const value of list) enc.preencode(state, value)
},
encode(state, list) {
for (const value of list) enc.encode(state, value)
},
decode(state) {
const arr = []
while (state.start < state.end) arr.push(enc.decode(state))
return arr
}
}
}
exports.json = {
preencode(state, v) {
utf8.preencode(state, JSON.stringify(v))
},
encode(state, v) {
utf8.encode(state, JSON.stringify(v))
},
decode(state) {
return JSON.parse(utf8.decode(state))
}
}
exports.ndjson = {
preencode(state, v) {
utf8.preencode(state, JSON.stringify(v) + '\n')
},
encode(state, v) {
utf8.encode(state, JSON.stringify(v) + '\n')
},
decode(state) {
return JSON.parse(utf8.decode(state))
}
}
+79
View File
@@ -0,0 +1,79 @@
{
"name": "hyperdht",
"version": "6.32.0",
"description": "The DHT powering Hyperswarm",
"main": "index.js",
"browser": "browser.js",
"bin": {
"hyperdht": "./bin.js"
},
"files": [
"index.js",
"browser.js",
"testnet.js",
"bin.js",
"lib/**.js"
],
"imports": {
"events": {
"bare": "bare-events",
"default": "events"
},
"child_process": {
"bare": "bare-node-child-process",
"default": "child_process"
}
},
"dependencies": {
"@hyperswarm/secret-stream": "^6.6.2",
"b4a": "^1.3.1",
"bare-events": "^2.2.0",
"blind-relay": "^1.3.0",
"bogon": "^1.0.0",
"compact-encoding": "^3.0.0",
"dht-rpc": "^6.15.1",
"hypercore-crypto": "^3.3.0",
"hypercore-id-encoding": "^1.2.0",
"hyperdht-address": "^1.0.1",
"noise-curve-ed": "^2.0.0",
"noise-handshake": "^4.0.0",
"record-cache": "^1.1.1",
"safety-catch": "^1.0.1",
"signal-promise": "^1.0.3",
"sodium-universal": "^5.0.1",
"streamx": "^2.16.1",
"unslab": "^1.3.0",
"xache": "^1.1.0"
},
"devDependencies": {
"bare-node-child-process": "^1.0.1",
"brittle": "^3.0.0",
"graceful-goodbye": "^1.3.0",
"newline-decoder": "^1.0.2",
"prettier": "^3.6.2",
"prettier-config-holepunch": "^2.0.0"
},
"scripts": {
"format": "prettier --write .",
"test": "prettier --check . && node test/all.js",
"test:bare": "bare test/all.js",
"test:generate": "brittle -r test/all.js test/*.js",
"integration": "brittle test/integration/*.js",
"end-to-end": "brittle test/end-to-end/*.js"
},
"author": "Mathias Buus (@mafintosh)",
"license": "MIT",
"directories": {
"lib": "lib",
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/holepunchto/hyperdht.git"
},
"keywords": [],
"bugs": {
"url": "https://github.com/holepunchto/hyperdht/issues"
},
"homepage": "https://github.com/holepunchto/hyperdht#readme"
}
+81
View File
@@ -0,0 +1,81 @@
const DHT = require('.')
module.exports = async function createTestnet(size = 10, opts = {}) {
const swarm = []
const teardown =
typeof opts === 'function' ? opts : opts.teardown ? opts.teardown.bind(opts) : noop
const host = opts.host || '127.0.0.1'
const port = opts.port || 0
const bootstrap = opts.bootstrap ? [...opts.bootstrap] : []
const bindHost = host === '127.0.0.1' ? '127.0.0.1' : '0.0.0.0'
if (size === 0) return new Testnet(swarm)
const first = new DHT({
ephemeral: false,
firewalled: false,
bootstrap,
port,
host: bindHost
})
await first.fullyBootstrapped()
if (bootstrap.length === 0) bootstrap.push({ host, port: first.address().port })
swarm.push(first)
while (swarm.length < size) {
const node = new DHT({
ephemeral: false,
firewalled: false,
bootstrap,
host: bindHost
})
await node.fullyBootstrapped()
swarm.push(node)
}
const testnet = new Testnet(swarm, bootstrap)
teardown(() => testnet.destroy(), { order: Infinity })
return testnet
}
class Testnet {
constructor(nodes, bootstrap = []) {
this.nodes = nodes
this.bootstrap = bootstrap
}
createNode(opts = {}) {
const node = new DHT({
ephemeral: true,
bootstrap: this.bootstrap,
host: '127.0.0.1',
...opts
})
this.nodes.push(node)
return node
}
async destroy() {
for (const node of this.nodes) {
for (const server of node.listening) await server.close()
}
for (let i = this.nodes.length - 1; i >= 0; i--) {
await this.nodes[i].destroy()
}
}
[Symbol.iterator]() {
return this.nodes[Symbol.iterator]()
}
}
function noop() {}