From 91f02d699e6a53eb414350bb4e841952e6d1f8b1 Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Mon, 25 May 2015 11:11:37 -0700
Subject: [PATCH 01/11] Right now only FreeBSD can use BSDEthernetTap, which
 will probably get renamed accordingly. NetBSD and OpenBSD do not support tap,
 only tun, so they will have to wait.

---
 service/OneService.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/service/OneService.cpp b/service/OneService.cpp
index 9a72c2408..9a024c2ce 100644
--- a/service/OneService.cpp
+++ b/service/OneService.cpp
@@ -95,7 +95,7 @@ namespace ZeroTier { typedef LinuxEthernetTap EthernetTap; }
 #include "../osdep/WindowsEthernetTap.hpp"
 namespace ZeroTier { typedef WindowsEthernetTap EthernetTap; }
 #endif
-#if defined(__BSD__) && (!defined(__APPLE__))
+#ifdef __FreeBSD__
 #include "../osdep/BSDEthernetTap.hpp"
 namespace ZeroTier { typedef BSDEthernetTap EthernetTap; }
 #endif

From af1d29cc6f02545c1c359c3adf7006041b07d699 Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Mon, 25 May 2015 11:54:32 -0700
Subject: [PATCH 02/11] Remove obsolete method.

---
 node/Address.hpp | 17 -----------------
 1 file changed, 17 deletions(-)

diff --git a/node/Address.hpp b/node/Address.hpp
index 7548d56e2..137e4f4f3 100644
--- a/node/Address.hpp
+++ b/node/Address.hpp
@@ -166,23 +166,6 @@ public:
 		return _a;
 	}
 
-	/**
-	 * Test whether this address is within a multicast propagation prefix
-	 *
-	 * Multicast propagation prefixes are (right-to-left a.k.a. LSB to MSB)
-	 * bit pattern prefixes of prefixBits bits that restrict which peers are
-	 * visited along a given multicast graph traversal path.
-	 *
-	 * @param prefix Prefix bit pattern (LSB to MSB)
-	 * @param prefixBits Number of bits in prefix bit pattern
-	 * @return True if address is within prefix
-	 */
-	inline bool withinMulticastPropagationPrefix(uint64_t prefix,unsigned int prefixBits) const
-		throw()
-	{
-		return ((_a & (0xffffffffffffffffULL >> (64 - prefixBits))) == prefix);
-	}
-
 	/**
 	 * @return Hexadecimal string
 	 */

From d29f2ce858297efa8b350439002541d14402f21f Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Mon, 25 May 2015 11:55:27 -0700
Subject: [PATCH 03/11] Clean up attic/

---
 attic/http-tunnel-proxy.js | 136 -------------------------------------
 1 file changed, 136 deletions(-)
 delete mode 100755 attic/http-tunnel-proxy.js

diff --git a/attic/http-tunnel-proxy.js b/attic/http-tunnel-proxy.js
deleted file mode 100755
index 5a47a7d37..000000000
--- a/attic/http-tunnel-proxy.js
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/env node
-
-// Note: this is unfinished and not currently used. Stashed in case we resurrect this idea.
-
-var UDP_PORT_START = 9994;
-var UDP_PORT_COUNT = 16384;
-var HTTP_PORT = 8080;
-var LONG_POLLING_TIMEOUT = 25000;
-
-var http = require('http');
-var dgram = require('dgram');
-
-// clients[token] = [ most recent HTTP activity, assigned UDP socket ]
-var clients = {};
-
-// GETs[token] = [ [ request, timestamp ], ... ]
-var GETs = {};
-
-// mappings[localPort+'/'+remoteIp+'/'+remotePort] = { ZT source: [ token ] }
-var mappings = {};
-
-// Array of available UDP sockets to assign randomly to clients
-var udpSocketPool = [];
-
-function onIncomingUdp(socket,message,remoteIp,remotePort)
-{
-	if (message.length > 16) {
-		var mappingKey = socket.localPort + '/' + remoteIp + '/' + remotePort;
-		var mapping = mappings[mappingKey];
-		if (mapping) {
-			var ztDestination = message.readUIntBE(8,5);
-			if (ztDestination in mapping) {
-			}
-		}
-	}
-}
-
-function onOutgoingUdp(token,socket,message,remoteIp,remotePort)
-{
-	if (message.length > 16) {
-		var ztDestination = message.readUIntBE(8,5);
-		var ztSource = (message.length >= 28) ? message.readUIntBE(13,5) ? 0;
-		if ((ztSource & 0xff00000000) == 0xff00000000) // fragment
-			ztSource = 0;
-
-		if ((ztDestination !== 0)&&((ztDestination & 0xff00000000) !== 0xff00000000)) {
-			socket.send(message,0,message.length,remotePort,remoteIp);
-		}
-	}
-}
-
-function doHousekeeping()
-{
-}
-
-for(var udpPort=UDP_PORT_START;udpPort<(UDP_PORT_START+UDP_PORT_COUNT)++udpPort) {
-	var socket = dgram.createSocket('udp4',function(message,rinfo) { onIncomingUdp(socket,message,rinfo.address,rinfo.port); });
-	socket.on('listening',function() {
-		console.log('Listening on '+socket.localPort);
-		udpSocketPool.push(socket);
-	}
-	socket.on('error',function() {
-		console.log('Error listening on '+socket.localPort);
-		socket.close();
-	})
-	socket.bind(udpPort);
-}
-
-server = http.createServer(function(request,response) {
-	console.log(request.socket.remoteAddress+" "+request.method+" "+request.url);
-
-	try {
-		// /<proxy token>/<ignored>/...
-		var urlSp = request.url.split('/');
-		if ((urlSp.length >= 3)&&(udpSocketPool.length > 0)) {
-			var token = urlSp[1]; // urlSp[0] == '' since URLs start with /
-
-			if (token.length >= 8) {
-				var client = clients[token];
-				if (!Array.isArray(client)) {
-					client = [ Date.now(),udpSocketPool[Math.floor(Math.random() * udpSocketPool.length)] ];
-					clients[token] = client;
-				} else client[0] = Date.now();
-
-				if (request.method === "GET") {
-
-					// /<proxy token>/<ignored> ... waits via old skool long polling
-
-				} else if (request.method === "POST") {
-
-					// /<proxy token>/<ignored>/<dest ip>/<dest port>
-					if (urlSp.length === 5) {
-						var ipSp = urlSp[3].split('.');
-						var port = parseInt(urlSp[4],10);
-						// Note: do not allow the use of this proxy to talk to privileged ports
-						if ((ipSp.length === 4)&&(port >= 1024)&&(port <= 0xffff)) {
-							var ip = [ parseInt(ipSp[0]),parseInt(ipSp[1]),parseInt(ipSp[2]),parseInt(ipSp[3]) ];
-							if (   (ip[0] > 0)
-							     &&(ip[0] < 240)
-							     &&(ip[0] !== 127)
-							     &&(ip[1] >= 0)
-							     &&(ip[1] <= 255)
-							     &&(ip[2] >= 0)
-							     &&(ip[2] <= 255)
-							     &&(ip[3] > 0)
-							     &&(ip[3] < 255) ) {
-								var postData = null;
-								request.on('data',function(chunk) {
-									postData = ((postData === null) ? chunk : Buffer.concat([ postData,chunk ]));
-								});
-								request.on('end',function() {
-									if (postData !== null)
-										onOutgoingUdp(token,client[1],postData,urlSp[3],port);
-									response.writeHead(200,{'Content-Length':0,'Pragma':'no-cache','Cache-Control':'no-cache'});
-									response.end();
-								});
-								return; // no 400 -- read from stream
-							} // else 400
-						} // else 400
-					} // else 400
-
-				} // else 400
-
-			} // else 400
-		} // else 400
-	} catch (e) {} // 400
-
-	response.writeHead(400,{'Content-Length':0,'Pragma':'no-cache','Cache-Control':'no-cache'});
-	response.end();
-	return;
-});
-
-setInterval(doHousekeeping,5000);
-
-server.setTimeout(120000);
-server.listen(HTTP_PORT);

From d8ad555b9ad7d70f6733f3d1e2ef795c752f45a4 Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Mon, 25 May 2015 13:20:10 -0700
Subject: [PATCH 04/11] Go ahead and add flags and invFlags to the Rule table.

---
 controller/SqliteNetworkController.cpp | 27 ++++++++++++++++++++++----
 controller/schema.sql                  |  2 ++
 controller/schema.sql.c                |  2 ++
 3 files changed, 27 insertions(+), 4 deletions(-)

diff --git a/controller/SqliteNetworkController.cpp b/controller/SqliteNetworkController.cpp
index 25a491dc1..a1905221f 100644
--- a/controller/SqliteNetworkController.cpp
+++ b/controller/SqliteNetworkController.cpp
@@ -167,7 +167,7 @@ SqliteNetworkController::SqliteNetworkController(const char *dbPath) :
 			||(sqlite3_prepare_v2(_db,"SELECT n.id FROM Member AS m,Node AS n WHERE m.networkId = ? AND n.id = m.nodeId ORDER BY n.id ASC",-1,&_sListNetworkMembers,(const char **)0) != SQLITE_OK)
 			||(sqlite3_prepare_v2(_db,"SELECT m.authorized,m.activeBridge,n.identity,n.lastAt,n.lastSeen,n.firstSeen FROM Member AS m,Node AS n WHERE m.networkId = ? AND m.nodeId = ?",-1,&_sGetMember2,(const char **)0) != SQLITE_OK)
 			||(sqlite3_prepare_v2(_db,"SELECT ipNetwork,ipNetmaskBits,ipVersion FROM IpAssignmentPool WHERE networkId = ? ORDER BY ipNetwork ASC",-1,&_sGetIpAssignmentPools2,(const char **)0) != SQLITE_OK)
-			||(sqlite3_prepare_v2(_db,"SELECT ruleId,nodeId,vlanId,vlanPcp,etherType,macSource,macDest,ipSource,ipDest,ipTos,ipProtocol,ipSourcePort,ipDestPort,\"action\" FROM Rule WHERE networkId = ? ORDER BY ruleId ASC",-1,&_sListRules,(const char **)0) != SQLITE_OK)
+			||(sqlite3_prepare_v2(_db,"SELECT ruleId,nodeId,vlanId,vlanPcp,etherType,macSource,macDest,ipSource,ipDest,ipTos,ipProtocol,ipSourcePort,ipDestPort,\"flags\",invFlags,\"action\" FROM Rule WHERE networkId = ? ORDER BY ruleId ASC",-1,&_sListRules,(const char **)0) != SQLITE_OK)
 			||(sqlite3_prepare_v2(_db,"INSERT INTO Rule (networkId,ruleId,nodeId,vlanId,vlanPcP,etherType,macSource,macDest,ipSource,ipDest,ipTos,ipProtocol,ipSourcePort,ipDestPort,\"action\") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",-1,&_sCreateRule,(const char **)0) != SQLITE_OK)
 			||(sqlite3_prepare_v2(_db,"INSERT INTO Network (id,name,creationTime,revision) VALUES (?,?,?,1)",-1,&_sCreateNetwork,(const char **)0) != SQLITE_OK)
 			||(sqlite3_prepare_v2(_db,"SELECT revision FROM Network WHERE id = ?",-1,&_sGetNetworkRevision,(const char **)0) != SQLITE_OK)
@@ -868,6 +868,8 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
 												const json_int_t *ipProtocol;
 												const json_int_t *ipSourcePort;
 												const json_int_t *ipDestPort;
+												const json_int_t *flags;
+												const json_int_t *invFlags;
 												const char *action;
 											} rule;
 											memset(&rule,0,sizeof(rule));
@@ -899,6 +901,10 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
 													rule.ipSourcePort = &(rj->u.object.values[rk].value->u.integer);
 												else if ((!strcmp(rj->u.object.values[rk].name,"ipDestPort"))&&(rj->u.object.values[rk].value->type == json_integer))
 													rule.ipDestPort = &(rj->u.object.values[rk].value->u.integer);
+												else if ((!strcmp(rj->u.object.values[rk].name,"flags"))&&(rj->u.object.values[rk].value->type == json_integer))
+													rule.flags = &(rj->u.object.values[rk].value->u.integer);
+												else if ((!strcmp(rj->u.object.values[rk].name,"invFlags"))&&(rj->u.object.values[rk].value->type == json_integer))
+													rule.invFlags = &(rj->u.object.values[rk].value->u.integer);
 												else if ((!strcmp(rj->u.object.values[rk].name,"action"))&&(rj->u.object.values[rk].value->type == json_string))
 													rule.action = rj->u.object.values[rk].value->u.string.ptr;
 											}
@@ -908,7 +914,9 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
 												sqlite3_reset(_sCreateRule);
 												sqlite3_bind_text(_sCreateRule,1,nwids,16,SQLITE_STATIC);
 												sqlite3_bind_int64(_sCreateRule,2,*rule.ruleId);
-												for(int i=3;i<=14;++i)
+
+												// Optional values: null by default
+												for(int i=3;i<=16;++i)
 													sqlite3_bind_null(_sCreateRule,i);
 												if ((rule.nodeId)&&(strlen(rule.nodeId) == 10)) sqlite3_bind_text(_sCreateRule,3,rule.nodeId,10,SQLITE_STATIC);
 												if (rule.vlanId) sqlite3_bind_int(_sCreateRule,4,(int)*rule.vlanId);
@@ -930,7 +938,10 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
 												if (rule.ipProtocol) sqlite3_bind_int(_sCreateRule,12,(int)*rule.ipProtocol);
 												if (rule.ipSourcePort) sqlite3_bind_int(_sCreateRule,13,(int)*rule.ipSourcePort & (int)0xffff);
 												if (rule.ipDestPort) sqlite3_bind_int(_sCreateRule,14,(int)*rule.ipDestPort & (int)0xffff);
-												sqlite3_bind_text(_sCreateRule,15,rule.action,-1,SQLITE_STATIC);
+												if (rule.flags) sqlite3_bind_int64(_sCreateRule,15,(int64_t)*rule.flags);
+												if (rule.invFlags) sqlite3_bind_int64(_sCreateRule,16,(int64_t)*rule.invFlags);
+
+												sqlite3_bind_text(_sCreateRule,17,rule.action,-1,SQLITE_STATIC);
 												sqlite3_step(_sCreateRule);
 											}
 										}
@@ -1262,8 +1273,16 @@ unsigned int SqliteNetworkController::_doCPGet(
 							Utils::snprintf(json,sizeof(json),"\t\t\"ipDestPort\": %d,\n",sqlite3_column_int(_sListRules,12));
 							responseBody.append(json);
 						}
+						if (sqlite3_column_type(_sListRules,13) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"flags\": %lu,\n",(unsigned long)sqlite3_column_int64(_sListRules,13));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,14) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"invFlags\": %lu,\n",(unsigned long)sqlite3_column_int64(_sListRules,14));
+							responseBody.append(json);
+						}
 						responseBody.append("\t\t\"action\": \"");
-						responseBody.append(_jsonEscape((const char *)sqlite3_column_text(_sListRules,13)));
+						responseBody.append(_jsonEscape( (sqlite3_column_type(_sListRules,15) == SQLITE_NULL) ? "drop" : (const char *)sqlite3_column_text(_sListRules,15) ));
 						responseBody.append("\"\n\t}");
 					}
 
diff --git a/controller/schema.sql b/controller/schema.sql
index 8d93a4dcc..b5646ee90 100644
--- a/controller/schema.sql
+++ b/controller/schema.sql
@@ -96,6 +96,8 @@ CREATE TABLE Rule (
   ipProtocol integer,
   ipSourcePort integer,
   ipDestPort integer,
+  flags integer,
+  invFlags integer,
   "action" varchar(4096) NOT NULL DEFAULT('accept')
 );
 
diff --git a/controller/schema.sql.c b/controller/schema.sql.c
index 01b7c9493..1384b9008 100644
--- a/controller/schema.sql.c
+++ b/controller/schema.sql.c
@@ -97,6 +97,8 @@
 "  ipProtocol integer,\n"\
 "  ipSourcePort integer,\n"\
 "  ipDestPort integer,\n"\
+"  flags integer,\n"\
+"  invFlags integer,\n"\
 "  \"action\" varchar(4096) NOT NULL DEFAULT('accept')\n"\
 ");\n"\
 "\n"\

From 5e3c6d9e0d89b8284cf60978b658dab12d2814d1 Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Mon, 25 May 2015 14:21:05 -0700
Subject: [PATCH 05/11] Some nodeJS work, and apply fix from GitHub issue #166
 plus a small optimization to avoid repeated calls to _allMulticastGroups().

---
 node/Network.cpp           | 40 ++++++++++++++++++++------------------
 node/Network.hpp           |  7 ++++++-
 nodejs-zt1-client/index.js | 23 ++++++++++++++++++++++
 3 files changed, 50 insertions(+), 20 deletions(-)

diff --git a/node/Network.cpp b/node/Network.cpp
index ebff1a5dc..deb05d1cc 100644
--- a/node/Network.cpp
+++ b/node/Network.cpp
@@ -139,21 +139,6 @@ Network::~Network()
 	}
 }
 
-std::vector<MulticastGroup> Network::allMulticastGroups() const
-{
-	Mutex::Lock _l(_lock);
-	std::vector<MulticastGroup> mgs(_myMulticastGroups);
-	std::vector<MulticastGroup>::iterator oldend(mgs.end());
-	for(std::map< MulticastGroup,uint64_t >::const_iterator i(_multicastGroupsBehindMe.begin());i!=_multicastGroupsBehindMe.end();++i) {
-		if (!std::binary_search(mgs.begin(),oldend,i->first))
-			mgs.push_back(i->first);
-	}
-	if ((_config)&&(_config->enableBroadcast()))
-		mgs.push_back(Network::BROADCAST);
-	std::sort(mgs.begin(),mgs.end());
-	return mgs;
-}
-
 bool Network::subscribedToMulticastGroup(const MulticastGroup &mg,bool includeBridgedGroups) const
 {
 	Mutex::Lock _l(_lock);
@@ -510,6 +495,22 @@ bool Network::_isAllowed(const Address &peer) const
 	return false; // default position on any failure
 }
 
+std::vector<MulticastGroup> Network::_allMulticastGroups() const
+{
+	// Assumes _lock is locked
+	std::vector<MulticastGroup> mgs(_myMulticastGroups);
+	std::vector<MulticastGroup>::iterator oldend(mgs.end());
+	for(std::map< MulticastGroup,uint64_t >::const_iterator i(_multicastGroupsBehindMe.begin());i!=_multicastGroupsBehindMe.end();++i) {
+		if (!std::binary_search(mgs.begin(),oldend,i->first))
+			mgs.push_back(i->first);
+	}
+	if ((_config)&&(_config->enableBroadcast()))
+		mgs.push_back(Network::BROADCAST);
+	std::sort(mgs.begin(),mgs.end());
+	std::unique(mgs.begin(),mgs.end());
+	return mgs;
+}
+
 // Used in Network::_announceMulticastGroups()
 class _AnnounceMulticastGroupsToPeersWithActiveDirectPaths
 {
@@ -518,7 +519,8 @@ public:
 		RR(renv),
 		_now(renv->node->now()),
 		_network(nw),
-		_supernodeAddresses(renv->topology->supernodeAddresses())
+		_supernodeAddresses(renv->topology->supernodeAddresses()),
+		_allMulticastGroups(nw->_allMulticastGroups())
 	{}
 
 	inline void operator()(Topology &t,const SharedPtr<Peer> &p)
@@ -526,9 +528,8 @@ public:
 		if ( ( (p->hasActiveDirectPath(_now)) && (_network->_isAllowed(p->address())) ) || (std::find(_supernodeAddresses.begin(),_supernodeAddresses.end(),p->address()) != _supernodeAddresses.end()) ) {
 			Packet outp(p->address(),RR->identity.address(),Packet::VERB_MULTICAST_LIKE);
 
-			std::vector<MulticastGroup> mgs(_network->allMulticastGroups());
-			for(std::vector<MulticastGroup>::iterator mg(mgs.begin());mg!=mgs.end();++mg) {
-				if ((outp.size() + 18) > ZT_UDP_DEFAULT_PAYLOAD_MTU) {
+			for(std::vector<MulticastGroup>::iterator mg(_allMulticastGroups.begin());mg!=_allMulticastGroups.end();++mg) {
+				if ((outp.size() + 18) >= ZT_UDP_DEFAULT_PAYLOAD_MTU) {
 					outp.armor(p->key(),true);
 					p->send(RR,outp.data(),outp.size(),_now);
 					outp.reset(p->address(),RR->identity.address(),Packet::VERB_MULTICAST_LIKE);
@@ -552,6 +553,7 @@ private:
 	uint64_t _now;
 	Network *_network;
 	std::vector<Address> _supernodeAddresses;
+	std::vector<MulticastGroup> _allMulticastGroups;
 };
 
 void Network::_announceMulticastGroups()
diff --git a/node/Network.hpp b/node/Network.hpp
index f99ea525b..7976d9018 100644
--- a/node/Network.hpp
+++ b/node/Network.hpp
@@ -106,7 +106,11 @@ public:
 	/**
 	 * @return All multicast groups including learned groups that are behind any bridges we're attached to
 	 */
-	std::vector<MulticastGroup> allMulticastGroups() const;
+	inline std::vector<MulticastGroup> allMulticastGroups() const
+	{
+		Mutex::Lock _l(_lock);
+		return _allMulticastGroups();
+	}
 
 	/**
 	 * @param mg Multicast group
@@ -356,6 +360,7 @@ private:
 	void _externalConfig(ZT1_VirtualNetworkConfig *ec) const; // assumes _lock is locked
 	bool _isAllowed(const Address &peer) const;
 	void _announceMulticastGroups();
+	std::vector<MulticastGroup> _allMulticastGroups() const;
 
 	const RuntimeEnvironment *RR;
 	uint64_t _id;
diff --git a/nodejs-zt1-client/index.js b/nodejs-zt1-client/index.js
index f61e3b541..55f7fb24e 100644
--- a/nodejs-zt1-client/index.js
+++ b/nodejs-zt1-client/index.js
@@ -8,6 +8,25 @@ function ZT1Client(url,authToken)
 	this.authToken = authToken;
 }
 
+// Generate new ZeroTier identity -- mostly for testing
+ZT1Client.prototype.newIdentity = function(callback)
+{
+	request({
+		url: this.url + 'newIdentity',
+		method: 'GET',
+		json: false,
+		headers: {
+			'X-ZT1-Auth': this.authToken
+		}
+	},function(error,response,body) {
+		if (error)
+			return callback(error,null);
+		if (response.statusCode === 200)
+			return callback(null,body);
+		return callback(new Error('server responded with error: '+response.statusCode),'');
+	});
+}
+
 ZT1Client.prototype._jsonGet = function(getPath,callback)
 {
 	request({
@@ -134,4 +153,8 @@ ZT1Client.prototype.saveControllerNetwork = function(network,callback)
 	});
 };
 
+ZT1Client.prototype.getControllerNetworkMember = function(nwid,address,callback) {
+	this._jsonGet('controller/network/' + nwid + '/member/' + address,callback);
+};
+
 exports.ZT1Client = ZT1Client;

From ba7809367a8ef1c628bc927e2d4148b6dc43fa46 Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Tue, 26 May 2015 09:01:58 -0700
Subject: [PATCH 06/11] JS stuff reorg.

---
 {nodejs-zt1-client => js/zt1-api-client}/index.js     | 0
 {nodejs-zt1-client => js/zt1-api-client}/package.json | 0
 {nodejs-zt1-client => js/zt1-api-client}/test.js      | 0
 3 files changed, 0 insertions(+), 0 deletions(-)
 rename {nodejs-zt1-client => js/zt1-api-client}/index.js (100%)
 rename {nodejs-zt1-client => js/zt1-api-client}/package.json (100%)
 rename {nodejs-zt1-client => js/zt1-api-client}/test.js (100%)

diff --git a/nodejs-zt1-client/index.js b/js/zt1-api-client/index.js
similarity index 100%
rename from nodejs-zt1-client/index.js
rename to js/zt1-api-client/index.js
diff --git a/nodejs-zt1-client/package.json b/js/zt1-api-client/package.json
similarity index 100%
rename from nodejs-zt1-client/package.json
rename to js/zt1-api-client/package.json
diff --git a/nodejs-zt1-client/test.js b/js/zt1-api-client/test.js
similarity index 100%
rename from nodejs-zt1-client/test.js
rename to js/zt1-api-client/test.js

From ecb1ee8e0d70e8a6770611398f25a274c4ae6ce8 Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Tue, 26 May 2015 09:03:39 -0700
Subject: [PATCH 07/11] Renaming...

---
 js/zt1-api-client/index.js     | 22 +++++++++++-----------
 js/zt1-api-client/package.json |  6 +++---
 js/zt1-api-client/test.js      | 12 ++++++------
 3 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/js/zt1-api-client/index.js b/js/zt1-api-client/index.js
index 55f7fb24e..4e3598b58 100644
--- a/js/zt1-api-client/index.js
+++ b/js/zt1-api-client/index.js
@@ -2,14 +2,14 @@
 
 var request = require('request');
 
-function ZT1Client(url,authToken)
+function ZT1ApiClient(url,authToken)
 {
 	this.url = url;
 	this.authToken = authToken;
 }
 
 // Generate new ZeroTier identity -- mostly for testing
-ZT1Client.prototype.newIdentity = function(callback)
+ZT1ApiClient.prototype.newIdentity = function(callback)
 {
 	request({
 		url: this.url + 'newIdentity',
@@ -27,7 +27,7 @@ ZT1Client.prototype.newIdentity = function(callback)
 	});
 }
 
-ZT1Client.prototype._jsonGet = function(getPath,callback)
+ZT1ApiClient.prototype._jsonGet = function(getPath,callback)
 {
 	request({
 		url: this.url + getPath,
@@ -44,7 +44,7 @@ ZT1Client.prototype._jsonGet = function(getPath,callback)
 	});
 };
 
-ZT1Client.prototype.status = function(callback)
+ZT1ApiClient.prototype.status = function(callback)
 {
 	request({
 		url: this.url + 'controller',
@@ -77,27 +77,27 @@ ZT1Client.prototype.status = function(callback)
 	}.bind(this));
 };
 
-ZT1Client.prototype.getNetworks = function(callback)
+ZT1ApiClient.prototype.getNetworks = function(callback)
 {
 	this._jsonGet('network',callback);
 };
 
-ZT1Client.prototype.getPeers = function(callback)
+ZT1ApiClient.prototype.getPeers = function(callback)
 {
 	this._jsonGet('peer',callback);
 };
 
-ZT1Client.prototype.listControllerNetworks = function(callback)
+ZT1ApiClient.prototype.listControllerNetworks = function(callback)
 {
 	this._jsonGet('controller/network',callback);
 };
 
-ZT1Client.prototype.getControllerNetwork = function(nwid,callback)
+ZT1ApiClient.prototype.getControllerNetwork = function(nwid,callback)
 {
 	this._jsonGet('controller/network/' + nwid,callback);
 };
 
-ZT1Client.prototype.saveControllerNetwork = function(network,callback)
+ZT1ApiClient.prototype.saveControllerNetwork = function(network,callback)
 {
 	if ((typeof network.nwid !== 'string')||(network.nwid.length !== 16))
 		return callback(new Error('Missing required field: nwid'),null);
@@ -153,8 +153,8 @@ ZT1Client.prototype.saveControllerNetwork = function(network,callback)
 	});
 };
 
-ZT1Client.prototype.getControllerNetworkMember = function(nwid,address,callback) {
+ZT1ApiClient.prototype.getControllerNetworkMember = function(nwid,address,callback) {
 	this._jsonGet('controller/network/' + nwid + '/member/' + address,callback);
 };
 
-exports.ZT1Client = ZT1Client;
+exports.ZT1ApiClient = ZT1ApiClient;
diff --git a/js/zt1-api-client/package.json b/js/zt1-api-client/package.json
index 953f6d37d..bb818710b 100644
--- a/js/zt1-api-client/package.json
+++ b/js/zt1-api-client/package.json
@@ -1,7 +1,7 @@
 {
-  "name": "nodejs-zt1-client",
-  "version": "1.0.0",
-  "description": "ZeroTier One Network Virtualization Service JSON API Client",
+  "name": "zt1-api-client",
+  "version": "0.0.1",
+  "description": "ZeroTier One JSON API Client",
   "main": "index.js",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
diff --git a/js/zt1-api-client/test.js b/js/zt1-api-client/test.js
index 347ca1a18..bfe9b1120 100644
--- a/js/zt1-api-client/test.js
+++ b/js/zt1-api-client/test.js
@@ -1,24 +1,24 @@
-var ZT1Client = require('./index.js').ZT1Client;
+var ZT1ApiClient = require('./index.js').ZT1ApiClient;
 
-var zt1c = new ZT1Client('http://127.0.0.1:9993/','5d6181b71fae2684f9cc64ed');
+var zt1 = new ZT1ApiClient('http://127.0.0.1:9993/','5d6181b71fae2684f9cc64ed');
 
-zt1c.status(function(err,status) {
+zt1.status(function(err,status) {
 	if (err)
 		console.log(err);
 	else console.log(status);
 
-	zt1c.getNetworks(function(err,networks) {
+	zt1.getNetworks(function(err,networks) {
 		if (err)
 			console.log(err);
 		else console.log(networks);
 
-		zt1c.getPeers(function(err,peers) {
+		zt1.getPeers(function(err,peers) {
 			if (err)
 				console.log(err);
 			else console.log(peers);
 
 			if (status.controller) {
-				zt1c.saveControllerNetwork({
+				zt1.saveControllerNetwork({
 					nwid: status.address + 'dead01',
 					name: 'test network',
 					private: true

From c075e68c6cdddb8d932a6aad315b85aa633e8e0e Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Tue, 26 May 2015 13:32:47 -0700
Subject: [PATCH 08/11] More work on ZT1 NodeJS API client library.

---
 js/zt1-api-client/constrain-types.js |  58 +++++++++
 js/zt1-api-client/index.js           | 185 ++++++++++++++++++---------
 js/zt1-api-client/test.js            |   6 +-
 3 files changed, 189 insertions(+), 60 deletions(-)
 create mode 100644 js/zt1-api-client/constrain-types.js

diff --git a/js/zt1-api-client/constrain-types.js b/js/zt1-api-client/constrain-types.js
new file mode 100644
index 000000000..5b1137d59
--- /dev/null
+++ b/js/zt1-api-client/constrain-types.js
@@ -0,0 +1,58 @@
+'use strict'
+
+function convertType(v,t)
+{
+	if (Array.isArray(t)) {
+		var r = v;
+		if (t.length !== 0) {
+			if (Array.isArray(v)) {
+				r = [];
+				for(var i=0;i<v.length;++i)
+					r.push(convertType(v[i],t[0]));
+			} else r = [ convertType(v,t[0]) ];
+		} else r = [ v ];
+		return r;
+	} else if (t === 'string') {
+		if (typeof v === 'string')
+			return v;
+		else if ((typeof v === 'boolean')||(typeof v === 'number'))
+			return v.toString();
+		else if (Array.isArray(v)||(typeof v === 'object'))
+			return JSON.stringify(v);
+		else return '';
+	} else if (t === 'integer') {
+		if (typeof v === 'number')
+			return Math.round(v);
+		else if (typeof v === 'string')
+			return parseInt(v);
+		else if (typeof v === 'boolean')
+			return ((v) ? 1 : 0);
+		else return 0;
+	} else if (t === 'number') {
+		if (typeof v === 'number')
+			return v;
+		else if (typeof v === 'string')
+			return parseFloat(v);
+		else if (typeof v === 'boolean')
+			return ((v) ? 1 : 0);
+		else return 0;
+	} else if (t === 'boolean') {
+		return ((v) ? true : false);
+	} else if (typeof t === 'object') {
+		if ((v !== null)&&(typeof v === 'object'))
+			return constrainTypes(v,t);
+		else return {};
+	} else return v;
+}
+
+function constrainTypes(obj,typeMap)
+{
+	var r = {};
+	for(var k in obj) {
+		var t = typeMap[k];
+		r[k] = convertType(v,t);
+	}
+	return r;
+}
+
+exports = constrainTypes;
diff --git a/js/zt1-api-client/index.js b/js/zt1-api-client/index.js
index 4e3598b58..a5f109292 100644
--- a/js/zt1-api-client/index.js
+++ b/js/zt1-api-client/index.js
@@ -1,13 +1,83 @@
 'use strict'
 
 var request = require('request');
+var constrainTypes = require('./constrain-types.js');
 
+// Types that fields must be in submissions -- used with constrainTypes to
+// ensure that submitted JSON objects are correctly typed since the JSON
+// API is very sensitive to this. This only includes writable fields since
+// non-writable and unknown fields are ignored.
+var REQUEST_TYPE_MAPS = {
+	'controller/network/*/relay': {
+		'address': 'string',
+		'phyAddress': 'string'
+	},
+	'controller/network/*/rule': {
+		'ruleId': 'integer',
+		'nodeId': 'string',
+		'vlanId': 'integer',
+		'vlanPcp': 'integer',
+		'etherType': 'integer',
+		'macSource': 'string',
+		'macDest': 'string',
+		'ipSource': 'string',
+		'ipDest': 'string',
+		'ipTos': 'integer',
+		'ipProtocol': 'integer',
+		'ipSourcePort': 'integer',
+		'ipDestPort': 'integer',
+		'flags': 'integer',
+		'invFlags': 'integer',
+		'action': 'string'
+	},
+	'controller/network/*/ipAssignmentPool': {
+		'network': 'string',
+		'netmaskBits': 'integer'
+	},
+	'controller/network/*/member': {
+		'authorized': 'boolean',
+		'activeBridge': 'boolean',
+		'ipAssignments': [ 'string' ]
+	},
+	'controller/network/*': {
+		'name': 'string',
+		'private': 'boolean',
+		'enableBroadcast': 'boolean',
+		'allowPassiveBridging': 'boolean',
+		'v4AssignMode': 'string',
+		'v6AssignMode': 'string',
+		'multicastLimit': 'integer',
+		'relays': [ this['controller/network/*/relay'] ],
+		'ipAssignmentPools': [ this['controller/network/*/ipAssignmentPool'] ],
+		'rules': [ this['controller/network/*/rule'] ]
+	}
+};
+
+// URL must end with trailing slash e.g. http://127.0.0.1:9993/
 function ZT1ApiClient(url,authToken)
 {
 	this.url = url;
 	this.authToken = authToken;
 }
 
+// Simple JSON URI getter, for internal use.
+ZT1ApiClient.prototype._jsonGet = function(getPath,callback)
+{
+	request({
+		url: this.url + getPath,
+		method: 'GET',
+		headers: {
+			'X-ZT1-Auth': this.authToken
+		}
+	},function(error,response,body) {
+		if (error)
+			return callback(error,null);
+		if (response.statusCode !== 200)
+			return callback(new Error('server responded with error: '+response.statusCode),null);
+		return callback(null,(typeof body === 'string') ? JSON.parse(body) : null);
+	});
+};
+
 // Generate new ZeroTier identity -- mostly for testing
 ZT1ApiClient.prototype.newIdentity = function(callback)
 {
@@ -27,23 +97,7 @@ ZT1ApiClient.prototype.newIdentity = function(callback)
 	});
 }
 
-ZT1ApiClient.prototype._jsonGet = function(getPath,callback)
-{
-	request({
-		url: this.url + getPath,
-		method: 'GET',
-		headers: {
-			'X-ZT1-Auth': this.authToken
-		}
-	},function(error,response,body) {
-		if (error)
-			return callback(error,null);
-		if (response.statusCode !== 200)
-			return callback(new Error('server responded with error: '+response.statusCode),null);
-		return callback(null,(typeof body === 'string') ? JSON.parse(body) : null);
-	});
-};
-
+// Get node status -- returns a combination of regular status and (if present) controller info
 ZT1ApiClient.prototype.status = function(callback)
 {
 	request({
@@ -54,9 +108,9 @@ ZT1ApiClient.prototype.status = function(callback)
 		}
 	},function(error,response,body) {
 		if (error)
-			return callback(error,{});
+			return callback(error,null);
 		var controllerStatus = {};
-		if (typeof body === 'string')
+		if ((typeof body === 'string')&&(response.statusCode === 200))
 			controllerStatus = JSON.parse(body);
 		request({
 			url: this.url + 'status',
@@ -97,50 +151,15 @@ ZT1ApiClient.prototype.getControllerNetwork = function(nwid,callback)
 	this._jsonGet('controller/network/' + nwid,callback);
 };
 
+// If NWID is the special ##########______ format, a new NWID will
+// be generated server side and filled in in returned object.
 ZT1ApiClient.prototype.saveControllerNetwork = function(network,callback)
 {
-	if ((typeof network.nwid !== 'string')||(network.nwid.length !== 16))
-		return callback(new Error('Missing required field: nwid'),null);
-
-	// The ZT1 service is type variation intolerant, so recreate our submission with the correct types
-	var n = {
-		nwid: network.nwid
-	};
-	if (network.name)
-		n.name = network.name.toString();
-	if ('private' in network)
-		n.private = (network.private) ? true : false;
-	if ('enableBroadcast' in network)
-		n.enableBroadcast = (network.enableBroadcast) ? true : false;
-	if ('allowPassiveBridging' in network)
-		n.allowPassiveBridging = (network.allowPassiveBridging) ? true : false;
-	if ('v4AssignMode' in network) {
-		if (network.v4AssignMode)
-			n.v4AssignMode = network.v4AssignMode.toString();
-		else n.v4AssignMode = 'none';
-	}
-	if ('v6AssignMode' in network) {
-		if (network.v6AssignMode)
-			n.v6AssignMode = network.v6AssignMode.toString();
-		else n.v4AssignMode = 'none';
-	}
-	if ('multicastLimit' in network) {
-		if (typeof network.multicastLimit === 'number')
-			n.multicastLimit = network.multicastLimit;
-		else n.multicastLimit = parseInt(network.multicastLimit.toString());
-	}
-	if (Array.isArray(network.relays))
-		n.relays = network.relays;
-	if (Array.isArray(network.ipAssignmentPools))
-		n.ipAssignmentPools = network.ipAssignmentPools;
-	if (Array.isArray(network.rules))
-		n.rules = network.rules;
-
 	request({
 		url: this.url + 'controller/network/' + n.nwid,
 		method: 'POST',
 		json: true,
-		body: n,
+		body: constrainTypes(network,REQUEST_TYPE_MAPS['controller/network/*']),
 		headers: {
 			'X-ZT1-Auth': this.authToken
 		}
@@ -153,8 +172,60 @@ ZT1ApiClient.prototype.saveControllerNetwork = function(network,callback)
 	});
 };
 
+ZT1ApiClient.prototype.deleteControllerNetwork = function(nwid,callback) {
+	request({
+		url: this.url + 'controller/network/'+ nwid,
+		method: 'DELETE',
+		headers: {
+			'X-ZT1-Auth': this.authToken
+		}
+	},function(err,response,body) {
+		if (err)
+			return callback(err);
+		else if (response.statusCode === 200)
+			return callback(null);
+		else return callback(new Error('server responded with error: '+response.statusCode));
+	});
+};
+
 ZT1ApiClient.prototype.getControllerNetworkMember = function(nwid,address,callback) {
 	this._jsonGet('controller/network/' + nwid + '/member/' + address,callback);
 };
 
+ZT1ApiClient.prototype.saveControllerNetworkMember = function(nwid,member,callback) {
+	var m = constrainTypes(member,REQUEST_TYPE_MAPS['controller/network/*/member']);
+	m.nwid = nwid;
+	request({
+		url: this.url + 'controller/network' + nwid + '/member/' + member.address,
+		method: 'POST',
+		json: true,
+		body: m,
+		headers: {
+			'X-ZT1-Auth': this.authToken
+		}
+	},function(err,response,body) {
+		if (err)
+			return callback(err,null);
+		if (response.statusCode !== 200)
+			return callback(new Error('server responded with error: '+response.statusCode),null);
+		return callback(null,(typeof body === 'string') ? JSON.parse(body) : body);
+	});
+};
+
+ZT1ApiClient.prototype.deleteControllerNetworkMember = function(nwid,address,callback) {
+	request({
+		url: this.url + 'controller/network/' + nwid + '/member/' + address,
+		method: 'DELETE',
+		headers: {
+			'X-ZT1-Auth': this.authToken
+		}
+	},function(err,response,body) {
+		if (err)
+			return callback(err);
+		else if (response.statusCode === 200)
+			return callback(null);
+		else return callback(new Error('server responded with error: '+response.statusCode));
+	});
+};
+
 exports.ZT1ApiClient = ZT1ApiClient;
diff --git a/js/zt1-api-client/test.js b/js/zt1-api-client/test.js
index bfe9b1120..6a5db453d 100644
--- a/js/zt1-api-client/test.js
+++ b/js/zt1-api-client/test.js
@@ -19,9 +19,9 @@ zt1.status(function(err,status) {
 
 			if (status.controller) {
 				zt1.saveControllerNetwork({
-					nwid: status.address + 'dead01',
-					name: 'test network',
-					private: true
+					"nwid": status.address + 'dead01',
+					"name": 'test network',
+					"private": true
 				},function(err,network) {
 					if (err)
 						console.log(err);

From ff2272d59adb8a83c3a1a83d53f7a0062b5b21fd Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Tue, 26 May 2015 13:34:08 -0700
Subject: [PATCH 09/11] Delete some temporary code.

---
 js/zt1-api-client/test.js | 33 ---------------------------------
 1 file changed, 33 deletions(-)
 delete mode 100644 js/zt1-api-client/test.js

diff --git a/js/zt1-api-client/test.js b/js/zt1-api-client/test.js
deleted file mode 100644
index 6a5db453d..000000000
--- a/js/zt1-api-client/test.js
+++ /dev/null
@@ -1,33 +0,0 @@
-var ZT1ApiClient = require('./index.js').ZT1ApiClient;
-
-var zt1 = new ZT1ApiClient('http://127.0.0.1:9993/','5d6181b71fae2684f9cc64ed');
-
-zt1.status(function(err,status) {
-	if (err)
-		console.log(err);
-	else console.log(status);
-
-	zt1.getNetworks(function(err,networks) {
-		if (err)
-			console.log(err);
-		else console.log(networks);
-
-		zt1.getPeers(function(err,peers) {
-			if (err)
-				console.log(err);
-			else console.log(peers);
-
-			if (status.controller) {
-				zt1.saveControllerNetwork({
-					"nwid": status.address + 'dead01',
-					"name": 'test network',
-					"private": true
-				},function(err,network) {
-					if (err)
-						console.log(err);
-					else console.log(network);
-				});
-			}
-		});
-	});
-});

From fbb990f8a3327470771c7d56bd6c93e512c576ad Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Tue, 26 May 2015 14:36:04 -0700
Subject: [PATCH 10/11] Mac uninstall.sh update.

---
 ext/installfiles/mac/uninstall.sh | 64 +++++++++++++++----------------
 1 file changed, 31 insertions(+), 33 deletions(-)

diff --git a/ext/installfiles/mac/uninstall.sh b/ext/installfiles/mac/uninstall.sh
index 022af53d8..d1effb933 100755
--- a/ext/installfiles/mac/uninstall.sh
+++ b/ext/installfiles/mac/uninstall.sh
@@ -2,49 +2,47 @@
 
 export PATH=/bin:/usr/bin:/sbin:/usr/sbin
 
-zthome="/Library/Application Support/ZeroTier/One"
-ztapp="/Applications/ZeroTier One.app"
-if [ -z "$ztapp" -o ! -d "$ztapp" ]; then
-	ztapp=`mdfind kMDItemCFBundleIdentifier == 'com.zerotier.ZeroTierOne' | grep -E '.*ZeroTier One[.]app$' | grep -v -F '/build-' | grep -v -F '/Volumes/ZeroTier' | sort | head -n 1`
-fi
-
 if [ "$UID" -ne 0 ]; then
 	echo "Must be run as root; try: sudo $0"
 	exit 1
 fi
 
-echo "Killing any running zerotier-one service..."
-killall -TERM zerotier-one >>/dev/null 2>&1
-sleep 3
-killall -KILL zerotier-one >>/dev/null 2>&1
-sleep 1
-
-echo "Unloading kernel extension..."
-kextunload "$zthome/pre10.8/tap.kext" >>/dev/null 2>&1
-kextunload "$zthome/tap.kext" >>/dev/null 2>&1
-
-echo "Erasing GUI app (if installed)..."
-if [ ! -z "$ztapp" -a -d "$ztapp" -a -f "$ztapp/Contents/Info.plist" ]; then
-	rm -rf "$ztapp"
+if [ ! -f '/Library/LaunchDaemons/com.zerotier.one.plist' ]; then
+	echo 'ZeroTier One does not seem to be installed.'
+	exit 1
 fi
 
-echo "Erasing service and support files..."
-rm -f /usr/bin/zerotier-cli
-rm -f /usr/bin/zerotier-idtool
-cd "$zthome"
-rm -f zerotier-one *.persist identity.public *.log *.pid *.sh shutdownIfUnreadable
-rm -rf pre10.8 tap.kext updates.d networks.d
+cd /
 
-echo "Removing LaunchDaemons item..."
-rm -f /Library/LaunchDaemons/com.zerotier.one.plist
-launchctl remove com.zerotier.one
+echo 'Stopping any running ZeroTier One service...'
+launchctl unload '/Library/LaunchDaemons/com.zerotier.one.plist' >>/dev/null 2>&1
+sleep 1
+killall -TERM zerotier-one >>/dev/null 2>&1
+sleep 1
+killall -KILL zerotier-one >>/dev/null 2>&1
 
-echo "Done."
+echo "Making sure kext is unloaded..."
+kextunload '/Library/Application Support/ZeroTier/One/tap.kext' >>/dev/null 2>&1
+
+echo "Removing ZeroTier One files..."
+
+rm -rf '/Applications/ZeroTier One.app'
+rm -f '/usr/bin/zerotier-one' '/usr/bin/zerotier-idtool' '/usr/bin/zerotier-cli' '/Library/LaunchDaemons/com.zerotier.one.plist'
+mkdir -p /tmp/ZeroTierOne_uninstall_tmp
+cp "/Library/Application Support/ZeroTier/One/*.secret" /tmp/ZeroTierOne_uninstall_tmp
+rm -rf '/Library/Application Support/ZeroTier/One'
+mkdir -p '/Library/Application Support/ZeroTier/One'
+cp "/tmp/ZeroTierOne_uninstall_tmp/*.secret" '/Library/Application Support/ZeroTier/One'
+chmod 0600 "/Library/Application Support/ZeroTier/One/*.secret"
+rm -rf /tmp/ZeroTierOne_uninstall_tmp
+
+echo 'Uninstall complete.'
 echo
-echo "Your ZeroTier One identity is still in: $zthome"
-echo "as identity.secret and can be manually deleted if you wish. Save it if"
-echo "you wish to re-use the address of this node, as it cannot be regenerated."
-
+echo 'Your identity and secret authentication token have been preserved in:'
+echo '  /Library/Application Support/ZeroTier/One'
+echo
+echo 'You can delete this folder and its contents if you do not intend to re-use'
+echo 'them.'
 echo
 
 exit 0

From e184aa4cb43151690959d895cba7d1f1bbc036fa Mon Sep 17 00:00:00 2001
From: Adam Ierymenko <adam.ierymenko@gmail.com>
Date: Tue, 26 May 2015 18:16:12 -0700
Subject: [PATCH 11/11] Clean old netconf-service from attic.

---
 .gitignore                              |   1 +
 attic/netconf-service/README.md         |  51 --
 attic/netconf-service/config.js         |   3 -
 attic/netconf-service/initdb.js         |  73 ---
 attic/netconf-service/netconf-master.js | 624 ------------------------
 attic/netconf-service/netconf.service   |  13 -
 attic/netconf-service/package.json      |  15 -
 attic/netconf-service/redis-schema.md   | 111 -----
 8 files changed, 1 insertion(+), 890 deletions(-)
 delete mode 100644 attic/netconf-service/README.md
 delete mode 100644 attic/netconf-service/config.js
 delete mode 100644 attic/netconf-service/initdb.js
 delete mode 100644 attic/netconf-service/netconf-master.js
 delete mode 100755 attic/netconf-service/netconf.service
 delete mode 100644 attic/netconf-service/package.json
 delete mode 100644 attic/netconf-service/redis-schema.md

diff --git a/.gitignore b/.gitignore
index 64bf02dea..47abf6d75 100755
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,7 @@
 *.obj
 *.tlog
 *.pid
+*.pkg
 /*.deb
 /*.rpm
 /build-*
diff --git a/attic/netconf-service/README.md b/attic/netconf-service/README.md
deleted file mode 100644
index ae5fb8b9d..000000000
--- a/attic/netconf-service/README.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# ZeroTier One Network Configuration Service
-
-## What is it?
-
-It's the thing that controls virtual networks. It's a completely separate subsystem from supernodes; see the [Technical FAQ](https://github.com/zerotier/ZeroTierOne/wiki/Technical-FAQ) for more information on that.
-
-ZeroTier's 16-digit / 64-bit network IDs are actually two numbers packed together into one. The most significant 40 bits / first 10 digits of a network ID are the ZeroTier address of the *network configuration master* responsible for issuing network configurations and certificates to members of the network. The least significant 24 bits / last 6 digits are an arbitrary 24-bit number used to identify this network on its given master.
-
-When a ZeroTier node joins a network or updates its network configuration, it queries the network configuration master and receives a response containing either an error or a dictionary with that node's membership information and (if it's a private network) membership certificate.
-
-Network configuration masters can go offline without affecting communication on the network, since they're only needed when it's necessary to issue new configurations. (Certificates are a more involved topic that's beyond the scope of this document, but suffice to say that the same applies there.) They can also be made fault tolerant by mirroring their identities and databases to a backup server that can take over if the main server dies.
-
-Networks managed through the *zerotier.com* site are managed by network configuration masters run by ZeroTier networks, but if you're willing to do a bit of manual system administration you can set up and run your own.
-
-## Installation
-
-The first step is to choose a node to act as a netconf master. The netconf master subprocess is only supported on Unix-like platforms and has only been tested so far on Linux, but will probably work on Mac as well. But Windows builds don't support service subprocesses so the master cannot be a Windows node.
-
-At the moment, netconf masters *cannot join their own networks*. We recommend using a node that you don't intend to make a member of the networks it administrates. Or, as an alternative, you can run ZeroTier One on a different port and with a different home folder. This is what we do, and on our masters we run it separately on startup with a script that executes "sudo /var/lib/zerotier-one/zerotier-one -p9994 /var/lib/zerotier-one-netconf-master &". This runs the service on UDP port 9994 and uses "/var/lib/zerotier-one-netconf-master" as its home folder. *Two or more instances of ZeroTier One can co-exist on the same machine as long as only one of them tries to actually join networks.*
-
-Before setting up, three prerequisites must be installed: [Node](http://nodejs.org), npm (node's package manager), and Redis (a NoSQL database). On most Linux distributions there are packages for all these, so installing them should be easy. On CentOS/RedHat based distributions they are in the [EPEL](https://fedoraproject.org/wiki/EPEL) repository, so after installing or enabling EPEL you can just type "sudo yum install nodejs npm redis". Then start the Redis service with "sudo service redis start" and enable it on boot with "sudo chkconfig redis on". (Commands may differ on other Linux distributions.) Verify that it's running by typing "redis-cli" and testing whether it connects.
-
-Once prerequisites are installed, follow these instructions:
-
-1) Go to the ZeroTier home (for the node you plan to designate as master) and create a subfolder called "services.d".
-
-2) Copy or symlink "netconf-master" from this repository into "services.d".
-
-3) In the "services.d" folder, create a symlink called "netconf.service" that points to "netconf-master/netconf.service". Also check to make sure that "netconf-master/netconf.service" is executable. This is what the ZeroTier One service will execute to launch the subprocess.
-
-4) In "services.d/netconf-master" type "npm install" to install NodeJS package dependencies.
-
-5) Edit the initdb.js and change the network ID and other settings of the network you want to create. Remember how network IDs are made; the *first 10 digits* of your network's ID *must be the ZeroTier address of your netconf master node*. If it doesn't have one yet, try starting the ZeroTier service briefly and then killing it and it will generate *identity.public* and *identity.secret* automatically. The address is the first hex field in either of these files.
-
-6) Run "node initdb.js" to initialize your Redis database. (If you want to use a Redis database other than 0, such as to avoid polluting another Redis database on the same machine, edit "config.js" and change the Redis database index there before initializing the database or running the service.)
-
-7) Start or restart the ZeroTier One service. Check its "node.log" and your system process list to ensure that it successfully started the netconf server slave process.
-
-## Using Your Own Networks
-
-Once the netconf master service is running, you can test it by simply joining the network ID of the network you created from any ZeroTier One node anywhere in the world. If everything is set up correctly, it should just work. Your netconf service is controlling this network.
-
-## Private Networks and Certificates
-
-If you set the "private" flag to "1" and designated your network as private, members must be authorized. Each time a member attempts to join a network, a member record is created in Redis. These will have Redis names like "zt1:network:################:member:##########:~". Open this hash in any Redis database editor (or use hgetall and hset in redis-cli) and set "authorized" to "1" for whichever members you wish to authorize.
-
-## What About the Web UI?
-
-At the moment, the web-based administration interface used on zerotier.com is the only part of the system that is not open source. Right now it's all tied in with our payment processor, and our model right now is to charge for the convenience of using it.
-
-We *might* open source it in the future, but in the meantime anyone with a bit of developer expertise should be able to write a few scripts to list networks, change network parameters, authorize members on private networks, etc. The Redis database schema is documented in *redis-schema.md*, and only the stuff under "zt1:network:..." is actually used by the netconf master service. The stuff under "zt1:user:..." is used by our web UI and doesn't need to be present for the netconf service to operate.
diff --git a/attic/netconf-service/config.js b/attic/netconf-service/config.js
deleted file mode 100644
index b4d9733ef..000000000
--- a/attic/netconf-service/config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-exports.redisDb = 0; // live
-//exports.redisDb = 1; // test
-//exports.redisDb = 2; // dev
diff --git a/attic/netconf-service/initdb.js b/attic/netconf-service/initdb.js
deleted file mode 100644
index 813cc04a3..000000000
--- a/attic/netconf-service/initdb.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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:31443a1a0a111111:~": {
-		"id": "31443a1a0a111111",           // 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,                  // unused 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": "192.168.123.0/24", // IPv4 net block / netmask bits
-		"v6AssignMode": "none"              // 'zt' to assign, 'none' to let OS do it
-	}
-};
-
-var config = require('./config.js');
-
-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/attic/netconf-service/netconf-master.js b/attic/netconf-service/netconf-master.js
deleted file mode 100644
index 34d8634fa..000000000
--- a/attic/netconf-service/netconf-master.js
+++ /dev/null
@@ -1,624 +0,0 @@
-//
-// 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 <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');
-
diff --git a/attic/netconf-service/netconf.service b/attic/netconf-service/netconf.service
deleted file mode 100755
index df46e39ff..000000000
--- a/attic/netconf-service/netconf.service
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-
-export PATH=/bin:/usr/bin:/usr/local/bin
-
-# We will start in ZT_HOME
-
-if [ ! -d ./services.d/netconf-service ]; then
-	echo 'cannot find netconf-service subfolder to launch subprocess' >&2
-	exit 1
-fi
-
-cd services.d/netconf-service
-exec node netconf-master.js
diff --git a/attic/netconf-service/package.json b/attic/netconf-service/package.json
deleted file mode 100644
index 19e06917c..000000000
--- a/attic/netconf-service/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "name": "zt1-netconf-service",
-  "version": "0.0.0",
-  "description": "Worker in charge of issuing network configuration from ZeroTier One netconf masters",
-  "main": "netconf-master.js",
-  "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
-  },
-  "author": "ZeroTier Networks LLC",
-  "license": "GPL",
-  "dependencies": {
-    "redis": "~0.10.3",
-    "async": "~0.9.0"
-  }
-}
diff --git a/attic/netconf-service/redis-schema.md b/attic/netconf-service/redis-schema.md
deleted file mode 100644
index e950f9e78..000000000
--- a/attic/netconf-service/redis-schema.md
+++ /dev/null
@@ -1,111 +0,0 @@
-# ZeroTier One Redis Database Schema
-
-## Notes
-
-- : is used as the key namespace separator as per de-facto Redis standard.
-- A top-level record may have a :~ child containing a hash. This is the root hash and contains any simple key=value properties of the record.
-- Booleans: any value other than "1" or "true" is false.
-- Timestamps are in milliseconds since the epoch and are stored as base-10 integers.
-- IPv4 addresees: stored in standard dot notation e.g. 1.2.3.4
-- IPv6 addresses: :'s are optional and addresses must be stored *without* shortening, e.g. with :0000: instead of ::. It must be possible to strip :'s from the address and get 128 bits of straight hex.
-- Hexadecimal: all hex values must be lower case
-- 16-digit network IDs and 10-digit addresses are left zero-padded to 16 and 10 digits respectively, as they are everywhere else in the ZT1 universe.
-
-Note: right now KEYS globbing is used in a few places in the code to search for stuff. In the future we'll want to add SET/ZSET index fields for these to optimize, but that won't become an issue until there are at least millions of records. (Millions of users will give us lots of "good problems to have." :)
-
-## Field attribute flags used in this documentation (not in database)
-
-- ! required
-- M mutable (user-editable via API)
-- R read-only (at least from API/user perspective)
-- H hidden (not returned by API queries)
-
-## Base Configuration
-
-### zt1 (value: 1)
-
-If this key is not present with a value of 1, the API server code will auto-init the DB with initial data containing stuff like the Earth network and default users. This is not used by netconf-service.
-
-## Users (ZeroTier Networks only)
-
-This record type is only of interest to ZeroTier Networks itself. It holds user records, billing information, subscriptions, etc. Netconf masters do not use these records so you don't need to worry about this if you are trying to run your own.
-
-### zt1:user:\<auth\>:\<authUserId\>:~
-
-Note: users are referred to elsewhere in the database by their compound key \<auth\>:\<authUserId\> as stored here in the id field.
-
-- !R id :: must be auth:authUserId
-- !R auth :: authentication type e.g. 'google' or 'local'
-- !R authUserId :: user ID under auth schema, like an e-mail address or a Google profile ID.
-- M email :: user's email address
-- R confirmed :: is e-mail confirmed?
-- R lastLogin :: timestamp of last login
-- R creationTime: :: timestamp of account creation
-- M displayName :: usually First Last, defaults to e-mail address for 'local' auth and whatever the OpenID API says for third party auth such as Google.
-- M defaultCard :: ID of default credit card (actual card objects are stored by Stripe, not in this database)
-- M ui :: arbitrary field that can be used by the UI to store stuff
-- R stripeCustomerId :: customer ID for Stripe credit card service if the user has cards on file (we don't store cards, we let Stripe do that)
-
-## Networks
-
-Network records are used by the netconf master to issue network configuration information to peers on the ZT1 network itself. Here is where you should look if you want to play with running your own!
-
-### zt1:network:\<nwid\>:~
-
-Each network has a network record indexed by its 64-bit network ID in lower-case hexadecimal. Unless otherwise indicated all integer values are in hexadecimal.
-
-- !R id :: must be \<nwid\>
-- !M name :: network's globally unique short name, which can contain only characters valid in an e-mail address. It's the job of the code that populates this DB to ensure that this is globally unique.
-- R owner :: id of user who owns this network (not used by netconf master, only for web UI and web API)
-- R billingUser :: user paying for premium subscriptions (also unused by netconf-master)
-- R billingUserConfirmed :: if true, billingUser has confirmed and authorized billing
-- M desc :: a longer network description
-- R infrastructure :: if true, network can't be deleted through API or web UI
-- M private :: if true, network requires authentication
-- R creationTime :: timestamp of network creation
-- M etherTypes :: comma-delimited list of integers indicating Ethernet types permitted on network
-- M enableBroadcast :: if true, ff:ff:ff:ff:ff:ff is enabled network-wide
-- M v4AssignMode :: 'none' (or null/empty/etc.), 'zt', 'dhcp'
-- M v4AssignPool :: network/bits from which to assign IPs
-- M v6AssignMode :: 'none' (or null/empty/etc.), 'zt', 'v6native', 'dhcp6'
-- M v6AssignPool :: network/bits from which to assign IPs
-- M allowPassiveBridging :: if true, allow passive bridging
-- M multicastLimit :: maximum number of recipients to receive a multicast on this network
-- M multicastRates :: packed JSON containing multicast rates (see below)
-- M subscriptions :: comma-delimited list of subscriptions for this network
-- M ui :: arbitrary field that can be used by the UI to store stuff
-
-Multicast rates are encoded as a JSON document. Each key is a multicast group in "MAC/ADI" format (e.g. *ff:ff:ff:ff:ff:ff/0*), and each value is a comma-delimited tuple of hex integer values: preload, max balance, and rate of accrual in bytes per second. An entry for *0* (or *0/0* or *00:00:00:00:00:00/0*) indicates the default setting for all unspecified multicast groups. Setting a rate limit like *ffffffff,ffffffff,ffffffff* as default will effectively turn off rate limits.
-
-### zt1:network:\<nwid\>:member:\<address\>:~
-
-For private networks, each member of the network must have a record that indicates whether it is allowed to communicate. The address is the 10-digit lower-case hexadecimal ZeroTier address.
-
-The netconf-master will automatically add any peer that even attempts to request a netconf / certificate. These are added with authorized set to false. The hideInSlushpile field is used in the UI to allow network admins to hide unknown/bogus join attempts that they don't want to keep seeing.
-
-- !R id :: must be \<address\>
-- !R nwid :: must be \<nwid\>
-- M authorized :: true if node is authorized and will be issued valid certificates and network configurations
-- M activeBridge :: true if node is an active bridge
-- M name :: name of system
-- M notes :: annotation field
-- R authorizedBy :: user ID of user who authorized membership
-- R authorizedAt :: timestamp of authorization
-- R identity :: string-serialized full public node identity as last seen by netconf-master
-- R firstSeen :: timestamp node first tried to authorize to this net
-- R lastSeen :: timestamp node last tried to authorize to this net
-- R lastAt :: real network address from which node was last seen
-- R clientVersion :: software version last seen by netconf-master
-- R clientOs :: operating system last seen by netconf-master
-- R ipAssignments :: comma-delimited list of IP address assignments (see below)
-- M ui :: string-serialized JSON blob for use by the user interface, ignored by netconf-master
-
-The name, notes, authorizedBy, and authorizedAt fields are only for use by the administration UI. The netconf-master does not care about this. The identity, firstSeen, lastSeen, lastAt, version, and os fields are populated by netconf-master.
-
-The ipAssignments field is re-generated whenever the zt1:network:\<nwid\>:ipAssignments hash is modified for this member. Both the API code and the netconf-master code must keep this in sync. This field is read-only for API users; the ipAssignments part of the API must be used to modify member IP address assignments.
-
-### zt1:network:\<nwid\>:ipAssignments
-
-This is a hash mapping IP/netmask bits fields to 10-digit ZeroTier addresses of network members. IPv4 fields contain dots, e.g. "10.2.3.4/24" or "29.1.1.1/7". IPv6 fields contain colons. Note that IPv6 IP abbreviations must *not* be used; use \:0000\: instead of \:\: for zero parts of addresses. This is to simplify parser code and canonicalize for rapid search. All hex digits must be lower-case.
-
-This is only used if the network's IPv4 and/or IPv6 auto-assign mode is 'zt' for ZeroTier assignment. The netconf-master will auto-populate by choosing unused IPs, and it can be edited via the API as well.