'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