node-stun
Version:
STUN (Session Traversal Unitilities for NAT) for Node.js
777 lines (684 loc) • 23.1 kB
JavaScript
'use strict';
var dns = require('dns');
var dgram = require('dgram');
var crypto = require('crypto');
var Const = require('./const');
var Utils = require('./utils');
var Message = require('./message');
// Client state
var State = Object.freeze({
// src dst chIp chPort breq
IDLE : 0, // ----- ---- ---- ------ ------
RESOLV : 1, // - - - - -
NBDaDp : 2, // _soc0 DaDp 0 0 _breq0
NBDaCp : 3, // _soc0 DaCp 0 0 _breq0
NBCaDp : 4, // _soc0 CaDp 0 0 _breq0
NBCaCp : 5, // _soc0 CaCp 0 0 _breq0
EFDiscov: 6, // _soc0 DaDp 1 1 _breq0
// _soc0 DaDp 1 0 _breq1
COMPLETE: 7
});
function Rtt() {
this._sum = 0;
this._num = 0;
this.init = function () { this._sum = 0; this._num = 0; };
this.addSample = function (rtt) { this._sum += rtt; this._num++; };
this.get = function () { return this._num?(this._sum/this._num):0; };
}
/**
* Client class.
* @class
* @see stun.createClient()
*/
function Client() {
this._domain; // FQDN
this._serv0; // Dotted decimal.
this._serv1; // Dotted decimal.
this._port0 = 3478;
this._port1; // Obtained via CHANGE-ADDRESS
this._local = { addr:'0.0.0.0', port:0 };
this._soc0;
this._soc1;
this._breq0; // Binding request 0 of type Message.
this._breq1; // Binding request 1 of type Message.
this._state = State.IDLE;
this._mapped = [
{ addr:0, port:0 }, // mapped addr from DaDp
{ addr:0, port:0 }, // mapped addr from DaCp
{ addr:0, port:0 }, // mapped addr from CaDp
{ addr:0, port:0 }]; // mapped addr from CaCp
// pd ad
// 0 0 : Independent
// 0 1 : Address dependent
// 1 0 : Port dependent (rare)
// 1 1 : Address & port dependent
// -1 * : pd check in progress
// * -1 : ad check in progress
this._ef = { ad: undefined, pd: undefined };
this._numSocs = 0;
this._cbOnComplete;
this._cbOnClosed;
this._intervalId;
this._retrans = 0; // num of retransmissions
this._elapsed = 0; // *100 msec
this._mode = Const.Mode.FULL;
this._rtt = new Rtt();
}
/**
* @private
* @static
*/
Client._isLocalAddr = function (addr, cb) {
var dummy = dgram.createSocket('udp4');
dummy.bind(0, addr, function () {
dummy.close();
cb(null, true);
});
dummy.on('error', function (err) {
if (err.code !== 'EADDRNOTAVAIL') {
return cb(err);
}
cb(null, false);
});
};
Client.prototype._discover = function () {
var self = this;
var Ctor = this.constructor;
// Create socket 0.
this._soc0 = dgram.createSocket("udp4");
this._soc0.on("listening", function () {
self._onListening();
});
this._soc0.on("message", function (msg, rinfo) {
self._onReceived(msg, rinfo);
});
this._soc0.on("close", function () {
self._onClosed();
});
// Start listening on the local port.
this._soc0.bind(0, this._local.addr, function () {
// Get assigned port name for this socket.
self._local.addr = self._soc0.address().address;
self._local.port = self._soc0.address().port;
self._breq0 = new Message();
self._breq0.init();
self._breq0.setType('breq');
self._breq0.setTransactionId(Ctor._randTransId());
/*
self._breq0.addAttribute('timestamp', {
'respDelay': 0,
'timestamp': (Date.now() & 0xffff)
});
*/
var msg = self._breq0.serialize();
self._soc0.send(msg, 0, msg.length, self._port0, self._serv0);
self._retrans = 0;
self._elapsed = 0;
self._intervalId = setInterval(function () {
self._onTick();
}, 100);
self._state = State.NBDaDp;
});
};
Client.prototype._onResolved = function (err, addresses) {
if (err) {
if (this._cbOnComplete != undefined) {
this._cbOnComplete(Const.Result.HOST_NOT_FOUND);
}
return;
}
this._serv0 = addresses[0];
this._discover();
};
Client.prototype._onListening = function () {
this._numSocs++;
//console.log("this._numSocs++: " + this._numSocs);
};
Client.prototype._onClosed = function () {
if (this._numSocs > 0) {
this._numSocs--;
//console.log("this._numSocs--: " + this._numSocs);
if (this._cbOnClosed != undefined && !this._numSocs) {
this._cbOnClosed();
}
}
};
Client.prototype._onTick = function () {
var sbuf;
// this._retrans this._elapsed
// 0 1( 1) == Math.min((1 << this._retrans), 16)
// 1 2( 3)
// 2 4( 7)
// 3 8(15)
// 4 16(31)
// 5 16(47)
// 6 16(63)
// 7 16(79)
// 8 16(95)
this._elapsed++;
if (this._elapsed >= Math.min((1 << this._retrans), 16)) {
// Retransmission timeout.
this._retrans++;
this._elapsed = 0;
if (this._state == State.NBDaDp ||
this._state == State.NBDaCp ||
this._state == State.NBCaDp ||
this._state == State.NBCaCp) {
if (this._retrans < 9) {
/*
this._breq0.addAttribute('timestamp', {
'respDelay': 0,
'timestamp': (Date.now() & 0xffff)
});
*/
sbuf = this._breq0.serialize();
var toAddr;
var toPort;
switch (this._state) {
case State.NBDaDp:
toAddr = this._serv0; toPort = this._port0;
break;
case State.NBDaCp:
toAddr = this._serv0; toPort = this._port1;
break;
case State.NBCaDp:
toAddr = this._serv1; toPort = this._port0;
break;
case State.NBCaCp:
toAddr = this._serv1; toPort = this._port1;
break;
}
this._soc0.send(sbuf, 0, sbuf.length, toPort, toAddr);
console.log(
"NB-Rtx0: len=" + sbuf.length +
" retrans=" + this._retrans +
" elapsed=" + this._elapsed +
" to=" + toAddr +
":" + toPort);
}
else {
clearInterval(this._intervalId);
var firstNB = (this._state == State.NBDaDp);
this._state = State.COMPLETE;
if (this._cbOnComplete != undefined) {
if (firstNB) {
this._cbOnComplete(Const.Result.UDP_BLOCKED);
}
else {
// First binding succeeded, then subsequent
// binding should work, but didn't.
this._cbOnComplete(Const.Result.NB_INCOMPLETE);
}
}
}
}
else if (this._state == State.EFDiscov) {
if (this._ef.ad == undefined) {
if (this._retrans < 9) {
sbuf = this._breq0.serialize();
this._soc1.send(sbuf, 0, sbuf.length, this._port0, this._serv0);
console.log("EF-Rtx0: retrans=" + this._retrans + " elapsed=" + this._elapsed);
}
else {
this._ef.ad = 1;
}
}
if (this._ef.pd == undefined) {
if (this._retrans < 9) {
sbuf = this._breq1.serialize();
this._soc1.send(sbuf, 0, sbuf.length, this._port0, this._serv0);
console.log("EF-Rtx1: retrans=" + this._retrans + " elapsed=" + this._elapsed);
}
else {
this._ef.pd = 1;
}
}
if (this._ef.ad != undefined && this._ef.pd != undefined) {
clearInterval(this._intervalId);
this._state = State.COMPLETE;
if (this._cbOnComplete != undefined) {
this._cbOnComplete(Const.Result.OK);
}
}
}
else {
console.log("Warning: unexpected timer event. Forgot to clear timer?");
clearInterval(this._intervalId);
}
}
};
Client.prototype._onReceived = function (msg, rinfo) {
var self = this;
var Ctor = this.constructor;
var bres = new Message();
var val;
var now = Date.now();
var sbuf;
void rinfo;
try {
bres.deserialize(msg);
} catch (e) {
console.log("Error: " + e.message);
return;
}
// We are only interested in binding response.
if (bres.getType() != 'bres') {
return;
}
if (this._state == State.NBDaDp) {
if (!Utils.bufferCompare(bres.getTransactionId(), this._breq0.getTransactionId())) {
return; // discard
}
clearInterval(this._intervalId);
// Get MAPPED-ADDRESS value.
val = bres.getAttribute('mappedAddr');
if (val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
this._mapped[0].addr = val.addr;
this._mapped[0].port = val.port;
// Check if the mappped address is a local or not (natted)
if (this._local.addr === '0.0.0.0') {
Ctor._isLocalAddr(this._mapped[0].addr, function (err, isLocal) {
if (!err) {
self._isNatted = !isLocal;
}
});
} else {
this._isNatted = (this._mapped[0].addr !== this._local.addr);
}
// Get CHANGED-ADDRESS value.
val = bres.getAttribute('changedAddr');
if (val == undefined) {
console.log("Error: CHANGED-ADDRESS not present");
return;
}
console.log('CHANGED: addr=%s:%d', val.addr, val.port);
this._serv1 = val.addr;
this._port1 = val.port;
// Calculate RTT if timestamp is attached.
val = bres.getAttribute('timestamp');
if (val != undefined) {
this._rtt.addSample(((now & 0xffff) - val.timestamp) - val.respDelay);
}
console.log("MAPPED0: addr=" + this._mapped[0].addr + ":" + this._mapped[0].port);
//console.log("CHANGED: addr=" + this._serv1 + ":" + this._port1);
// Start NBDaCp.
this._breq0.init();
this._breq0.setType('breq');
this._breq0.setTransactionId(Ctor._randTransId());
/*
this._breq0.addAttribute('timestamp', {
'respDelay': 0,
'timestamp': (now & 0xffff)
});
*/
sbuf = this._breq0.serialize();
this._soc0.send(sbuf, 0, sbuf.length, this._port1, this._serv0);
this._retrans = 0;
this._elapsed = 0;
this._intervalId = setInterval(function () {
self._onTick();
}, 100);
this._state = State.NBDaCp;
}
else if (this._state == State.NBDaCp) {
if (!Utils.bufferCompare(bres.getTransactionId(), this._breq0.getTransactionId())) {
return; // discard
}
clearInterval(this._intervalId);
// Get MAPPED-ADDRESS value.
val = bres.getAttribute('mappedAddr');
if (val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
this._mapped[1].addr = val.addr;
this._mapped[1].port = val.port;
// Calculate RTT if timestamp is attached.
val = bres.getAttribute('timestamp');
if (val != undefined) {
this._rtt.addSample(((now & 0xffff) - val.timestamp) - val.respDelay);
}
console.log("MAPPED1: addr=" + this._mapped[1].addr + ":" + this._mapped[1].port);
// Start NBCaDp.
this._breq0.init();
this._breq0.setType('breq');
this._breq0.setTransactionId(Ctor._randTransId());
/*
this._breq0.addAttribute('timestamp', {
'respDelay': 0,
'timestamp': (now & 0xffff)
});
*/
sbuf = this._breq0.serialize();
this._soc0.send(sbuf, 0, sbuf.length, this._port0, this._serv1);
this._retrans = 0;
this._elapsed = 0;
this._intervalId = setInterval(function () {
self._onTick();
}, 100);
this._state = State.NBCaDp;
}
else if (this._state == State.NBCaDp) {
if (!Utils.bufferCompare(bres.getTransactionId(), this._breq0.getTransactionId())) {
return; // discard
}
clearInterval(this._intervalId);
// Get MAPPED-ADDRESS value.
val = bres.getAttribute('mappedAddr');
if (val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
this._mapped[2].addr = val.addr;
this._mapped[2].port = val.port;
// Calculate RTT if timestamp is attached.
val = bres.getAttribute('timestamp');
if (val != undefined) {
this._rtt.addSample(((now & 0xffff) - val.timestamp) - val.respDelay);
}
console.log("MAPPED2: addr=" + this._mapped[2].addr + ":" + this._mapped[2].port);
// Start NBCaCp.
this._breq0.init();
this._breq0.setType('breq');
this._breq0.setTransactionId(Ctor._randTransId());
/*
this._breq0.addAttribute('timestamp', {
'respDelay': 0,
'timestamp': (now & 0xffff)
});
*/
sbuf = this._breq0.serialize();
this._soc0.send(sbuf, 0, sbuf.length, this._port1, this._serv1);
this._retrans = 0;
this._elapsed = 0;
this._intervalId = setInterval(function () {
self._onTick();
}, 100);
this._state = State.NBCaCp;
}
else if (this._state == State.NBCaCp) {
if (!Utils.bufferCompare(bres.getTransactionId(), this._breq0.getTransactionId())) {
return; // discard
}
clearInterval(this._intervalId);
// Get MAPPED-ADDRESS value.
val = bres.getAttribute('mappedAddr');
if (val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
this._mapped[3].addr = val.addr;
this._mapped[3].port = val.port;
// Calculate RTT if timestamp is attached.
val = bres.getAttribute('timestamp');
if (val != undefined) {
this._rtt.addSample(((now & 0xffff) - val.timestamp) - val.respDelay);
}
console.log("MAPPED3: addr=" + this._mapped[3].addr + ":" + this._mapped[3].port);
// Start NBDiscov.
this._ef.ad = undefined;
this._ef.pd = undefined;
// Create another socket (this._soc1) from which EFDiscov is performed).
this._soc1 = dgram.createSocket("udp4");
this._soc1.on("listening", function () {
self._onListening();
});
this._soc1.on("message", function (msg, rinfo) {
self._onReceived(msg, rinfo);
});
this._soc1.on("close", function () {
self._onClosed();
});
// Start listening on the local port.
this._soc1.bind(0, this._local.addr);
// changeIp=true,changePort=true from this._soc1
this._breq0.init();
this._breq0.setType('breq');
this._breq0.setTransactionId(Ctor._randTransId());
this._breq0.addAttribute('changeReq', {
'changeIp': true,
'changePort': true
});
sbuf = this._breq0.serialize();
this._soc1.send(sbuf, 0, sbuf.length, this._port0, this._serv0);
// changeIp=false,changePort=true from this._soc1
this._breq1 = new Message();
this._breq1.setType('breq');
this._breq1.setTransactionId(Ctor._randTransId());
this._breq1.addAttribute('changeReq', {
'changeIp': false,
'changePort': true
});
sbuf = this._breq1.serialize();
this._soc1.send(sbuf, 0, sbuf.length, this._port0, this._serv0);
this._retrans = 0;
this._elapsed = 0;
this._intervalId = setInterval(function () {
self._onTick();
}, 100);
this._state = State.EFDiscov;
}
else if (this._state == State.EFDiscov) {
var res = -1;
if (this._ef.ad == undefined) {
if (Utils.bufferCompare(bres.getTransactionId(), this._breq0.getTransactionId())) {
res = 0;
}
}
if (res < 0 && this._ef.pd == undefined) {
if (Utils.bufferCompare(bres.getTransactionId(), this._breq1.getTransactionId())) {
res = 1;
}
}
if (res < 0) {
return; // discard
}
if (res == 0) {
this._ef.ad = 0;
} else {
this._ef.pd = 0;
}
if (this._ef.ad !== undefined && this._ef.pd !== undefined) {
clearInterval(this._intervalId);
this._state = State.COMPLETE;
if (this._cbOnComplete) {
this._cbOnComplete(Const.Result.OK);
}
}
}
else {
return; // discard
}
};
/**
* @private
* @static
* @returns {Buffer} Returns a 16-random-bytes.
*/
Client._randTransId = function () {
var seed = process.pid.toString(16);
seed += Math.round(Math.random() * 0x100000000).toString(16);
seed += (new Date()).getTime().toString(16);
var md5 = crypto.createHash('md5');
md5.update(seed);
return md5.digest();
};
/**
* Sets local address. Use of this method is optional. If your
* local device has more then one interfaces, you can specify
* one of these interfaces form which STUN is performed.
* @param {string} addr Local IP address.
* @throws {Error} The address not available.
*/
Client.prototype.setLocalAddr = function (addr) {
this._local.addr = addr;
this._local.port = 0;
};
/**
* Sets STUN server address.
* @param {string} addr Domain name of the STUN server. Dotted
* decimal IP address can be used.
* @param {number} port Port number of the STUN server. If not
* defined, default port number 3478 will be used.
*/
Client.prototype.setServerAddr = function (addr, port) {
var d = addr.split('.');
if (d.length != 4 || (
isNaN(parseInt(d[0])) ||
isNaN(parseInt(d[1])) ||
isNaN(parseInt(d[2])) ||
isNaN(parseInt(d[3])))) {
this._domain = addr;
this._serv0 = undefined;
} else {
this._domain = undefined;
this._serv0 = addr;
}
if (port != undefined) {
this._port0 = port;
}
};
/**
* Starts NAT discovery.
* @param {object} [option]. Options.
* @param {boolean} [option.bindingOnly] Perform NAT binding only. Otheriwse
* perform full NAT discovery process.
* @param {function} cb Callback made when NAT discovery is complete.
* The callback function takes an argument, a result code of type {number}
* defined as stun.Result.
* @see stun.Result
* @throws {Error} STUN is already in progress.
* @throws {Error} STUN server address is not defined yet.
*/
Client.prototype.start = function (option, cb) {
if (typeof option !== 'object') {
cb = option;
option = {};
}
// Sanity check
if (this._state !== State.IDLE)
throw new Error("Not allowed in state " + this._state);
if (!this._domain && !this._serv0)
throw new Error("Address undefined");
this._cbOnComplete = cb;
this._mode = (option && option.bindingOnly)? Const.NB_ONLY:Const.Mode.FULL;
// Initialize.
this._rtt.init();
if (!this._serv0) {
dns.resolve4(this._domain, this._onResolved.bind(this));
this._state = State.RESOLV;
} else {
this._discover();
}
};
/**
* Closes STUN client.
* @param {function} callback Callback made when UDP sockets in use
* are all closed.
*/
Client.prototype.close = function (callback) {
this._cbOnClosed = callback;
if (this._soc0) {
this._soc0.close();
}
if (this._soc1) {
this._soc1.close();
}
};
/**
* Tells whether we are behind a NAT or not.
* @type boolean
*/
Client.prototype.isNatted = function () {
return this._isNatted;
};
/**
* Gets NAT binding type.
* @type string
* @see stun.Type
*/
Client.prototype.getNB = function () {
if (!this.isNatted()) {
return Const.Type.I;
}
if (this._mapped[1].addr && this._mapped[2].addr && this._mapped[3].addr) {
if (this._mapped[0].port == this._mapped[2].port) {
if (this._mapped[0].port == this._mapped[1].port) {
return Const.Type.I;
}
return Const.Type.PD;
}
if (this._mapped[0].port == this._mapped[1].port) {
return Const.Type.AD;
}
return Const.Type.APD;
}
return Const.Type.UNDEF;
};
/**
* Gets endpoint filter type.
* @type string
* @see stun.Type
*/
Client.prototype.getEF = function () {
if (this.isNatted() == undefined) {
return Const.Type.UNDEF;
}
if (!this.isNatted()) {
return Const.Type.I;
}
if (this._ef.ad == undefined) {
return Const.Type.UNDEF;
}
if (this._ef.pd == undefined) {
return Const.Type.UNDEF;
}
if (this._ef.ad == 0) {
if (this._ef.pd == 0) {
return Const.Type.I;
}
return Const.Type.PD;
}
if (this._ef.pd == 0) {
return Const.Type.AD;
}
return Const.Type.APD;
};
/**
* Gets name of NAT type.
* @type string
*/
Client.prototype.getNatType = function () {
var natted = this.isNatted();
var nb = this.getNB();
var ef = this.getEF();
if (natted == undefined) return "UDP blocked";
if (!natted) return "Open to internet";
if (nb == Const.Type.UNDEF || ef == Const.Type.UNDEF)
return "Natted (details not available)";
if (nb == Const.Type.I) {
// Cone.
if (ef == Const.Type.I) return "Full cone";
if (ef == Const.Type.PD) return "Port-only-restricted cone";
if (ef == Const.Type.AD) return "Address-restricted cone";
return "Port-restricted cone";
}
return "Symmetric";
};
/**
* Gets mapped address (IP address & port) returned by STUN server.
* @type object
*/
Client.prototype.getMappedAddr = function () {
return { address:this._mapped[0].addr, port:this._mapped[0].port };
};
/**
* Gets RTT (Round-Trip Time) in milliseconds measured during
* NAT binding discovery.
* @type number
*/
Client.prototype.getRtt = function () { return this._rtt.get(); };
module.exports = Client;