├── .gitignore ├── History.md ├── README.md ├── index.js ├── package.json └── test ├── test-error-bad-code.js ├── test-error-two-clients.js ├── test-external.js ├── test-mapping-tcp.js ├── test-mapping-udp.js └── test-unmapping.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.0.0 / 2016-01-12 3 | ================== 4 | 5 | * index: update default TTL to 7200 (2 hours) 6 | * bumping to v1.0.0 major for better semver semantics 7 | 8 | 0.0.4 / 2016-01-12 9 | ================== 10 | 11 | * package: update "netroute" to v1.0.2 12 | * prevented a connection from being done twice (#6, @unRob) 13 | 14 | 0.0.1 / 2012-08-05 15 | ================== 16 | 17 | * Initial release 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-nat-pmp 2 | ============ 3 | ### Node.js implementation of the [NAT Port Mapping Protocol][wikipedia] 4 | 5 | This module offers an implementation of the [NAT-PMP][protocol] written in 6 | pure JavaScript. You can use this module to dynamically open and close arbitrary 7 | TCP and UDP ports against the network's internet gateway device. 8 | 9 | 10 | Installation 11 | ------------ 12 | 13 | Install with `npm`: 14 | 15 | ``` bash 16 | $ npm install nat-pmp 17 | ``` 18 | 19 | 20 | Examples 21 | -------- 22 | 23 | ``` js 24 | var natpmp = require('nat-pmp'); 25 | 26 | // create a "client" instance connecting to your local gateway 27 | var client = natpmp.connect('10.0.1.1'); 28 | 29 | 30 | // explicitly ask for the current external IP address 31 | client.externalIp(function (err, info) { 32 | if (err) throw err; 33 | console.log('Current external IP address: %s', info.ip.join('.')); 34 | }); 35 | 36 | 37 | // setup a new port mapping 38 | client.portMapping({ private: 22, public: 2222, ttl: 3600 }, function (err, info) { 39 | if (err) throw err; 40 | console.log(info); 41 | // { 42 | // type: 'tcp', 43 | // epoch: 8922109, 44 | // private: 22, 45 | // public: 2222, 46 | // ... 47 | // } 48 | }); 49 | ``` 50 | 51 | 52 | API 53 | --- 54 | 55 | 56 | 57 | 58 | License 59 | ------- 60 | 61 | (The MIT License) 62 | 63 | Copyright (c) 2012 Nathan Rajlich <nathan@tootallnate.net> 64 | 65 | Permission is hereby granted, free of charge, to any person obtaining 66 | a copy of this software and associated documentation files (the 67 | 'Software'), to deal in the Software without restriction, including 68 | without limitation the rights to use, copy, modify, merge, publish, 69 | distribute, sublicense, and/or sell copies of the Software, and to 70 | permit persons to whom the Software is furnished to do so, subject to 71 | the following conditions: 72 | 73 | The above copyright notice and this permission notice shall be 74 | included in all copies or substantial portions of the Software. 75 | 76 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 77 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 78 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 79 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 80 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 81 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 82 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 83 | 84 | 85 | [wikipedia]: http://wikipedia.org/wiki/NAT_Port_Mapping_Protocol 86 | [protocol]: http://tools.ietf.org/html/draft-cheshire-nat-pmp-03 87 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Node.js implementation of the NAT Port Mapping Protocol (a.k.a NAT-PMP). 3 | * 4 | * References: 5 | * http://miniupnp.free.fr/nat-pmp.html 6 | * http://wikipedia.org/wiki/NAT_Port_Mapping_Protocol 7 | * http://tools.ietf.org/html/draft-cheshire-nat-pmp-03 8 | */ 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | var dgram = require('dgram'); 15 | var assert = require('assert'); 16 | var debug = require('debug')('nat-pmp'); 17 | var inherits = require('util').inherits; 18 | var EventEmitter = require('events').EventEmitter; 19 | 20 | /** 21 | * The ports defined in draft-cheshire-nat-pmp-03 to send NAT-PMP requests to. 22 | */ 23 | 24 | exports.CLIENT_PORT = 5350; 25 | exports.SERVER_PORT = 5351; 26 | 27 | /** 28 | * The opcodes for client requests. 29 | */ 30 | 31 | exports.OP_EXTERNAL_IP = 0; 32 | exports.OP_MAP_UDP = 1; 33 | exports.OP_MAP_TCP = 2; 34 | exports.SERVER_DELTA = 128; 35 | 36 | /** 37 | * Map of result codes the gateway sends back when mapping a port. 38 | */ 39 | 40 | exports.RESULT_CODES = { 41 | 0: 'Success', 42 | 1: 'Unsupported Version', 43 | 2: 'Not Authorized/Refused (gateway may have NAT-PMP disabled)', 44 | 3: 'Network Failure (gateway may have not obtained a DHCP lease)', 45 | 4: 'Out of Resources (no ports left)', 46 | 5: 'Unsupported opcode' 47 | }; 48 | 49 | /** 50 | * Creates a Client instance. Familiar API to `net.connect()`. 51 | */ 52 | 53 | exports.connect = function (gateway) { 54 | var client = new Client(gateway); 55 | process.nextTick(function () { 56 | client.connect(); 57 | }); 58 | return client; 59 | }; 60 | 61 | /** 62 | * The NAT-PMP "Client" class. 63 | */ 64 | 65 | function Client (gateway) { 66 | if (!(this instanceof Client)) { 67 | return new Client(gateway); 68 | } 69 | debug('creating new Client instance for gateway', gateway); 70 | EventEmitter.call(this); 71 | 72 | this._queue = []; 73 | this.listening = false; 74 | this.gateway = gateway; 75 | 76 | this.socket = dgram.createSocket('udp4'); 77 | on('listening', this); 78 | on('message', this); 79 | on('close', this); 80 | on('error', this); 81 | } 82 | inherits(Client, EventEmitter); 83 | exports.Client = Client; 84 | 85 | /** 86 | * Binds to the nat-pmp Client port. 87 | */ 88 | 89 | Client.prototype.connect = function () { 90 | debug('Client#connect()'); 91 | if (this._connecting) { 92 | return false; 93 | } 94 | this._connecting = true; 95 | this.socket.bind(exports.CLIENT_PORT); 96 | }; 97 | 98 | /** 99 | * Queues a UDP request to be send to the gateway device. 100 | */ 101 | 102 | Client.prototype.request = function (op, obj, cb) { 103 | if (typeof obj === 'function') { 104 | cb = obj; 105 | obj = null; 106 | } 107 | debug('Client#request()', [op, obj, cb]); 108 | var buf; 109 | var size; 110 | var pos = 0; 111 | 112 | switch (op) { 113 | case exports.OP_MAP_UDP: 114 | case exports.OP_MAP_TCP: 115 | if (!obj) { 116 | throw new Error('mapping a port requires an "options" object'); 117 | } 118 | var internal = +(obj.private || obj.internal || 0); 119 | if (internal !== (internal | 0) || internal < 0) { 120 | throw new Error('the "private" port must be a whole integer >= 0'); 121 | } 122 | var external = +(obj.public || obj.external || 0); 123 | if (external !== (external | 0) || external < 0) { 124 | throw new Error('the "public" port must be a whole integer >= 0'); 125 | } 126 | var ttl = +(obj.ttl); 127 | if (ttl !== (ttl | 0)) { 128 | // The RECOMMENDED Port Mapping Lifetime is 7200 seconds (two hours). 129 | debug('using default "ttl" value of 7200'); 130 | ttl = 7200; 131 | } 132 | size = 12; 133 | buf = new Buffer(size); 134 | buf.writeUInt8(0, pos); pos++; // Vers = 0 135 | buf.writeUInt8(op, pos); pos++; // OP = x 136 | buf.writeUInt16BE(0, pos); pos+=2; // Reserved (MUST be zero) 137 | buf.writeUInt16BE(internal, pos); pos+=2; // Internal Port 138 | buf.writeUInt16BE(external, pos); pos+=2; // Requested External Port 139 | buf.writeUInt32BE(ttl, pos); pos+=4; // Requested Port Mapping Lifetime in Seconds 140 | break; 141 | case exports.OP_EXTERNAL_IP: 142 | default: 143 | if (op !== exports.OP_EXTERNAL_IP) { 144 | debug('WARN: invalid opcode given', op); 145 | } 146 | size = 2; 147 | buf = new Buffer(size); 148 | buf.writeUInt8(0, pos); pos++; // Vers = 0 149 | buf.writeUInt8(op, pos); pos++; // OP = x 150 | } 151 | assert.equal(pos, size, 'buffer not fully written!'); 152 | 153 | // queue out the request 154 | this._queue.push({ op: op, buf: buf, cb: cb }); 155 | this._next(); 156 | }; 157 | 158 | /** 159 | * Sends a request to the server for the current external IP address. 160 | */ 161 | 162 | Client.prototype.externalIp = function (cb) { 163 | this.request(exports.OP_EXTERNAL_IP, cb); 164 | }; 165 | 166 | /** 167 | * Sets up a new port mapping. 168 | */ 169 | 170 | Client.prototype.portMapping = function (opts, cb) { 171 | var opcode; 172 | switch (String(opts.type || 'tcp').toLowerCase()) { 173 | case 'tcp': 174 | opcode = exports.OP_MAP_TCP; 175 | break; 176 | case 'udp': 177 | opcode = exports.OP_MAP_UDP; 178 | break; 179 | default: 180 | throw new Error('"type" must be either "tcp" or "udp"'); 181 | } 182 | this.request(opcode, opts, cb); 183 | }; 184 | 185 | /** 186 | * To unmap a port you simply set the TTL to 0. 187 | */ 188 | 189 | Client.prototype.portUnmapping = function (opts, cb) { 190 | opts.ttl = 0; 191 | return this.portMapping(opts, cb); 192 | }; 193 | 194 | /** 195 | * Processes the next request if the socket is listening. 196 | */ 197 | 198 | Client.prototype._next = function () { 199 | debug('Client#_next()'); 200 | var req = this._queue[0]; 201 | if (!req) { 202 | debug('_next: nothing to process'); 203 | return; 204 | } 205 | if (!this.listening) { 206 | debug('_next: not "listening" yet, cannot send out request yet'); 207 | if (!this._connecting) { 208 | this.connect(); 209 | } 210 | return; 211 | } 212 | if (this._reqActive) { 213 | debug('_next: already an active request so waiting...'); 214 | return; 215 | } 216 | this._reqActive = true; 217 | this._req = req; 218 | 219 | var self = this; 220 | var buf = req.buf; 221 | var size = buf.length; 222 | var port = exports.SERVER_PORT; 223 | var gateway = this.gateway; 224 | 225 | debug('_next: sending request', buf, gateway); 226 | this.socket.send(buf, 0, size, port, gateway, function (err, bytes) { 227 | if (err) { 228 | self.onerror(err); 229 | } else if (bytes !== size) { 230 | self.onerror(new Error('Entire request buffer not sent. This should not happen!')); 231 | } 232 | }); 233 | }; 234 | 235 | /** 236 | * Closes the underlying socket. 237 | */ 238 | 239 | Client.prototype.close = function () { 240 | debug('Client#close()'); 241 | if (this.socket) { 242 | this.socket.close(); 243 | } 244 | }; 245 | 246 | /** 247 | * Called for the underlying socket's "listening" event. 248 | */ 249 | 250 | Client.prototype.onlistening = function () { 251 | debug('Client#onlistening()'); 252 | this.listening = true; 253 | this._connecting = false; 254 | this.emit('listening'); 255 | this._next(); 256 | }; 257 | 258 | /** 259 | * Called for the underlying socket's "message" event. 260 | */ 261 | 262 | Client.prototype.onmessage = function (msg, rinfo) { 263 | // Ignore message if we're not expecting it 264 | if (this._queue.length === 0) return; 265 | 266 | debug('Client#onmessage()', [msg, rinfo]); 267 | 268 | function cb (err) { 269 | debug('invoking "req" callback'); 270 | self._reqActive = false; 271 | if (err) { 272 | if (req.cb) { 273 | req.cb.call(self, err); 274 | } else { 275 | self.emit('error', err); 276 | } 277 | } else if (req.cb) { 278 | req.cb.apply(self, arguments); 279 | } 280 | self._next(); 281 | } 282 | 283 | var self = this; 284 | var req = this._queue[0]; 285 | var parsed = { msg: msg }; 286 | var pos = 0; 287 | parsed.vers = msg.readUInt8(pos); pos++; 288 | parsed.op = msg.readUInt8(pos); pos++; 289 | 290 | if (parsed.op - exports.SERVER_DELTA !== req.op) { 291 | debug('onmessage: WARN: got unexpected message opcode; ignoring', parsed.op); 292 | return; 293 | } 294 | 295 | // if we got here, then we're gonna invoke the request's callback, 296 | // so shift this request off of the queue. 297 | debug('removing "req" off of the queue'); 298 | this._queue.shift(); 299 | 300 | if (parsed.vers !== 0) { 301 | cb(new Error('"vers" must be 0. Got: ' + parsed.vers)); 302 | return; 303 | } 304 | 305 | // common fields 306 | parsed.resultCode = msg.readUInt16BE(pos); pos += 2; 307 | parsed.resultMessage = exports.RESULT_CODES[parsed.resultCode]; 308 | parsed.epoch = msg.readUInt32BE(pos); pos += 4; 309 | 310 | if (parsed.resultCode === 0) { 311 | // success response 312 | switch (req.op) { 313 | case exports.OP_EXTERNAL_IP: 314 | parsed.ip = []; 315 | parsed.ip.push(msg.readUInt8(pos)); pos++; 316 | parsed.ip.push(msg.readUInt8(pos)); pos++; 317 | parsed.ip.push(msg.readUInt8(pos)); pos++; 318 | parsed.ip.push(msg.readUInt8(pos)); pos++; 319 | break; 320 | case exports.OP_MAP_UDP: 321 | case exports.OP_MAP_TCP: 322 | parsed.private = parsed.internal = msg.readUInt16BE(pos); pos += 2; 323 | parsed.public = parsed.external = msg.readUInt16BE(pos); pos += 2; 324 | parsed.ttl = msg.readUInt32BE(pos); pos += 4; 325 | parsed.type = req.op === 1 ? 'udp' : 'tcp'; 326 | break; 327 | default: 328 | return cb(new Error('unknown OP code: ' + req.op)); 329 | } 330 | assert.equal(msg.length, pos); 331 | cb(null, parsed); 332 | } else { 333 | // error response 334 | var err = new Error(parsed.resultMessage); 335 | err.code = parsed.resultCode; 336 | cb(err); 337 | } 338 | }; 339 | 340 | /** 341 | * Called for the underlying socket's "close" event. 342 | */ 343 | 344 | Client.prototype.onclose = function () { 345 | debug('Client#onclose()'); 346 | this.listening = false; 347 | this.socket = null; 348 | }; 349 | 350 | /** 351 | * Called for the underlying socket's "error" event. 352 | */ 353 | 354 | Client.prototype.onerror = function (err) { 355 | debug('Client#onerror()', [err]); 356 | if (this._req && this._req.cb) { 357 | this._req.cb(err); 358 | } else { 359 | this.emit('error', err); 360 | } 361 | }; 362 | 363 | 364 | function on (name, target) { 365 | target.socket.on(name, function () { 366 | debug('on: socket event %j', name); 367 | return target['on' + name].apply(target, arguments); 368 | }); 369 | } 370 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nat-pmp", 3 | "version": "1.0.0", 4 | "description": "Node.js implementation of the NAT Port Mapping Protocol", 5 | "author": "Nathan Rajlich (http://tootallnate.net)", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/TooTallNate/node-nat-pmp.git" 13 | }, 14 | "keywords": [ 15 | "nat", 16 | "pmp", 17 | "nat-pmp", 18 | "port", 19 | "forward", 20 | "map", 21 | "mapping", 22 | "protocol" 23 | ], 24 | "license": "MIT", 25 | "dependencies": { 26 | "debug": "2" 27 | }, 28 | "devDependencies": { 29 | "netroute": "~1.0.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/test-error-bad-code.js: -------------------------------------------------------------------------------- 1 | 2 | var natpmp = require('../'); 3 | var assert = require('assert'); 4 | var netroute = require('netroute'); 5 | var gateway = netroute.getGateway(); 6 | 7 | var client = new natpmp.Client(gateway); 8 | 9 | client.request(17, function (err) { 10 | assert(err); 11 | console.log('Got error:', err); 12 | assert.equal(5, err.code); 13 | client.close(); 14 | }); 15 | -------------------------------------------------------------------------------- /test/test-error-two-clients.js: -------------------------------------------------------------------------------- 1 | 2 | var natpmp = require('../'); 3 | var assert = require('assert'); 4 | var netroute = require('netroute'); 5 | var gateway = netroute.getGateway(); 6 | 7 | var first = natpmp.connect(gateway); 8 | 9 | first.once('listening', function () { 10 | var second = natpmp.connect(gateway); 11 | 12 | second.externalIp(function (err) { 13 | assert(err); 14 | console.log('Got error:', err); 15 | assert(/EALREADY|EADDRINUSE/.test(err.code)); 16 | 17 | first.close(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/test-external.js: -------------------------------------------------------------------------------- 1 | 2 | var natpmp = require('../'); 3 | var netroute = require('netroute'); 4 | var gateway = netroute.getGateway(); 5 | 6 | var client = new natpmp.Client(gateway); 7 | 8 | client.externalIp(function (err, info) { 9 | if (err) throw err; 10 | console.log('External IP Address:', info.ip.join('.')); 11 | client.close(); 12 | }); 13 | -------------------------------------------------------------------------------- /test/test-mapping-tcp.js: -------------------------------------------------------------------------------- 1 | 2 | var natpmp = require('../'); 3 | var assert = require('assert'); 4 | var netroute = require('netroute'); 5 | var gateway = netroute.getGateway(); 6 | 7 | var client = new natpmp.Client(gateway); 8 | 9 | client.portMapping({ public: 3000, private: 3000 }, function (err, info) { 10 | if (err) throw err; 11 | assert.equal(3000, info.private); 12 | assert.equal('tcp', info.type); 13 | console.log('Port Mapping:', info); 14 | client.close(); 15 | }); 16 | -------------------------------------------------------------------------------- /test/test-mapping-udp.js: -------------------------------------------------------------------------------- 1 | 2 | var natpmp = require('../'); 3 | var netroute = require('netroute'); 4 | var gateway = netroute.getGateway(); 5 | 6 | var client = new natpmp.Client(gateway); 7 | 8 | client.portMapping({ public: 3000, private: 3000, type: 'udp' }, function (err, info) { 9 | if (err) throw err; 10 | console.log('Port Mapping:', info); 11 | client.close(); 12 | }); 13 | -------------------------------------------------------------------------------- /test/test-unmapping.js: -------------------------------------------------------------------------------- 1 | 2 | var natpmp = require('../'); 3 | 4 | var client = new natpmp.Client('10.0.1.1'); 5 | 6 | client.portUnmapping({ public: 3000, private: 3000 }, function (err, info) { 7 | if (err) throw err; 8 | console.log('Port Unmapping:', info); 9 | client.close(); 10 | }); 11 | --------------------------------------------------------------------------------