summaryrefslogtreecommitdiff
path: root/attic/netconf-service/netconf-master.js
diff options
context:
space:
mode:
Diffstat (limited to 'attic/netconf-service/netconf-master.js')
-rw-r--r--attic/netconf-service/netconf-master.js624
1 files changed, 624 insertions, 0 deletions
diff --git a/attic/netconf-service/netconf-master.js b/attic/netconf-service/netconf-master.js
new file mode 100644
index 00000000..fb32bc60
--- /dev/null
+++ b/attic/netconf-service/netconf-master.js
@@ -0,0 +1,624 @@
+//
+// ZeroTier One - Global Peer to Peer Ethernet
+// Copyright (C) 2011-2014 ZeroTier Networks LLC
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// --
+//
+// ZeroTier may be used and distributed under the terms of the GPLv3, which
+// are available at: http://www.gnu.org/licenses/gpl-3.0.html
+//
+// If you would like to embed ZeroTier into a commercial application or
+// redistribute it in a modified binary form, please contact ZeroTier Networks
+// LLC. Start here: http://www.zerotier.com/
+//
+
+var config = require('./config.js');
+
+// Fields in netconf response dictionary
+var ZT_NETWORKCONFIG_DICT_KEY_ALLOWED_ETHERNET_TYPES = "et";
+var ZT_NETWORKCONFIG_DICT_KEY_NETWORK_ID = "nwid";
+var ZT_NETWORKCONFIG_DICT_KEY_TIMESTAMP = "ts";
+var ZT_NETWORKCONFIG_DICT_KEY_ISSUED_TO = "id";
+var ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_LIMIT = "ml";
+var ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_RATES = "mr";
+var ZT_NETWORKCONFIG_DICT_KEY_PRIVATE = "p";
+var ZT_NETWORKCONFIG_DICT_KEY_NAME = "n";
+var ZT_NETWORKCONFIG_DICT_KEY_DESC = "d";
+var ZT_NETWORKCONFIG_DICT_KEY_IPV4_STATIC = "v4s";
+var ZT_NETWORKCONFIG_DICT_KEY_IPV6_STATIC = "v6s";
+var ZT_NETWORKCONFIG_DICT_KEY_CERTIFICATE_OF_MEMBERSHIP = "com";
+var ZT_NETWORKCONFIG_DICT_KEY_ENABLE_BROADCAST = "eb";
+var ZT_NETWORKCONFIG_DICT_KEY_ALLOW_PASSIVE_BRIDGING = "pb";
+var ZT_NETWORKCONFIG_DICT_KEY_ACTIVE_BRIDGES = "ab";
+
+// Path to zerotier-idtool binary, invoked to enerate certificates of membership
+var ZEROTIER_IDTOOL = '/usr/local/bin/zerotier-idtool';
+
+// From Constants.hpp in node/
+var ZT_NETWORK_AUTOCONF_DELAY = 60000;
+var ZT_NETWORK_CERTIFICATE_TTL_WINDOW = (ZT_NETWORK_AUTOCONF_DELAY * 4);
+
+// Connect to redis, assuming database 0 and no auth (for now)
+var async = require('async');
+var redis = require('redis');
+var DB = redis.createClient();
+DB.on("error",function(err) { console.error('redis query error: '+err); });
+DB.select(config.redisDb,function() {});
+
+// Global variables -- these are initialized on startup or netconf-init message
+var netconfSigningIdentity = null; // identity of netconf master, with private key portion
+
+// spawn() function to launch sub-processes
+var spawn = require('child_process').spawn;
+
+// Returns true for fields that are "true" according to ZT redis schema
+function ztDbTrue(v) { return ((v === '1')||(v === 'true')||(v > 0)); }
+
+//
+// ZeroTier One Dictionary -- encoding-compatible with Dictionary in C++ code base
+//
+
+function Dictionary(fromStr)
+{
+ var self = this;
+
+ this.data = {};
+
+ this._esc = function(data) {
+ var es = '';
+ for(var i=0;i<data.length;++i) {
+ var c = data.charAt(i);
+ switch(c) {
+ case '\0': es += '\\0'; break;
+ case '\r': es += '\\r'; break;
+ case '\n': es += '\\n'; break;
+ case '\\': es += '\\\\'; break;
+ case '=': es += '\\='; break;
+ default: es += c; break;
+ }
+ }
+ return es;
+ };
+ this._unesc = function(s) {
+ if (typeof s !== 'string')
+ return '';
+ var uns = '';
+ var escapeState = false;
+ for(var i=0;i<s.length;++i) {
+ var c = s.charAt(i);
+ if (escapeState) {
+ escapeState = false;
+ switch(c) {
+ case '0': uns += '\0'; break;
+ case 'r': uns += '\r'; break;
+ case 'n': uns += '\n'; break;
+ default: uns += c; break;
+ }
+ } else{
+ if ((c !== '\r')&&(c !== '\n')&&(c !== '\0')) {
+ if (c === '\\')
+ escapeState = true;
+ else uns += c;
+ }
+ }
+ }
+ return uns;
+ };
+
+ this.toString = function() {
+ var str = '';
+
+ for(var key in self.data) {
+ str += self._esc(key);
+ str += '=';
+ var value = self.data[key];
+ if (value)
+ str += self._esc(value.toString());
+ str += '\n';
+ }
+
+ return str;
+ };
+
+ this.fromString = function(str) {
+ self.data = {};
+ if (typeof str !== 'string')
+ return self;
+
+ var lines = str.split('\n');
+ for(var l=0;l<lines.length;++l) {
+ var escapeState = false;
+ var eqAt = 0;
+ for(;eqAt<lines[l].length;++eqAt) {
+ var c = lines[l].charAt(eqAt);
+ if (escapeState)
+ escapeState = false;
+ else if (c === '\\')
+ escapeState = true;
+ else if (c === '=')
+ break;
+ }
+
+ var k = self._unesc(lines[l].substr(0,eqAt));
+ ++eqAt;
+ if ((k)&&(k.length > 0))
+ self.data[k] = self._unesc((eqAt < lines[l].length) ? lines[l].substr(eqAt) : '');
+ }
+
+ return self;
+ };
+
+ if ((typeof fromStr === 'string')&&(fromStr.length > 0))
+ self.fromString(fromStr);
+};
+
+//
+// Identity implementation using zerotier-idtool as subprocess to do actual crypto work
+//
+
+function Identity(idstr)
+{
+ var self = this;
+
+ this.str = '';
+ this.fields = [];
+
+ this.toString = function() {
+ return self.str;
+ };
+
+ this.address = function() {
+ return ((self.fields.length > 0) ? self.fields[0] : '0000000000');
+ };
+
+ this.fromString = function(str) {
+ self.str = '';
+ self.fields = [];
+ if (typeof str !== 'string')
+ return;
+ for(var i=0;i<str.length;++i) {
+ if ("0123456789abcdef:".indexOf(str.charAt(i)) < 0)
+ return; // invalid character in identity
+ }
+ var fields = str.split(':');
+ if ((fields.length < 3)||(fields[0].length !== 10)||(fields[1] !== '0'))
+ return;
+ self.str = str;
+ self.fields = fields;
+ };
+
+ this.isValid = function() {
+ return (! ((self.fields.length < 3)||(self.fields[0].length !== 10)||(self.fields[1] !== '0')) );
+ };
+
+ this.hasPrivate = function() {
+ return ((self.isValid())&&(self.fields.length >= 4));
+ };
+
+ if (typeof idstr === 'string')
+ self.fromString(idstr);
+};
+
+//
+// Invokes zerotier-idtool to generate certificates for private networks
+//
+
+function generateCertificateOfMembership(nwid,peerAddress,callback)
+{
+ // The first fields of these COM tuples come from
+ // CertificateOfMembership.hpp's enum of required
+ // certificate default fields.
+ var comTimestamp = '0,' + Date.now().toString(16) + ',' + ZT_NETWORK_CERTIFICATE_TTL_WINDOW.toString(16);
+ var comNwid = '1,' + nwid + ',0';
+ var comIssuedTo = '2,' + peerAddress + ',ffffffffffffffff';
+
+ var cert = '';
+ var certErr = '';
+
+ var idtool = spawn(ZEROTIER_IDTOOL,[ 'mkcom',netconfSigningIdentity,comTimestamp,comNwid,comIssuedTo ]);
+ idtool.stdout.on('data',function(data) {
+ cert += data;
+ });
+ idtool.stderr.on('data',function(data) {
+ certErr += data;
+ });
+ idtool.on('close',function(exitCode) {
+ if (certErr.length > 0)
+ console.error('zerotier-idtool stderr returned: '+certErr);
+ return callback((cert.length > 0) ? cert : null,exitCode);
+ });
+}
+
+//
+// Message handler for messages over ZeroTier One service bus
+//
+
+function doNetconfInit(message)
+{
+ netconfSigningIdentity = new Identity(message.data['netconfId']);
+ if (!netconfSigningIdentity.hasPrivate()) {
+ netconfSigningIdentity = null;
+ console.error('got invalid netconf signing identity in netconf-init');
+ } // else console.error('got netconf-init, running! id: '+netconfSigningIdentity.address());
+}
+
+function doNetconfRequest(message)
+{
+ if ((netconfSigningIdentity === null)||(!netconfSigningIdentity.hasPrivate())) {
+ console.error('got netconf-request before netconf-init, ignored');
+ return;
+ }
+
+ var peerId = new Identity(message.data['peerId']);
+ var nwid = message.data['nwid'];
+ var requestId = message.data['requestId'];
+ if ((!peerId)||(!peerId.isValid())||(!nwid)||(nwid.length !== 16)||(!requestId)) {
+ console.error('missing one or more required fields in netconf-request');
+ return;
+ }
+
+ var networkKey = 'zt1:network:'+nwid+':~';
+ var memberKey = 'zt1:network:'+nwid+':member:'+peerId.address()+':~';
+ var ipAssignmentsKey = 'zt1:network:'+nwid+':ipAssignments';
+
+ var network = null;
+ var member = null;
+
+ var authorized = false;
+
+ var v4NeedAssign = false;
+ var v6NeedAssign = false;
+ var v4Assignments = [];
+ var v6Assignments = [];
+ var ipAssignments = []; // both v4 and v6
+ var activeBridges = '';
+
+ async.series([function(next) {
+
+ // network lookup
+ DB.hgetall(networkKey,function(err,obj) {
+ if ((!err)&&(obj)&&(obj.id === nwid))
+ network = obj;
+ return next(null);
+ });
+
+ },function(next) {
+
+ // member lookup
+ if (!network)
+ return next(null);
+
+ DB.hgetall(memberKey,function(err,obj) {
+ if (err)
+ return next(err);
+
+ if (obj) {
+ // Update existing member record with new last seen time, etc.
+ member = obj;
+ authorized = ((!ztDbTrue(network['private'])) || ztDbTrue(member['authorized']));
+ var updatedFields = {
+ 'lastSeen': Date.now(),
+ 'authorized': authorized ? '1' : '0' // reset authorized to unhide in UI, since UI uses -1 to hide
+ };
+ if (!('identity' in member))
+ updatedFields['identity'] = peerId.toString();
+ if (!('firstSeen' in member))
+ updatedFields['firstSeen'] = Date.now();
+ if (message.data['from'])
+ updatedFields['lastAt'] = message.data['from'];
+ if (message.data['clientVersion'])
+ updatedFields['clientVersion'] = message.data['clientVersion'];
+ if (message.data['clientOs'])
+ updatedFields['clientOs'] = message.data['clientOs'];
+ DB.hmset(memberKey,updatedFields,next);
+ } else {
+ // Add member record to network for newly seen peer
+ authorized = ztDbTrue(network['private']) ? false : true; // public networks authorize everyone by default
+ var now = Date.now().toString();
+ member = {
+ 'id': peerId.address(),
+ 'nwid': nwid,
+ 'authorized': authorized ? '1' : '0',
+ 'identity': peerId.toString(),
+ 'firstSeen': now,
+ 'lastSeen': now
+ };
+ if (message.data['from'])
+ member['lastAt'] = message.data['from'];
+ if (message.data['clientVersion'])
+ member['clientVersion'] = message.data['clientVersion'];
+ if (message.data['clientOs'])
+ member['clientOs'] = message.data['clientOs'];
+ DB.hmset(memberKey,member,next);
+ }
+ });
+
+ },function(next) {
+
+ // Figure out which IP address auto-assignments we need to look up or make
+ if ((!network)||(!authorized))
+ return next(null);
+
+ v4NeedAssign = (network['v4AssignMode'] === 'zt');
+ v6NeedAssign = (network['v6AssignMode'] === 'zt');
+
+ var ipacsv = member['ipAssignments'];
+ if (ipacsv) {
+ var ipa = ipacsv.split(',');
+ for(var i=0;i<ipa.length;++i) {
+ if (ipa[i]) {
+ ipAssignments.push(ipa[i]);
+ if ((ipa[i].indexOf('.') > 0)&&(v4NeedAssign))
+ v4Assignments.push(ipa[i]);
+ else if ((ipa[i].indexOf(':') > 0)&&(v6NeedAssign))
+ v6Assignments.push(ipa[i]);
+ }
+ }
+ }
+
+ return next(null);
+
+ },function(next) {
+
+ // assign IPv4 if needed
+ if ((!network)||(!authorized)||(!v4NeedAssign)||(v4Assignments.length > 0))
+ return next(null);
+
+ var peerAddress = peerId.address();
+
+ var ipnetwork = 0;
+ var netmask = 0;
+ var netmaskBits = 0;
+ var v4pool = network['v4AssignPool']; // technically csv but only one netblock currently supported
+ if (v4pool) {
+ var v4poolSplit = v4pool.split('/');
+ if (v4poolSplit.length === 2) {
+ var networkSplit = v4poolSplit[0].split('.');
+ if (networkSplit.length === 4) {
+ ipnetwork |= (parseInt(networkSplit[0],10) << 24) & 0xff000000;
+ ipnetwork |= (parseInt(networkSplit[1],10) << 16) & 0x00ff0000;
+ ipnetwork |= (parseInt(networkSplit[2],10) << 8) & 0x0000ff00;
+ ipnetwork |= parseInt(networkSplit[3],10) & 0x000000ff;
+ netmaskBits = parseInt(v4poolSplit[1],10);
+ if (netmaskBits > 32)
+ netmaskBits = 32; // sanity check
+ for(var i=0;i<netmaskBits;++i)
+ netmask |= (0x80000000 >> i);
+ netmask &= 0xffffffff;
+ }
+ }
+ }
+ if ((ipnetwork === 0)||(netmask === 0xffffffff))
+ return next(null);
+ var invmask = netmask ^ 0xffffffff;
+
+ var abcd = 0;
+ var ipAssignmentAttempts = 0;
+
+ async.whilst(
+ function() { return ((v4Assignments.length === 0)&&(ipAssignmentAttempts < 1000)); },
+ function(next2) {
+ ++ipAssignmentAttempts;
+
+ // Generate or increment IP address source bits
+ if (abcd === 0) {
+ var a = parseInt(peerAddress.substr(2,2),16) & 0xff;
+ var b = parseInt(peerAddress.substr(4,2),16) & 0xff;
+ var c = parseInt(peerAddress.substr(6,2),16) & 0xff;
+ var d = parseInt(peerAddress.substr(8,2),16) & 0xff;
+ abcd = (a << 24) | (b << 16) | (c << 8) | d;
+ } else ++abcd;
+ if ((abcd & 0xff) === 0)
+ abcd |= 1;
+ abcd &= 0xffffffff;
+
+ // Derive an IP to test and generate assignment ip/bits string
+ var ip = (abcd & invmask) | (ipnetwork & netmask);
+ var assignment = ((ip >> 24) & 0xff).toString(10) + '.' + ((ip >> 16) & 0xff).toString(10) + '.' + ((ip >> 8) & 0xff).toString(10) + '.' + (ip & 0xff).toString(10) + '/' + netmaskBits.toString(10);
+
+ // Check :ipAssignments to see if this IP is already taken
+ DB.hget(ipAssignmentsKey,assignment,function(err,value) {
+ if (err)
+ return next2(err);
+
+ // IP is already taken, try again via async.whilst()
+ if ((value)&&(value !== peerAddress))
+ return next2(null); // if someone's already got this IP, keep looking
+
+ v4Assignments.push(assignment);
+ ipAssignments.push(assignment);
+
+ // Save assignment to :ipAssignments hash
+ DB.hset(ipAssignmentsKey,assignment,peerAddress,function(err) {
+ if (err)
+ return next2(err);
+
+ // Save updated CSV list of assignments to member record
+ var ipacsv = ipAssignments.join(',');
+ member['ipAssignments'] = ipacsv;
+ DB.hset(memberKey,'ipAssignments',ipacsv,next2);
+ });
+ });
+ },
+ next
+ );
+
+ },function(next) {
+
+ // assign IPv6 if needed -- TODO
+ if ((!network)||(!authorized)||(!v6NeedAssign)||(v6Assignments.length > 0))
+ return next(null);
+
+ return next(null);
+
+ },function(next) {
+
+ // Get active bridges
+ if ((!network)||(!authorized))
+ return next(null);
+
+ DB.keys('zt1:network:'+nwid+':member:*:~',function(err,keys) {
+ if (keys) {
+ async.eachSeries(keys,function(key,nextKey) {
+ DB.hgetall(key,function(err,abr) {
+ if ( (abr) &&
+ (abr.id) &&
+ (abr.id.length === 10) &&
+ ( (!ztDbTrue(network['private'])) || ztDbTrue(abr['authorized']) ) &&
+ (ztDbTrue(abr['activeBridge'])) ) {
+ if (activeBridges.length)
+ activeBridges += ',';
+ activeBridges += abr.id;
+ }
+ return nextKey(null);
+ });
+ },next);
+ } else return next(null);
+ });
+
+ }],function(err) {
+
+ if (err) {
+ console.error('error answering netconf-request for '+peerId.address()+': '+err);
+ return;
+ }
+
+ var response = new Dictionary();
+ response.data['peer'] = peerId.address();
+ response.data['nwid'] = nwid;
+ response.data['type'] = 'netconf-response';
+ response.data['requestId'] = requestId;
+
+ if ((network)&&(authorized)) {
+ var certificateOfMembership = null;
+ var privateNetwork = ztDbTrue(network['private']);
+
+ async.series([function(next) {
+
+ // Generate certificate of membership if necessary
+ if (privateNetwork) {
+ generateCertificateOfMembership(nwid,peerId.address(),function(cert,exitCode) {
+ if (cert) {
+ certificateOfMembership = cert;
+ return next(null);
+ } else return next(new Error('zerotier-idtool returned '+exitCode));
+ });
+ } else return next(null);
+
+ }],function(err) {
+
+ // Send response to parent process
+ if (err) {
+ console.error('unable to generate certificate for peer '+peerId.address()+' on network '+nwid+': '+err);
+ response.data['error'] = 'ACCESS_DENIED'; // unable to generate certificate
+ } else {
+ var netconf = new Dictionary();
+
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_ALLOWED_ETHERNET_TYPES] = network['etherTypes'];
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_NETWORK_ID] = nwid;
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_TIMESTAMP] = Date.now().toString(16);
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_ISSUED_TO] = peerId.address();
+ if (network['multicastLimit'])
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_LIMIT] = network['multicastLimit'];
+ if (network['multicastRates']) {
+ var ratesD = new Dictionary();
+ var ratesJ = JSON.parse(network['multicastRates']);
+ for(var k in ratesJ) {
+ if ((k)&&(ratesJ[k]))
+ ratesD.data[k] = ratesJ[k];
+ }
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_RATES] = ratesD.toString();
+ }
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_PRIVATE] = privateNetwork ? '1' : '0';
+ if (network['name'])
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_NAME] = network['name'];
+ if (network['desc'])
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_DESC] = network['desc'];
+ if ((v4NeedAssign)&&(v4Assignments.length > 0))
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_IPV4_STATIC] = v4Assignments.join(',');
+ if ((v6NeedAssign)&&(v6Assignments.length > 0))
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_IPV6_STATIC] = v6Assignments.join(',');
+ if (certificateOfMembership !== null)
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_CERTIFICATE_OF_MEMBERSHIP] = certificateOfMembership;
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_ENABLE_BROADCAST] = ztDbTrue(network['enableBroadcast']) ? '1' : '0';
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_ALLOW_PASSIVE_BRIDGING] = ztDbTrue(network['allowPassiveBridging']) ? '1' : '0';
+ if ((activeBridges)&&(activeBridges.length > 0))
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_ACTIVE_BRIDGES] = activeBridges; // comma-delimited list
+
+ response.data['netconf'] = netconf.toString();
+ }
+
+ process.stdout.write(response.toString()+'\n');
+
+ });
+
+ } else {
+
+ // Peer not authorized to join network or network not found (right now we always send ACCESS_DENIED)
+ response.data['error'] = 'ACCESS_DENIED';
+ process.stdout.write(response.toString()+'\n');
+
+ }
+
+ });
+}
+
+function handleMessage(dictStr)
+{
+ var message = new Dictionary(dictStr);
+ if (!('type' in message.data)) {
+ console.error('ignored message without request type field');
+ return;
+ } else if (message.data['type'] === 'netconf-init') {
+ doNetconfInit(message);
+ } else if (message.data['type'] === 'netconf-request') {
+ doNetconfRequest(message);
+ } else {
+ console.error('ignored unrecognized message type: '+message.data['type']);
+ }
+};
+
+//
+// Read stream of double-CR-terminated dictionaries from stdin until close/EOF
+//
+
+var stdinReadBuffer = '';
+
+process.stdin.on('readable',function() {
+ var chunk = process.stdin.read();
+ if (chunk)
+ stdinReadBuffer += chunk;
+ for(;;) {
+ var boundary = stdinReadBuffer.indexOf('\n\n');
+ if (boundary >= 0) {
+ handleMessage(stdinReadBuffer.substr(0,boundary + 1));
+ stdinReadBuffer = stdinReadBuffer.substr(boundary + 2);
+ } else break;
+ }
+});
+process.stdin.on('end',function() {
+ process.exit(0);
+});
+process.stdin.on('close',function() {
+ process.exit(0);
+});
+process.stdin.on('error',function() {
+ process.exit(0);
+});
+
+// Tell ZeroTier One that the service is running, solicit netconf-init
+process.stdout.write('type=ready\n\n');
+