diff --git a/index.js b/index.js index b90fc87d..cda34041 100644 --- a/index.js +++ b/index.js @@ -110,6 +110,11 @@ function WebTorrent (opts) { } } + self._natTraversal = require('./lib/nat-traversal') // browser exclude + if (opts.enableNatTraversal === false) { + self._natTraversal.portMapping = null + } + if (typeof TCPPool === 'function') { self._tcpPool = new TCPPool(self) } else { @@ -132,7 +137,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, 'udp') + } + } }) // Ignore warning when there are > 10 torrents in the client @@ -432,6 +442,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) @@ -448,7 +464,12 @@ WebTorrent.prototype._onListening = function () { if (this._tcpPool) { // Sometimes server.address() returns `null` in Docker. 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, 'tcp') + } + } } this.emit('listening') diff --git a/lib/nat-traversal.js b/lib/nat-traversal.js new file mode 100644 index 00000000..5efa32c7 --- /dev/null +++ b/lib/nat-traversal.js @@ -0,0 +1,161 @@ +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, protocol, cb) { + var self = this + + debug('Mapping port %d for protocol %s on router using UPnP', port, protocol) + self._upnpClient.portMapping({ + public: port, + private: port, + description: 'WebTorrent', + protocol: protocol, + 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, protocol), self.timeout) + debug('Port %d for protocol %s mapped on router using UPnP', port, protocol) + if (typeof cb === 'function') cb() + }) + } + + self._pmpPortMapping = function (port, protocol, cb) { + var self = this + + debug('Mapping port %d for protocol %s on router using NAT-PMP', port, protocol) + self._pmpClient.portMapping({ + private: port, + public: port, + ttl: self.ttl, + type: protocol + }, 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, protocol), self.timeout) + debug('Port %d for protocol %s mapped on router using NAT-PMP', port, protocol) + 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 (obj) { + self._pmpPortMapping(obj.port, obj.protocol) + }) + }) +} + +NatTraversal.prototype.portMapping = function (port, protocol, cb) { + var self = this + if (self._destroyed) return typeof c === 'function' && cb() + if (typeof protocol === 'function') { + cb = protocol + protocol = 'tcp' + } + + self._openedPorts.push({port: port, protocol: protocol}) + + // Try UPnP first + self._upnpPortMapping(port, protocol, 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, protocol, cb) + } else if (typeof cb === 'function') { + cb() + } + }) +} + +NatTraversal.prototype.portUnMapping = function (port, cb) { + var self = this + if (self._destroyed) return typeof cb === 'function' && cb() + arrayRemove(self._openedPorts, self._openedPorts.findIndex(o => o.port === 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 (obj) { + self.portUnMapping(obj.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 8ea9a487..5d6f7948 100644 --- a/lib/server.js +++ b/lib/server.js @@ -9,6 +9,7 @@ var url = require('url') function Server (torrent, opts) { var server = http.createServer() + var natTraversal = require('./nat-traversal') if (!opts) opts = {} if (!opts.origin) opts.origin = '*' // allow all origins by default @@ -18,6 +19,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) { @@ -37,6 +39,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 (!cb) cb = function () {} if (closed) process.nextTick(cb) @@ -220,6 +226,10 @@ function Server (torrent, opts) { } } + function onListening () { + natTraversal.portMapping(server.address().port) + } + return server } diff --git a/lib/torrent.js b/lib/torrent.js index a7253f66..b22800b3 100644 --- a/lib/torrent.js +++ b/lib/torrent.js @@ -72,6 +72,7 @@ function Torrent (torrentId, client, opts) { this.announce = opts.announce this.urlList = opts.urlList + this.discoveryIntervalMs = opts.discoveryIntervalMs this.path = opts.path this.skipVerify = !!opts.skipVerify @@ -339,7 +340,8 @@ Torrent.prototype._onListening = function () { dht: !self.private && self.client.dht, tracker: trackerOpts, port: self.client.torrentPort, - userAgent: USER_AGENT + userAgent: USER_AGENT, + intervalMs: self.discoveryIntervalMs }) self.discovery.on('error', onError) diff --git a/package.json b/package.json index 022f096c..08688a39 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 }, @@ -28,7 +32,7 @@ "dependencies": { "addr-to-ip-port": "^1.4.2", "bitfield": "^2.0.0", - "bittorrent-dht": "^8.0.0", + "bittorrent-dht": "git+https://github.com/oleiba/bittorrent-dht.git", "bittorrent-protocol": "^2.1.5", "chunk-store-stream": "^2.0.2", "create-torrent": "^3.24.5", @@ -41,6 +45,9 @@ "memory-chunk-store": "^1.2.0", "mime": "^2.2.0", "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-numeric-range": "^0.0.2", "parse-torrent": "^6.0.0",