diff --git a/index.js b/index.js index c0662887..838cb912 100644 --- a/index.js +++ b/index.js @@ -106,6 +106,8 @@ function WebTorrent (opts) { if (global.WRTC && !self.tracker.wrtc) self.tracker.wrtc = global.WRTC } + self._natTraversal = require('./lib/nat-traversal') // browser exclude + if (typeof TCPPool === 'function') { self._tcpPool = new TCPPool(self) } else { @@ -128,7 +130,12 @@ function WebTorrent (opts) { self.dht.once('listening', function () { var address = self.dht.address() - if (address) self.dhtPort = address.port + if (address) { + self.dhtPort = address.port + if (self._natTraversal.portMapping) { + self._natTraversal.portMapping(self.dhtPort) + } + } }) // Ignore warning when there are > 10 torrents in the client @@ -425,6 +432,12 @@ WebTorrent.prototype._destroy = function (err, cb) { }) } + if (self._natTraversal.destroy) { + tasks.push(function (cb) { + self._natTraversal.destroy(cb) + }) + } + parallel(tasks, cb) if (err) self.emit('error', err) @@ -441,7 +454,12 @@ WebTorrent.prototype._onListening = function () { // Sometimes server.address() returns `null` in Docker. // WebTorrent issue: https://github.com/feross/bittorrent-swarm/pull/18 var address = this._tcpPool.server.address() - if (address) this.torrentPort = address.port + if (address) { + this.torrentPort = address.port + if (this._natTraversal.portMapping) { + this._natTraversal.portMapping(this.torrentPort) + } + } } this.emit('listening') diff --git a/lib/nat-traversal.js b/lib/nat-traversal.js new file mode 100644 index 00000000..e63d4040 --- /dev/null +++ b/lib/nat-traversal.js @@ -0,0 +1,156 @@ +var arrayRemove = require('unordered-array-remove') +var debug = require('debug')('webtorrent:nat-traversal') +var natUpnp = require('nat-upnp') // browser exclude +var natpmp = require('nat-pmp') // browser exclude +var network = require('network') // browser exclude + +// Use single instance +module.exports = new NatTraversal() + +function NatTraversal () { + var self = this + self._destroyed = false + + // The RECOMMENDED Port Mapping Lifetime is 7200 seconds (two hours). + self.ttl = 7200 + // Refresh the mapping 10 minutes before the end of its lifetime + self.timeout = (self.ttl - 600) * 1000 + self._openedPorts = [] + self._intervalsUpnp = {} + self._intervalsPmp = {} + + self._upnpPortMapping = function (port, cb) { + var self = this + + debug('Mapping port %d on router using UPnP', port) + self._upnpClient.portMapping({ + public: port, + private: port, + description: 'WebTorrent', + ttl: self.ttl + }, function (err) { + if (self._destroyed) return typeof cb === 'function' && cb() + if (err) { + return typeof cb === 'function' && cb(err) + } + self._intervalsUpnp[port] = setInterval(self._pmpPortMapping.bind(self, port), self.timeout) + debug('Port %d mapped on router using UPnP', port) + if (typeof cb === 'function') cb() + }) + } + + self._pmpPortMapping = function (port, cb) { + var self = this + + debug('Mapping port %d on router using NAT-PMP', port) + self._pmpClient.portMapping({ + private: port, + public: port, + ttl: self.ttl, + type: 'tcp' + }, function (err/* , info */) { + if (self._destroyed) return typeof cb === 'function' && cb() + if (err) { + debug('Error mapping port %d using NAT-PMP', port, err) + return typeof cb === 'function' && cb(err) + } + self._intervalsPmp[port] = setInterval(self._pmpPortMapping.bind(self, port), self.timeout) + debug('Port %d mapped on router using NAT-PMP', port) + if (typeof cb === 'function') cb() + }) + } + + debug('UPnP client creation') + self._upnpClient = natUpnp.createClient() + + // Lookup gateway IP + debug('Lookup gateway IP') + network.get_gateway_ip(function (err, ip) { + if (self._destroyed) return + if (err) { + return debug('Could not find gateway IP for NAT-PMP', err) + } + debug('NAT-PMP client creation', ip) + self._pmpClient = natpmp.connect(ip) + self._openedPorts.forEach(function (port) { + self._pmpPortMapping(port) + }) + }) +} + +NatTraversal.prototype.portMapping = function (port, cb) { + var self = this + if (self._destroyed) return typeof cd === 'function' && cb() + + self._openedPorts.push(port) + + // Try UPnP first + self._upnpPortMapping(port, function (err) { + if (self._destroyed) return typeof cb === 'function' && cb() + if (err) { + debug('UPnP port mapping failed on %d', port, err.message) + } + + // Then NAT-PMP + if (self._pmpClient) { + self._pmpPortMapping(port, cb) + } else if (typeof cb === 'function') { + cb() + } + }) +} + +NatTraversal.prototype.portUnMapping = function (port, cb) { + var self = this + if (self._destroyed) return typeof cd === 'function' && cb() + arrayRemove(self._openedPorts, self._openedPorts.indexOf(port)) + + // Clear intervals + if (self._intervalsUpnp[port]) { + clearInterval(self._intervalsUpnp[port]) + delete self._intervalsUpnp[port] + } + if (self._intervalsPmp[port]) { + clearInterval(self._intervalsPmp[port]) + delete self._intervalsPmp[port] + } + + debug('Unmapping port %d on router using UPnP', port) + self._upnpClient.portUnmapping({ + public: port + }, function (err) { + if (!err) debug('Port %d unmapped on router using UPnP', port) + if (self._pmpClient) { + debug('Unmapping port %d on router using NAT-PMP', port) + self._pmpClient.portUnmapping({ + private: port, + public: port + }, cb) + } else { + if (typeof cb === 'function') cb() + } + }) +} + +NatTraversal.prototype.destroy = function (cb) { + var self = this + if (self._destroyed) return cb() + + // Unmap all ports + self._openedPorts.forEach(function (port) { + self.portUnMapping(port) + }) + self._destroyed = true + + if (self._pmpClient) { + debug('Close pmp client') + self._pmpClient.close() + } + + // Waiting next tick to prevent breaking some sockets + process.nextTick(function () { + debug('Close UPnP client') + self._upnpClient.close() + cb() + }) +} diff --git a/lib/server.js b/lib/server.js index 911da59c..b957989c 100644 --- a/lib/server.js +++ b/lib/server.js @@ -10,6 +10,7 @@ var url = require('url') function Server (torrent, opts) { var server = http.createServer(opts) + var natTraversal = require('./nat-traversal') var sockets = [] var pendingReady = [] @@ -17,6 +18,7 @@ function Server (torrent, opts) { server.on('connection', onConnection) server.on('request', onRequest) + server.on('listening', onListening) var _close = server.close server.close = function (cb) { @@ -36,6 +38,10 @@ function Server (torrent, opts) { socket.destroy() }) + if (server.address()) { + natTraversal.portUnMapping(server.address().port) + } + // Only call `server.close` if user has not called it already if (closed) process.nextTick(cb) else server.close(cb) @@ -134,5 +140,9 @@ function Server (torrent, opts) { } } + function onListening () { + natTraversal.portMapping(server.address().port) + } + return server } diff --git a/package.json b/package.json index 320014be..80fb22da 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,16 @@ "url": "https://webtorrent.io" }, "browser": { + "./lib/nat-traversal.js": false, "./lib/server.js": false, "./lib/tcp-pool.js": false, "bittorrent-dht/client": false, "fs-chunk-store": "memory-chunk-store", "load-ip-set": false, + "nat-pmp": false, + "nat-upnp": false, "net": false, + "network": false, "os": false, "ut_pex": false }, @@ -41,6 +45,9 @@ "memory-chunk-store": "^1.2.0", "mime": "^1.3.4", "multistream": "^2.0.5", + "nat-pmp": "github:yciabaud/node-nat-pmp#patch-1", + "nat-upnp": "^1.0.4", + "network": "^0.3.2", "package-json-versionify": "^1.0.2", "parse-torrent": "^5.8.0", "pump": "^1.0.1",