UNPKG

nat-api

Version:

Port mapping with UPnP and NAT-PMP

470 lines (398 loc) 12.7 kB
const arrayRemove = require('unordered-array-remove') const defaultGateway = require('default-gateway') const debug = require('debug')('nat-api') const NatUPNP = require('./lib/upnp') const NatPMP = require('./lib/pmp') class NatAPI { /** * opts: * - ttl * - description * - gateway * - autoUpdate * - enablePMP (default = false) **/ constructor (opts = {}) { // TTL is 2 hours (min 20 min) this.ttl = (opts.ttl) ? Math.max(opts.ttl, 1200) : 7200 this.description = opts.description || 'NatAPI' this.gateway = opts.gateway || null this.autoUpdate = !!opts.autoUpdate || true // Refresh the mapping 10 minutes before the end of its lifetime this._timeout = (this.ttl - 600) * 1000 this._destroyed = false this._openPorts = [] this._upnpIntervals = {} this._pmpIntervals = {} // Setup UPnP Client this._upnpClient = NatUPNP.createClient() // Setup NAT-PMP Client this.enablePMP = !!opts.enablePMP if (this.enablePMP) { try { // Lookup gateway IP const results = defaultGateway.v4.sync() this._pmpClient = NatPMP.connect(results.gateway) } catch (err) { debug('Could not find gateway IP for NAT-PMP', err) this._pmpClient = null } } else { // Not necessary - but good for readability this._pmpClient = null } } /** * opts: * - publicPort * - privatePort * - protocol * - description * - ttl * - gateway **/ map (publicPort, privatePort, cbParam) { const self = this if (self._destroyed) throw new Error('client is destroyed') // Validate input const { opts, cb } = self._validateInput(publicPort, privatePort, cbParam) if (opts.protocol) { // UDP or TCP self._map(opts, function (err) { if (err) return cb(err) const newOpts = Object.assign({}, opts) self._openPorts.push(newOpts) cb() }) } else { // UDP & TCP const newOptsUDP = Object.assign({}, opts) newOptsUDP.protocol = 'UDP' self._map(newOptsUDP, function (err) { if (err) return cb(err) self._openPorts.push(newOptsUDP) const newOptsTCP = Object.assign({}, opts) newOptsTCP.protocol = 'TCP' self._map(newOptsTCP, function (err) { if (err) return cb(err) self._openPorts.push(newOptsTCP) cb() }) }) } } /** * opts: * - publicPort * - privatePort * - protocol * - description * - ttl * - gateway **/ unmap (publicPort, privatePort, cbParam) { const self = this if (self._destroyed) throw new Error('client is destroyed') // Validate input const { opts, cb } = self._validateInput(publicPort, privatePort, cbParam) arrayRemove(self._openPorts, self._openPorts.findIndex(function (o) { return (o.publicPort === opts.publicPort) && (o.privatePort === opts.privatePort) && (o.protocol === opts.protocol || opts.protocol == null) })) if (opts.protocol) { // UDP or TCP self._unmap(opts, function (err) { if (err) return cb(err) cb() }) } else { // UDP & TCP const newOptsUDP = Object.assign({}, opts) newOptsUDP.protocol = 'UDP' self._unmap(newOptsUDP, function (err) { if (err) return cb(err) const newOptsTCP = Object.assign({}, opts) newOptsTCP.protocol = 'TCP' self._unmap(newOptsTCP, function (err) { if (err) return cb(err) cb() }) }) } } destroy (cb) { const self = this if (self._destroyed) throw new Error('client already destroyed') if (!cb) cb = noop function continueDestroy () { self._destroyed = true // Close NAT-PMP client if (self._pmpClient) { debug('Close PMP client') self._pmpClient.close() } // Close UPNP Client if (self._upnpClient) { debug('Close UPnP client') self._upnpClient.destroy() } // Use callback for future versions cb() } // Unmap all ports const openPortsCopy = Object.assign([], self._openPorts) let numPorts = openPortsCopy.length if (numPorts === 0) return continueDestroy() openPortsCopy.forEach(function (openPortObj) { self.unmap(openPortObj, function () { // Ignore the errors numPorts-- if (numPorts === 0) { continueDestroy() } }) }) } _validateInput (publicPort, privatePort, cb) { let opts if (typeof publicPort === 'object') { // opts opts = publicPort if (typeof privatePort === 'function') cb = privatePort else if (!privatePort) cb = noop else throw new Error('invalid parameters') } else if (typeof publicPort === 'number' && typeof privatePort === 'number') { // number, number opts = {} opts.publicPort = publicPort opts.privatePort = privatePort if (!cb) cb = noop } else if (typeof publicPort === 'number') { // number opts = {} opts.publicPort = publicPort opts.privatePort = publicPort if (typeof privatePort === 'function') cb = privatePort else if (!privatePort) cb = noop else throw new Error('invalid parameters') } else { throw new Error('port was not specified') } if (opts.protocol && (typeof opts.protocol !== 'string' || !['UDP', 'TCP'].includes(opts.protocol.toUpperCase()))) { throw new Error('protocol is invalid') } else { opts.protocol = opts.protocol || null } opts.description = opts.description || this.description opts.ttl = opts.ttl || this.ttl opts.gateway = opts.gateway || this.gateway return { opts: opts, cb: cb } } _map (opts, cb) { const self = this function tryUPNP () { self._upnpMap(opts, function (err) { if (err) { let newErr if (self._pmpClient) newErr = new Error('NAT-PMP and UPnP port mapping failed') else newErr = new Error('UPnP port mapping failed') return cb(newErr) } cb() }) } // Try NAT-PMP if (this._pmpClient) { this._pmpMap(opts, function (err) { if (self._destroyed) return // Try UPnP if (err) return tryUPNP() cb() }) } else { // Try UPnP tryUPNP() } } externalIp (cb) { const self = this function tryUPNP () { self._upnpClient.externalIp(function (err, ip) { if (err) { let newErr if (self._pmpClient) newErr = new Error('NAT-PMP and UPnP get external ip failed') else newErr = new Error('UPnP get external failed') return cb(newErr) } cb(undefined, ip) }) } // Try NAT-PMP if (this._pmpClient) { this._pmpClient.externalIp(function (err, ip) { if (self._destroyed) return // Try UPnP if (err) return tryUPNP() cb(undefined, ip) }) } else { // Try UPnP tryUPNP() } } _unmap (opts, cb) { const self = this function tryUPNP () { self._upnpUnmap(opts, function (err) { if (err) { let newErr if (self._pmpClient) newErr = new Error('NAT-PMP and UPnP port mapping failed') else newErr = new Error('UPnP port mapping failed') return cb(newErr) } cb() }) } // Try NAT-PMP if (this._pmpClient) { this._pmpUnmap(opts, function (err) { if (self._destroyed) return // Try UPnP if (err) return tryUPNP() cb() }) } else { // Try UPnP tryUPNP() } } _upnpMap (opts, cb) { const self = this debug('Mapping public port %d to private port %d by %s using UPnP', opts.publicPort, opts.privatePort, opts.protocol) self._upnpClient.portMapping({ public: opts.publicPort, private: opts.privatePort, description: opts.description, protocol: opts.protocol, ttl: opts.ttl }, function (err) { if (err) { debug('Error mapping port %d:%d using UPnP:', opts.publicPort, opts.privatePort, err.message) return cb(err) } if (self.autoUpdate) { self._upnpIntervals[opts.publicPort + ':' + opts.privatePort + '-' + opts.protocol] = setInterval( self._upnpMap.bind(self, opts, () => {}), self._timeout ) } debug('Port %d:%d for protocol %s mapped on router using UPnP', opts.publicPort, opts.privatePort, opts.protocol) cb() }) } _pmpMap (opts, cb) { const self = this debug('Mapping public port %d to private port %d by %s using NAT-PMP', opts.publicPort, opts.privatePort, opts.protocol) // If we come from a timeouted (or error) request, we need to reconnect if (self._pmpClient && self._pmpClient.socket == null) { self._pmpClient = NatPMP.connect(self._pmpClient.gateway) } let timeouted = false const pmpTimeout = setTimeout(function () { timeouted = true self._pmpClient.close() const err = new Error('timeout') debug('Error mapping port %d:%d using NAT-PMP:', opts.publicPort, opts.privatePort, err.message) cb(err) }, 250) self._pmpClient.portMapping({ public: opts.publicPort, private: opts.privatePort, type: opts.protocol, ttl: opts.ttl }, function (err/* , info */) { if (timeouted) return clearTimeout(pmpTimeout) // Always close socket self._pmpClient.close() if (err) { debug('Error mapping port %d:%d using NAT-PMP:', opts.publicPort, opts.privatePort, err.message) return cb(err) } if (self.autoUpdate) { self._pmpIntervals[opts.publicPort + ':' + opts.privatePort + '-' + opts.protocol] = setInterval( self._pmpMap.bind(self, opts, () => {}), self._timeout ) } debug('Port %d:%d for protocol %s mapped on router using NAT-PMP', opts.publicPort, opts.privatePort, opts.protocol) cb() }) } _upnpUnmap (opts, cb) { const self = this debug('Unmapping public port %d to private port %d by %s using UPnP', opts.publicPort, opts.privatePort, opts.protocol) self._upnpClient.portUnmapping({ public: opts.publicPort, private: opts.privatePort, protocol: opts.protocol }, function (err) { if (err) { debug('Error unmapping port %d:%d using UPnP:', opts.publicPort, opts.privatePort, err.message) return cb(err) } // Clear intervals const key = opts.publicPort + ':' + opts.privatePort + '-' + opts.protocol if (self._upnpIntervals[key]) { clearInterval(self._upnpIntervals[key]) delete self._upnpIntervals[key] } debug('Port %d:%d for protocol %s unmapped on router using UPnP', opts.publicPort, opts.privatePort, opts.protocol) cb() }) } _pmpUnmap (opts, cb) { const self = this debug('Unmapping public port %d to private port %d by %s using NAT-PMP', opts.publicPort, opts.privatePort, opts.protocol) // If we come from a timeouted (or error) request, we need to reconnect if (self._pmpClient && self._pmpClient.socket == null) { self._pmpClient = NatPMP.connect(self._pmpClient.gateway) } let timeouted = false const pmpTimeout = setTimeout(function () { timeouted = true self._pmpClient.close() const err = new Error('timeout') debug('Error unmapping port %d:%d using NAT-PMP:', opts.publicPort, opts.privatePort, err.message) cb(err) }, 250) self._pmpClient.portUnmapping({ public: opts.publicPort, private: opts.privatePort, type: opts.protocol }, function (err) { if (timeouted) return clearTimeout(pmpTimeout) // Always close socket self._pmpClient.close() if (err) { debug('Error unmapping port %d:%d using NAT-PMP:', opts.publicPort, opts.privatePort, err.message) return cb(err) } // Clear intervals const key = opts.publicPort + ':' + opts.privatePort + '-' + opts.protocol if (self._pmpIntervals[key]) { clearInterval(self._pmpIntervals[key]) delete self._pmpIntervals[key] } debug('Port %d:%d for protocol %s unmapped on router using NAT-PMP', opts.publicPort, opts.privatePort, opts.protocol) cb() }) } _checkPort (publicPort, cb) { // TOOD: check port } } function noop () {} module.exports = NatAPI