mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-04-25 18:42:15 +00:00
The retry operation in the NAT Manager can prevent node from shutting down, so unref the retries so they don't keep adding work to the event loop.
169 lines
5.1 KiB
JavaScript
169 lines
5.1 KiB
JavaScript
'use strict'
|
|
|
|
const NatAPI = require('@motrix/nat-api')
|
|
const debug = require('debug')
|
|
const promisify = require('promisify-es6')
|
|
const Multiaddr = require('multiaddr')
|
|
const log = Object.assign(debug('libp2p:nat'), {
|
|
error: debug('libp2p:nat:err')
|
|
})
|
|
const { isBrowser } = require('ipfs-utils/src/env')
|
|
const retry = require('p-retry')
|
|
const isPrivateIp = require('private-ip')
|
|
const pkg = require('../package.json')
|
|
const errcode = require('err-code')
|
|
const {
|
|
codes: { ERR_INVALID_PARAMETERS }
|
|
} = require('./errors')
|
|
const isLoopback = require('libp2p-utils/src/multiaddr/is-loopback')
|
|
|
|
/**
|
|
* @typedef {import('peer-id')} PeerId
|
|
* @typedef {import('./transport-manager')} TransportManager
|
|
* @typedef {import('./address-manager')} AddressManager
|
|
*/
|
|
|
|
function highPort (min = 1024, max = 65535) {
|
|
return Math.floor(Math.random() * (max - min + 1) + min)
|
|
}
|
|
|
|
const DEFAULT_TTL = 7200
|
|
|
|
class NatManager {
|
|
/**
|
|
* @class
|
|
* @param {object} options
|
|
* @param {PeerId} options.peerId - The peer ID of the current node
|
|
* @param {TransportManager} options.transportManager - A transport manager
|
|
* @param {AddressManager} options.addressManager - An address manager
|
|
* @param {boolean} options.enabled - Whether to enable the NAT manager
|
|
* @param {string} [options.externalIp] - Pass a value to use instead of auto-detection
|
|
* @param {string} [options.description] - A string value to use for the port mapping description on the gateway
|
|
* @param {number} [options.ttl] - How long UPnP port mappings should last for in seconds (minimum 1200)
|
|
* @param {boolean} [options.keepAlive] - Whether to automatically refresh UPnP port mappings when their TTL is reached
|
|
* @param {string} [options.gateway] - Pass a value to use instead of auto-detection
|
|
* @param {object} [options.pmp] - PMP options
|
|
* @param {boolean} [options.pmp.enabled] - Whether to enable PMP as well as UPnP
|
|
*/
|
|
constructor ({ peerId, addressManager, transportManager, ...options }) {
|
|
this._peerId = peerId
|
|
this._addressManager = addressManager
|
|
this._transportManager = transportManager
|
|
|
|
this._enabled = options.enabled
|
|
this._externalIp = options.externalIp
|
|
this._options = {
|
|
description: options.description || `${pkg.name}@${pkg.version} ${this._peerId}`,
|
|
ttl: options.ttl || DEFAULT_TTL,
|
|
autoUpdate: options.keepAlive || true,
|
|
gateway: options.gateway,
|
|
enablePMP: Boolean(options.pmp && options.pmp.enabled)
|
|
}
|
|
|
|
if (this._options.ttl < DEFAULT_TTL) {
|
|
throw errcode(new Error(`NatManager ttl should be at least ${DEFAULT_TTL} seconds`), ERR_INVALID_PARAMETERS)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the NAT manager
|
|
*/
|
|
start () {
|
|
if (isBrowser || !this._enabled) {
|
|
return
|
|
}
|
|
|
|
// done async to not slow down startup
|
|
this._start().catch((err) => {
|
|
// hole punching errors are non-fatal
|
|
log.error(err)
|
|
})
|
|
}
|
|
|
|
async _start () {
|
|
const addrs = this._transportManager.getAddrs()
|
|
|
|
for (const addr of addrs) {
|
|
// try to open uPnP ports for each thin waist address
|
|
const { family, host, port, transport } = addr.toOptions()
|
|
|
|
if (!addr.isThinWaistAddress() || transport !== 'tcp') {
|
|
// only bare tcp addresses
|
|
continue
|
|
}
|
|
|
|
if (isLoopback(addr)) {
|
|
continue
|
|
}
|
|
|
|
if (family !== 'ipv4') {
|
|
// ignore ipv6
|
|
continue
|
|
}
|
|
|
|
const client = this._getClient()
|
|
const publicIp = this._externalIp || await client.externalIp()
|
|
|
|
if (isPrivateIp(publicIp)) {
|
|
throw new Error(`${publicIp} is private - please set config.nat.externalIp to an externally routable IP or ensure you are not behind a double NAT`)
|
|
}
|
|
|
|
const publicPort = highPort()
|
|
|
|
log(`opening uPnP connection from ${publicIp}:${publicPort} to ${host}:${port}`)
|
|
|
|
await client.map({
|
|
publicPort,
|
|
privatePort: port,
|
|
protocol: transport.toUpperCase()
|
|
})
|
|
|
|
this._addressManager.addObservedAddr(Multiaddr.fromNodeAddress({
|
|
family: 'IPv4',
|
|
address: publicIp,
|
|
port: `${publicPort}`
|
|
}, transport))
|
|
}
|
|
}
|
|
|
|
_getClient () {
|
|
if (this._client) {
|
|
return this._client
|
|
}
|
|
|
|
const client = new NatAPI(this._options)
|
|
const map = promisify(client.map, { context: client })
|
|
const destroy = promisify(client.destroy, { context: client })
|
|
const externalIp = promisify(client.externalIp, { context: client })
|
|
|
|
this._client = {
|
|
// these are all network operations so add a retry
|
|
map: (...args) => retry(() => map(...args), { onFailedAttempt: log.error, unref: true }),
|
|
destroy: (...args) => retry(() => destroy(...args), { onFailedAttempt: log.error, unref: true }),
|
|
externalIp: (...args) => retry(() => externalIp(...args), { onFailedAttempt: log.error, unref: true })
|
|
}
|
|
|
|
return this._client
|
|
}
|
|
|
|
/**
|
|
* Stops the NAT manager
|
|
*
|
|
* @async
|
|
*/
|
|
async stop () {
|
|
if (isBrowser || !this._client) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await this._client.destroy()
|
|
this._client = null
|
|
} catch (err) {
|
|
log.error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = NatManager
|