// // ZeroTier One - Network Virtualization Everywhere // Copyright (C) 2011-2015 ZeroTier, Inc. // // 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_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 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(); 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');