From b8d29cb6baa11574f241d673ede1acb1bc7ba5b2 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 3 Sep 2014 20:04:58 +0000 Subject: Some netconf-service renames and add initdb script for user use. --- netconf-service/index.js | 615 -------------------------------------- netconf-service/initdb.js | 75 +++++ netconf-service/netconf-master.js | 615 ++++++++++++++++++++++++++++++++++++++ netconf-service/netconf.service | 2 +- netconf-service/package.json | 2 +- 5 files changed, 692 insertions(+), 617 deletions(-) delete mode 100644 netconf-service/index.js create mode 100644 netconf-service/initdb.js create mode 100644 netconf-service/netconf-master.js diff --git a/netconf-service/index.js b/netconf-service/index.js deleted file mode 100644 index fd6aae55..00000000 --- a/netconf-service/index.js +++ /dev/null @@ -1,615 +0,0 @@ -// -// 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 . -// -// -- -// -// 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_PREFIX_BITS = "mpb"; -var ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_DEPTH = "md"; -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 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= 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 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> 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(); - //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_PREFIX_BITS] = 0; - //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_DEPTH] = 0; - //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_RATES] = ''; - 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'); - diff --git a/netconf-service/initdb.js b/netconf-service/initdb.js new file mode 100644 index 00000000..37650c9c --- /dev/null +++ b/netconf-service/initdb.js @@ -0,0 +1,75 @@ +/* + * Populates a new Redis database with data, which can be edited below. + */ + +var INIT_DATA = { + // Must be present in any database + "zt1": 1, + + /* The network ID here must be set to the ZeroTier address of your netconf + * master (the node where netconf-master will be running) plus an arbitrary + * 24-bit network ID. This will create the full 16-digit network ID of the + * network you will join. This must be in the object name and in the "id" + * field within the object itself. */ + "zt1:network:ffffffffff111111:~": { + "id": "ffffffffff111111", // netconf master ZT address + 24-bit ID + "name": "zerotier-testnet", // short name, no spaces or special chars + "desc": "Test Network", // description + "infrastructure": 0, // unused by netconf-master + "private": 0, // set to '1' to require member approval + "creationTime": 0, // unuxed by netconf-master + "owner": "", // unused by netconf-master + "etherTypes": "0800,0806", // hex ethernet frame types allowed + "enableBroadcast": 1, // set to '1' to enable ff:ff:ff:ff:ff:ff + "v4AssignMode": "zt", // 'zt' to assign, 'none' to let OS do it + "v4AssignPool": "28.0.0.0/7", // IPv4 net block / netmask bits + "v6AssignMode": "none" // 'zt' to assign, 'none' to let OS do it + } +}; + +var config = require('./config.js'); + +config.redisDb = 2; + +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() {}); + +DB.get("zt1",function(err,value) { + if ((value)&&(!err)) { + console.log("Redis database #"+config.redisDb+" appears to already contain data; flush it first!"); + return process.exit(0); + } + + async.eachSeries(Object.keys(INIT_DATA),function(key,next) { + var value = INIT_DATA[key]; + if (typeof value === 'object') { + console.log(key); + async.eachSeries(Object.keys(value),function(hkey,next2) { + var hvalue = value[hkey]; + if (hvalue === true) + hvalue = 1; + if (hvalue === false) + hvalue = 0; + if (typeof hvalue !== 'string') + hvalue = hvalue.toString(); + console.log('\t'+hkey+': '+hvalue); + DB.hset(key,hkey,hvalue,next2); + },next); + } else if ((typeof value !== 'undefined')&&(value !== null)) { + if (value === true) + value = 1; + if (value === false) + value = 0; + if (typeof value !== 'string') + value = value.toString(); + console.log(key+': '+value); + DB.set(key,value,next); + } else return next(null); + },function(err) { + console.log('Done!'); + return process.exit(0); + }); +}); diff --git a/netconf-service/netconf-master.js b/netconf-service/netconf-master.js new file mode 100644 index 00000000..fd6aae55 --- /dev/null +++ b/netconf-service/netconf-master.js @@ -0,0 +1,615 @@ +// +// 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 . +// +// -- +// +// 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_PREFIX_BITS = "mpb"; +var ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_DEPTH = "md"; +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 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= 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 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> 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(); + //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_PREFIX_BITS] = 0; + //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_DEPTH] = 0; + //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_RATES] = ''; + 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'); + diff --git a/netconf-service/netconf.service b/netconf-service/netconf.service index e799b369..df46e39f 100755 --- a/netconf-service/netconf.service +++ b/netconf-service/netconf.service @@ -10,4 +10,4 @@ if [ ! -d ./services.d/netconf-service ]; then fi cd services.d/netconf-service -exec node index.js +exec node netconf-master.js diff --git a/netconf-service/package.json b/netconf-service/package.json index 50cc17cd..19e06917 100644 --- a/netconf-service/package.json +++ b/netconf-service/package.json @@ -2,7 +2,7 @@ "name": "zt1-netconf-service", "version": "0.0.0", "description": "Worker in charge of issuing network configuration from ZeroTier One netconf masters", - "main": "index.js", + "main": "netconf-master.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, -- cgit v1.2.3