Pārlūkot izejas kodu

Fix deadlock in test mode.

Adam Ierymenko 10 gadi atpakaļ
vecāks
revīzija
bebe3d7cfa
2 mainītis faili ar 1338 papildinājumiem un 1325 dzēšanām
  1. 1331 1325
      controller/SqliteNetworkController.cpp
  2. 7 0
      controller/SqliteNetworkController.hpp

+ 1331 - 1325
controller/SqliteNetworkController.cpp

@@ -303,431 +303,580 @@ SqliteNetworkController::~SqliteNetworkController()
 
 NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(const InetAddress &fromAddr,const Identity &signingId,const Identity &identity,uint64_t nwid,const Dictionary &metaData,Dictionary &netconf)
 {
-	// Decode some stuff from metaData
-	const unsigned int clientMajorVersion = (unsigned int)metaData.getHexUInt(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MAJOR_VERSION,0);
-	const unsigned int clientMinorVersion = (unsigned int)metaData.getHexUInt(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MINOR_VERSION,0);
-	const unsigned int clientRevision = (unsigned int)metaData.getHexUInt(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_REVISION,0);
-	const bool clientIs104 = (Utils::compareVersion(clientMajorVersion,clientMinorVersion,clientRevision,1,0,4) >= 0);
+	Mutex::Lock _l(_lock);
+	return _doNetworkConfigRequest(fromAddr,signingId,identity,nwid,metaData,netconf);
+}
 
+unsigned int SqliteNetworkController::handleControlPlaneHttpGET(
+	const std::vector<std::string> &path,
+	const std::map<std::string,std::string> &urlArgs,
+	const std::map<std::string,std::string> &headers,
+	const std::string &body,
+	std::string &responseBody,
+	std::string &responseContentType)
+{
 	Mutex::Lock _l(_lock);
+	return _doCPGet(path,urlArgs,headers,body,responseBody,responseContentType);
+}
 
-	// Note: we can't reuse prepared statements that return const char * pointers without
-	// making our own copy in e.g. a std::string first.
+unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
+	const std::vector<std::string> &path,
+	const std::map<std::string,std::string> &urlArgs,
+	const std::map<std::string,std::string> &headers,
+	const std::string &body,
+	std::string &responseBody,
+	std::string &responseContentType)
+{
+	if (path.empty())
+		return 404;
+	Mutex::Lock _l(_lock);
 
-	if ((!signingId)||(!signingId.hasPrivate())) {
-		netconf["error"] = "signing identity invalid or lacks private key";
-		return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
-	}
-	if (signingId.address().toInt() != (nwid >> 24)) {
-		netconf["error"] = "signing identity address does not match most significant 40 bits of network ID";
-		return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
-	}
+	if (path[0] == "network") {
 
-	// Check rate limit
+		if ((path.size() >= 2)&&(path[1].length() == 16)) {
+			uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
+			char nwids[24];
+			Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
 
-	{
-		uint64_t &lrt = _lastRequestTime[std::pair<Address,uint64_t>(identity.address(),nwid)];
-		uint64_t lrt2 = lrt;
-		if (((lrt = OSUtils::now()) - lrt2) <= ZT_NETCONF_MIN_REQUEST_PERIOD)
-			return NetworkController::NETCONF_QUERY_IGNORE;
-	}
+			int64_t revision = 0;
+			sqlite3_reset(_sGetNetworkRevision);
+			sqlite3_bind_text(_sGetNetworkRevision,1,nwids,16,SQLITE_STATIC);
+			bool networkExists = false;
+			if (sqlite3_step(_sGetNetworkRevision) == SQLITE_ROW) {
+				networkExists = true;
+				revision = sqlite3_column_int64(_sGetNetworkRevision,0);
+			}
 
-	NetworkRecord network;
-	memset(&network,0,sizeof(network));
-	Utils::snprintf(network.id,sizeof(network.id),"%.16llx",(unsigned long long)nwid);
+			if (path.size() >= 3) {
 
-	MemberRecord member;
-	memset(&member,0,sizeof(member));
-	Utils::snprintf(member.nodeId,sizeof(member.nodeId),"%.10llx",(unsigned long long)identity.address().toInt());
+				if (!networkExists)
+					return 404;
 
-	// Create Node record or do full identity check if we already have one
+				if ((path.size() == 4)&&(path[2] == "member")&&(path[3].length() == 10)) {
+					uint64_t address = Utils::hexStrToU64(path[3].c_str());
+					char addrs[24];
+					Utils::snprintf(addrs,sizeof(addrs),"%.10llx",address);
 
-	sqlite3_reset(_sGetNodeIdentity);
-	sqlite3_bind_text(_sGetNodeIdentity,1,member.nodeId,10,SQLITE_STATIC);
-	if (sqlite3_step(_sGetNodeIdentity) == SQLITE_ROW) {
-		try {
-			Identity alreadyKnownIdentity((const char *)sqlite3_column_text(_sGetNodeIdentity,0));
-			if (alreadyKnownIdentity != identity)
-				return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
-		} catch ( ... ) { // identity stored in database is not valid or is NULL
-			return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
-		}
-	} else {
-		std::string idstr(identity.toString(false));
-		sqlite3_reset(_sCreateOrReplaceNode);
-		sqlite3_bind_text(_sCreateOrReplaceNode,1,member.nodeId,10,SQLITE_STATIC);
-		sqlite3_bind_text(_sCreateOrReplaceNode,2,idstr.c_str(),-1,SQLITE_STATIC);
-		if (sqlite3_step(_sCreateOrReplaceNode) != SQLITE_DONE) {
-			netconf["error"] = "unable to create new Node record";
-			return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
-		}
-	}
+					int64_t addToNetworkRevision = 0;
 
-	// Fetch Network record
+					int64_t memberRowId = 0;
+					sqlite3_reset(_sGetMember);
+					sqlite3_bind_text(_sGetMember,1,nwids,16,SQLITE_STATIC);
+					sqlite3_bind_text(_sGetMember,2,addrs,10,SQLITE_STATIC);
+					bool memberExists = false;
+					if (sqlite3_step(_sGetMember) == SQLITE_ROW) {
+						memberExists = true;
+						memberRowId = sqlite3_column_int64(_sGetMember,0);
+					}
 
-	sqlite3_reset(_sGetNetworkById);
-	sqlite3_bind_text(_sGetNetworkById,1,network.id,16,SQLITE_STATIC);
-	if (sqlite3_step(_sGetNetworkById) == SQLITE_ROW) {
-		network.name = (const char *)sqlite3_column_text(_sGetNetworkById,0);
-		network.isPrivate = (sqlite3_column_int(_sGetNetworkById,1) > 0);
-		network.enableBroadcast = (sqlite3_column_int(_sGetNetworkById,2) > 0);
-		network.allowPassiveBridging = (sqlite3_column_int(_sGetNetworkById,3) > 0);
-		network.v4AssignMode = (const char *)sqlite3_column_text(_sGetNetworkById,4);
-		network.v6AssignMode = (const char *)sqlite3_column_text(_sGetNetworkById,5);
-		network.multicastLimit = sqlite3_column_int(_sGetNetworkById,6);
-		network.creationTime = (uint64_t)sqlite3_column_int64(_sGetNetworkById,7);
-		network.revision = (uint64_t)sqlite3_column_int64(_sGetNetworkById,8);
-		network.memberRevisionCounter = (uint64_t)sqlite3_column_int64(_sGetNetworkById,9);
-	} else {
-		return NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND;
-	}
+					if (!memberExists) {
+						sqlite3_reset(_sCreateMember);
+						sqlite3_bind_text(_sCreateMember,1,nwids,16,SQLITE_STATIC);
+						sqlite3_bind_text(_sCreateMember,2,addrs,10,SQLITE_STATIC);
+						sqlite3_bind_int(_sCreateMember,3,0);
+						sqlite3_bind_text(_sCreateMember,4,nwids,16,SQLITE_STATIC);
+						if (sqlite3_step(_sCreateMember) != SQLITE_DONE)
+							return 500;
+						memberRowId = (int64_t)sqlite3_last_insert_rowid(_db);
 
-	// Fetch Member record
+						sqlite3_reset(_sIncrementMemberRevisionCounter);
+						sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,nwids,16,SQLITE_STATIC);
+						sqlite3_step(_sIncrementMemberRevisionCounter);
+						addToNetworkRevision = 1;
+					}
 
-	bool foundMember = false;
-	sqlite3_reset(_sGetMember);
-	sqlite3_bind_text(_sGetMember,1,network.id,16,SQLITE_STATIC);
-	sqlite3_bind_text(_sGetMember,2,member.nodeId,10,SQLITE_STATIC);
-	if (sqlite3_step(_sGetMember) == SQLITE_ROW) {
-		foundMember = true;
-		member.rowid = (int64_t)sqlite3_column_int64(_sGetMember,0);
-		member.authorized = (sqlite3_column_int(_sGetMember,1) > 0);
-		member.activeBridge = (sqlite3_column_int(_sGetMember,2) > 0);
-	}
+					json_value *j = json_parse(body.c_str(),body.length());
+					if (j) {
+						if (j->type == json_object) {
+							for(unsigned int k=0;k<j->u.object.length;++k) {
 
-	// Create Member record for unknown nodes, auto-authorizing if network is public
+								if (!strcmp(j->u.object.values[k].name,"authorized")) {
+									if (j->u.object.values[k].value->type == json_boolean) {
+										sqlite3_reset(_sUpdateMemberAuthorized);
+										sqlite3_bind_int(_sUpdateMemberAuthorized,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
+										sqlite3_bind_text(_sUpdateMemberAuthorized,2,nwids,16,SQLITE_STATIC);
+										sqlite3_bind_int64(_sUpdateMemberAuthorized,3,memberRowId);
+										if (sqlite3_step(_sUpdateMemberAuthorized) != SQLITE_DONE)
+											return 500;
 
-	if (!foundMember) {
-		member.authorized = (network.isPrivate ? false : true);
-		member.activeBridge = false;
-		sqlite3_reset(_sCreateMember);
-		sqlite3_bind_text(_sCreateMember,1,network.id,16,SQLITE_STATIC);
-		sqlite3_bind_text(_sCreateMember,2,member.nodeId,10,SQLITE_STATIC);
-		sqlite3_bind_int(_sCreateMember,3,(member.authorized ? 1 : 0));
-		sqlite3_bind_text(_sCreateMember,4,network.id,16,SQLITE_STATIC);
-		if (sqlite3_step(_sCreateMember) != SQLITE_DONE) {
-			netconf["error"] = "unable to create new member record";
-			return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
-		}
-		member.rowid = (int64_t)sqlite3_last_insert_rowid(_db);
+										sqlite3_reset(_sIncrementMemberRevisionCounter);
+										sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,nwids,16,SQLITE_STATIC);
+										sqlite3_step(_sIncrementMemberRevisionCounter);
+										addToNetworkRevision = 1;
+									}
+								} else if (!strcmp(j->u.object.values[k].name,"activeBridge")) {
+									if (j->u.object.values[k].value->type == json_boolean) {
+										sqlite3_reset(_sUpdateMemberActiveBridge);
+										sqlite3_bind_int(_sUpdateMemberActiveBridge,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
+										sqlite3_bind_text(_sUpdateMemberActiveBridge,2,nwids,16,SQLITE_STATIC);
+										sqlite3_bind_int64(_sUpdateMemberActiveBridge,3,memberRowId);
+										if (sqlite3_step(_sUpdateMemberActiveBridge) != SQLITE_DONE)
+											return 500;
 
-		sqlite3_reset(_sIncrementMemberRevisionCounter);
-		sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,network.id,16,SQLITE_STATIC);
-		sqlite3_step(_sIncrementMemberRevisionCounter);
-	}
+										sqlite3_reset(_sIncrementMemberRevisionCounter);
+										sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,nwids,16,SQLITE_STATIC);
+										sqlite3_step(_sIncrementMemberRevisionCounter);
+										addToNetworkRevision = 1;
+									}
+								} else if (!strcmp(j->u.object.values[k].name,"ipAssignments")) {
+									if (j->u.object.values[k].value->type == json_array) {
+										sqlite3_reset(_sDeleteIpAllocations);
+										sqlite3_bind_text(_sDeleteIpAllocations,1,nwids,16,SQLITE_STATIC);
+										sqlite3_bind_text(_sDeleteIpAllocations,2,addrs,10,SQLITE_STATIC);
+										sqlite3_bind_int(_sDeleteIpAllocations,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
+										if (sqlite3_step(_sDeleteIpAllocations) != SQLITE_DONE)
+											return 500;
+										for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
+											json_value *ipalloc = j->u.object.values[k].value->u.array.values[kk];
+											if (ipalloc->type == json_string) {
+												InetAddress a(ipalloc->u.string.ptr);
+												char ipBlob[16];
+												int ipVersion = 0;
+												switch(a.ss_family) {
+													case AF_INET:
+														if ((a.netmaskBits() > 0)&&(a.netmaskBits() <= 32)) {
+															memset(ipBlob,0,12);
+															memcpy(ipBlob + 12,a.rawIpData(),4);
+															ipVersion = 4;
+														}
+														break;
+													case AF_INET6:
+														if ((a.netmaskBits() > 0)&&(a.netmaskBits() <= 128)) {
+															memcpy(ipBlob,a.rawIpData(),16);
+															ipVersion = 6;
+														}
+														break;
+												}
+												if (ipVersion > 0) {
+													sqlite3_reset(_sAllocateIp);
+													sqlite3_bind_text(_sAllocateIp,1,nwids,16,SQLITE_STATIC);
+													sqlite3_bind_text(_sAllocateIp,2,addrs,10,SQLITE_STATIC);
+													sqlite3_bind_int(_sAllocateIp,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
+													sqlite3_bind_blob(_sAllocateIp,4,(const void *)ipBlob,16,SQLITE_STATIC);
+													sqlite3_bind_int(_sAllocateIp,5,(int)a.netmaskBits());
+													sqlite3_bind_int(_sAllocateIp,6,ipVersion);
+													if (sqlite3_step(_sAllocateIp) != SQLITE_DONE)
+														return 500;
+												}
+											}
+										}
+										addToNetworkRevision = 1;
+									}
+								} else if (!strcmp(j->u.object.values[k].name,"identity")) {
+									// Identity is technically an immutable field, but if the member's Node has
+									// no identity we allow it to be populated. This is primarily for migrating
+									// node data from another controller.
+									json_value *idstr = j->u.object.values[k].value;
+									if (idstr->type == json_string) {
+										bool alreadyHaveIdentity = false;
 
-	// Add log entry
-	{
-		char ver[16];
-		std::string fa;
-		if (fromAddr) {
-			fa = fromAddr.toString();
-			if (fa.length() > 64)
-				fa = fa.substr(0,64);
-		}
-		sqlite3_reset(_sPutLog);
-		sqlite3_bind_text(_sPutLog,1,network.id,16,SQLITE_STATIC);
-		sqlite3_bind_text(_sPutLog,2,member.nodeId,10,SQLITE_STATIC);
-		sqlite3_bind_int64(_sPutLog,3,(long long)OSUtils::now());
-		sqlite3_bind_int(_sPutLog,4,member.authorized ? 1 : 0);
-		if ((clientMajorVersion > 0)||(clientMinorVersion > 0)||(clientRevision > 0)) {
-			Utils::snprintf(ver,sizeof(ver),"%u.%u.%u",clientMajorVersion,clientMinorVersion,clientRevision);
-			sqlite3_bind_text(_sPutLog,5,ver,-1,SQLITE_STATIC);
-		} else sqlite3_bind_null(_sPutLog,5);
-		if (fa.length() > 0)
-			sqlite3_bind_text(_sPutLog,6,fa.c_str(),-1,SQLITE_STATIC);
-		else sqlite3_bind_null(_sPutLog,6);
-		sqlite3_step(_sPutLog);
-	}
-
-	// Check member authorization
+										sqlite3_reset(_sGetNodeIdentity);
+										sqlite3_bind_text(_sGetNodeIdentity,1,addrs,10,SQLITE_STATIC);
+										if (sqlite3_step(_sGetNodeIdentity) == SQLITE_ROW) {
+											const char *tmp2 = (const char *)sqlite3_column_text(_sGetNodeIdentity,0);
+											if ((tmp2)&&(tmp2[0]))
+												alreadyHaveIdentity = true;
+										}
 
-	if (!member.authorized)
-		return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
+										if (!alreadyHaveIdentity) {
+											try {
+												Identity id2(idstr->u.string.ptr);
+												if (id2) {
+													std::string idstr2(id2.toString(false)); // object must persist until after sqlite3_step() for SQLITE_STATIC
+													sqlite3_reset(_sCreateOrReplaceNode);
+													sqlite3_bind_text(_sCreateOrReplaceNode,1,addrs,10,SQLITE_STATIC);
+													sqlite3_bind_text(_sCreateOrReplaceNode,2,idstr2.c_str(),-1,SQLITE_STATIC);
+													sqlite3_step(_sCreateOrReplaceNode);
+												}
+											} catch ( ... ) {} // ignore invalid identities
+										}
+									}
+								}
 
-	// Create and sign netconf
+							}
+						}
+						json_value_free(j);
+					}
 
-	netconf.clear();
-	{
-		char tss[24],rs[24];
-		Utils::snprintf(tss,sizeof(tss),"%.16llx",(unsigned long long)OSUtils::now());
-		Utils::snprintf(rs,sizeof(rs),"%.16llx",(unsigned long long)network.revision);
-		netconf[ZT_NETWORKCONFIG_DICT_KEY_TIMESTAMP] = tss;
-		netconf[ZT_NETWORKCONFIG_DICT_KEY_REVISION] = rs;
-		netconf[ZT_NETWORKCONFIG_DICT_KEY_NETWORK_ID] = network.id;
-		netconf[ZT_NETWORKCONFIG_DICT_KEY_ISSUED_TO] = member.nodeId;
-		netconf[ZT_NETWORKCONFIG_DICT_KEY_PRIVATE] = network.isPrivate ? "1" : "0";
-		netconf[ZT_NETWORKCONFIG_DICT_KEY_NAME] = (network.name) ? network.name : "";
-		netconf[ZT_NETWORKCONFIG_DICT_KEY_ENABLE_BROADCAST] = network.enableBroadcast ? "1" : "0";
-		netconf[ZT_NETWORKCONFIG_DICT_KEY_ALLOW_PASSIVE_BRIDGING] = network.allowPassiveBridging ? "1" : "0";
+					if ((addToNetworkRevision > 0)&&(revision > 0)) {
+						sqlite3_reset(_sSetNetworkRevision);
+						sqlite3_bind_int64(_sSetNetworkRevision,1,revision + addToNetworkRevision);
+						sqlite3_bind_text(_sSetNetworkRevision,2,nwids,16,SQLITE_STATIC);
+						sqlite3_step(_sSetNetworkRevision);
+					}
 
-		{	// TODO: right now only etherTypes are supported in rules
-			std::vector<int> allowedEtherTypes;
-			sqlite3_reset(_sGetEtherTypesFromRuleTable);
-			sqlite3_bind_text(_sGetEtherTypesFromRuleTable,1,network.id,16,SQLITE_STATIC);
-			while (sqlite3_step(_sGetEtherTypesFromRuleTable) == SQLITE_ROW) {
-				if (sqlite3_column_type(_sGetEtherTypesFromRuleTable,0) == SQLITE_NULL) {
-					allowedEtherTypes.clear();
-					allowedEtherTypes.push_back(0); // NULL 'allow' matches ANY
-					break;
-				} else {
-					int et = sqlite3_column_int(_sGetEtherTypesFromRuleTable,0);
-					if ((et >= 0)&&(et <= 0xffff))
-						allowedEtherTypes.push_back(et);
-				}
-			}
-			std::sort(allowedEtherTypes.begin(),allowedEtherTypes.end());
-			allowedEtherTypes.erase(std::unique(allowedEtherTypes.begin(),allowedEtherTypes.end()),allowedEtherTypes.end());
-			std::string allowedEtherTypesCsv;
-			for(std::vector<int>::const_iterator i(allowedEtherTypes.begin());i!=allowedEtherTypes.end();++i) {
-				if (allowedEtherTypesCsv.length())
-					allowedEtherTypesCsv.push_back(',');
-				char tmp[16];
-				Utils::snprintf(tmp,sizeof(tmp),"%.4x",(unsigned int)*i);
-				allowedEtherTypesCsv.append(tmp);
-			}
-			netconf[ZT_NETWORKCONFIG_DICT_KEY_ALLOWED_ETHERNET_TYPES] = allowedEtherTypesCsv;
-		}
+					return _doCPGet(path,urlArgs,headers,body,responseBody,responseContentType);
+				} // else 404
 
-		if (network.multicastLimit > 0) {
-			char ml[16];
-			Utils::snprintf(ml,sizeof(ml),"%lx",(unsigned long)network.multicastLimit);
-			netconf[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_LIMIT] = ml;
-		}
+			} else {
+				std::vector<std::string> path_copy(path);
 
-		{
-			std::string activeBridges;
-			sqlite3_reset(_sGetActiveBridges);
-			sqlite3_bind_text(_sGetActiveBridges,1,network.id,16,SQLITE_STATIC);
-			while (sqlite3_step(_sGetActiveBridges) == SQLITE_ROW) {
-				const char *ab = (const char *)sqlite3_column_text(_sGetActiveBridges,0);
-				if ((ab)&&(strlen(ab) == 10)) {
-					if (activeBridges.length())
-						activeBridges.push_back(',');
-					activeBridges.append(ab);
-				}
-				if (activeBridges.length() > 1024) // sanity check -- you can't have too many active bridges at the moment
-					break;
-			}
-			if (activeBridges.length())
-				netconf[ZT_NETWORKCONFIG_DICT_KEY_ACTIVE_BRIDGES] = activeBridges;
-		}
+				if (!networkExists) {
+					if (path[1].substr(10) == "______") {
+						// A special POST /network/##########______ feature lets users create a network
+						// with an arbitrary unused network number at this controller.
+						nwid = 0;
 
-		{
-			std::string relays;
-			sqlite3_reset(_sGetRelays);
-			sqlite3_bind_text(_sGetRelays,1,network.id,16,SQLITE_STATIC);
-			while (sqlite3_step(_sGetRelays) == SQLITE_ROW) {
-				const char *n = (const char *)sqlite3_column_text(_sGetRelays,0);
-				const char *a = (const char *)sqlite3_column_text(_sGetRelays,1);
-				if ((n)&&(a)) {
-					Address node(n);
-					InetAddress addr(a);
-					if ((node)&&(addr)) {
-						if (relays.length())
-							relays.push_back(',');
-						relays.append(node.toString());
-						relays.push_back(';');
-						relays.append(addr.toString());
-					}
-				}
-			}
-			if (relays.length())
-				netconf[ZT_NETWORKCONFIG_DICT_KEY_RELAYS] = relays;
-		}
+						uint64_t nwidPrefix = (Utils::hexStrToU64(path[1].substr(0,10).c_str()) << 24) & 0xffffffffff000000ULL;
+						uint64_t nwidPostfix = 0;
+						Utils::getSecureRandom(&nwidPostfix,sizeof(nwidPostfix));
+						uint64_t nwidOriginalPostfix = nwidPostfix;
+						do {
+							uint64_t tryNwid = nwidPrefix | (nwidPostfix & 0xffffffULL);
+							if (!nwidPostfix)
+								tryNwid |= 1;
+							Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)tryNwid);
 
-		{
-			char tmp[128];
-			std::string gateways;
-			sqlite3_reset(_sGetGateways);
-			sqlite3_bind_text(_sGetGateways,1,network.id,16,SQLITE_STATIC);
-			while (sqlite3_step(_sGetGateways) == SQLITE_ROW) {
-				const unsigned char *ip = (const unsigned char *)sqlite3_column_blob(_sGetGateways,0);
-				switch(sqlite3_column_int(_sGetGateways,1)) { // ipVersion
-					case 4:
-						Utils::snprintf(tmp,sizeof(tmp),"%s%d.%d.%d.%d/%d",
-							(gateways.length() > 0) ? "," : "",
-							(int)ip[12],
-							(int)ip[13],
-							(int)ip[14],
-							(int)ip[15],
-							(int)sqlite3_column_int(_sGetGateways,2)); // metric
-						gateways.append(tmp);
-						break;
-					case 6:
-						Utils::snprintf(tmp,sizeof(tmp),"%s%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x/%d",
-							(gateways.length() > 0) ? "," : "",
-							(int)ip[0],
-							(int)ip[1],
-							(int)ip[2],
-							(int)ip[3],
-							(int)ip[4],
-							(int)ip[5],
-							(int)ip[6],
-							(int)ip[7],
-							(int)ip[8],
-							(int)ip[9],
-							(int)ip[10],
-							(int)ip[11],
-							(int)ip[12],
-							(int)ip[13],
-							(int)ip[14],
-							(int)ip[15],
-							(int)sqlite3_column_int(_sGetGateways,2)); // metric
-						gateways.append(tmp);
-						break;
-				}
-			}
-			if (gateways.length())
-				netconf[ZT_NETWORKCONFIG_DICT_KEY_GATEWAYS] = gateways;
-		}
+							sqlite3_reset(_sGetNetworkRevision);
+							sqlite3_bind_text(_sGetNetworkRevision,1,nwids,16,SQLITE_STATIC);
+							if (sqlite3_step(_sGetNetworkRevision) != SQLITE_ROW) {
+								nwid = tryNwid;
+								break;
+							}
 
-		if ((network.v4AssignMode)&&(!strcmp(network.v4AssignMode,"zt"))) {
-			std::string v4s;
+							++nwidPostfix;
+						} while (nwidPostfix != nwidOriginalPostfix);
 
-			// Get existing IPv4 IP assignments and network routes -- keep routes in a
-			// vector for use in auto-assign if we need them.
-			std::vector< std::pair<uint32_t,int> > routedNetworks;
-			bool haveStaticIpAssignment = false;
-			sqlite3_reset(_sGetIpAssignmentsForNode);
-			sqlite3_bind_text(_sGetIpAssignmentsForNode,1,network.id,16,SQLITE_STATIC);
-			sqlite3_bind_text(_sGetIpAssignmentsForNode,2,member.nodeId,10,SQLITE_STATIC);
-			sqlite3_bind_int(_sGetIpAssignmentsForNode,3,4); // 4 == IPv4
-			while (sqlite3_step(_sGetIpAssignmentsForNode) == SQLITE_ROW) {
-				const unsigned char *ip = (const unsigned char *)sqlite3_column_blob(_sGetIpAssignmentsForNode,1);
-				if ((!ip)||(sqlite3_column_bytes(_sGetIpAssignmentsForNode,1) != 16))
-					continue;
-				int ipNetmaskBits = sqlite3_column_int(_sGetIpAssignmentsForNode,2);
-				if ((ipNetmaskBits <= 0)||(ipNetmaskBits > 32))
-					continue;
+						// 503 means we have no more free IDs for this prefix. You shouldn't host anywhere
+						// near 16 million networks on the same controller, so shouldn't happen.
+						if (!nwid)
+							return 503;
+					}
 
-				const IpAssignmentType ipt = (IpAssignmentType)sqlite3_column_int(_sGetIpAssignmentsForNode,0);
-				switch(ipt) {
-					case ZT_IP_ASSIGNMENT_TYPE_ADDRESS:
-						haveStaticIpAssignment = true;
-						break;
-					case ZT_IP_ASSIGNMENT_TYPE_NETWORK:
-						routedNetworks.push_back(std::pair<uint32_t,int>(Utils::ntoh(*(reinterpret_cast<const uint32_t *>(ip + 12))),ipNetmaskBits));
-						break;
-					default:
-						continue;
+					sqlite3_reset(_sCreateNetwork);
+					sqlite3_bind_text(_sCreateNetwork,1,nwids,16,SQLITE_STATIC);
+					sqlite3_bind_text(_sCreateNetwork,2,"",0,SQLITE_STATIC);
+					sqlite3_bind_int64(_sCreateNetwork,3,(long long)OSUtils::now());
+					if (sqlite3_step(_sCreateNetwork) != SQLITE_DONE)
+						return 500;
+					path_copy[1].assign(nwids);
 				}
 
-				// 1.0.4 or newer clients support network routes in addition to IPs.
-				// Older clients only support IP address / netmask entries.
-				if ((clientIs104)||(ipt == ZT_IP_ASSIGNMENT_TYPE_ADDRESS)) {
-					char tmp[32];
-					Utils::snprintf(tmp,sizeof(tmp),"%d.%d.%d.%d/%d",(int)ip[12],(int)ip[13],(int)ip[14],(int)ip[15],ipNetmaskBits);
-					if (v4s.length())
-						v4s.push_back(',');
-					v4s.append(tmp);
-				}
-			}
+				json_value *j = json_parse(body.c_str(),body.length());
+				if (j) {
+					if (j->type == json_object) {
+						for(unsigned int k=0;k<j->u.object.length;++k) {
+							sqlite3_stmt *stmt = (sqlite3_stmt *)0;
 
-			if (!haveStaticIpAssignment) {
-				// Attempt to auto-assign an IPv4 address from an available routed pool
-				sqlite3_reset(_sGetIpAssignmentPools);
-				sqlite3_bind_text(_sGetIpAssignmentPools,1,network.id,16,SQLITE_STATIC);
-				sqlite3_bind_int(_sGetIpAssignmentPools,2,4); // 4 == IPv4
-
-				while (sqlite3_step(_sGetIpAssignmentPools) == SQLITE_ROW) {
-					const unsigned char *ipRangeStartB = reinterpret_cast<const unsigned char *>(sqlite3_column_blob(_sGetIpAssignmentPools,0));
-					const unsigned char *ipRangeEndB = reinterpret_cast<const unsigned char *>(sqlite3_column_blob(_sGetIpAssignmentPools,1));
-					if ((!ipRangeStartB)||(!ipRangeEndB)||(sqlite3_column_bytes(_sGetIpAssignmentPools,0) != 16)||(sqlite3_column_bytes(_sGetIpAssignmentPools,1) != 16))
-						continue;
-
-					uint32_t ipRangeStart = Utils::ntoh(*(reinterpret_cast<const uint32_t *>(ipRangeStartB + 12)));
-					uint32_t ipRangeEnd = Utils::ntoh(*(reinterpret_cast<const uint32_t *>(ipRangeEndB + 12)));
-					if (ipRangeEnd < ipRangeStart)
-						continue;
-					uint32_t ipRangeLen = ipRangeEnd - ipRangeStart;
-
-					// Start with the LSB of the member's address
-					uint32_t ipTrialCounter = (uint32_t)(identity.address().toInt() & 0xffffffff);
+							if (!strcmp(j->u.object.values[k].name,"name")) {
+								if ((j->u.object.values[k].value->type == json_string)&&(j->u.object.values[k].value->u.string.ptr[0])) {
+									if (sqlite3_prepare_v2(_db,"UPDATE Network SET \"name\" = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
+										sqlite3_bind_text(stmt,1,j->u.object.values[k].value->u.string.ptr,-1,SQLITE_STATIC);
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"private")) {
+								if (j->u.object.values[k].value->type == json_boolean) {
+									if (sqlite3_prepare_v2(_db,"UPDATE Network SET \"private\" = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
+										sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"enableBroadcast")) {
+								if (j->u.object.values[k].value->type == json_boolean) {
+									if (sqlite3_prepare_v2(_db,"UPDATE Network SET enableBroadcast = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
+										sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"allowPassiveBridging")) {
+								if (j->u.object.values[k].value->type == json_boolean) {
+									if (sqlite3_prepare_v2(_db,"UPDATE Network SET allowPassiveBridging = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
+										sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"v4AssignMode")) {
+								if (j->u.object.values[k].value->type == json_string) {
+									if (sqlite3_prepare_v2(_db,"UPDATE Network SET v4AssignMode = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
+										sqlite3_bind_text(stmt,1,j->u.object.values[k].value->u.string.ptr,-1,SQLITE_STATIC);
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"v6AssignMode")) {
+								if (j->u.object.values[k].value->type == json_string) {
+									if (sqlite3_prepare_v2(_db,"UPDATE Network SET v6AssignMode = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
+										sqlite3_bind_text(stmt,1,j->u.object.values[k].value->u.string.ptr,-1,SQLITE_STATIC);
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"multicastLimit")) {
+								if (j->u.object.values[k].value->type == json_integer) {
+									if (sqlite3_prepare_v2(_db,"UPDATE Network SET multicastLimit = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
+										sqlite3_bind_int(stmt,1,(int)j->u.object.values[k].value->u.integer);
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"relays")) {
+								if (j->u.object.values[k].value->type == json_array) {
+									std::map<Address,InetAddress> nodeIdToPhyAddress;
+									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
+										json_value *relay = j->u.object.values[k].value->u.array.values[kk];
+										const char *address = (const char *)0;
+										const char *phyAddress = (const char *)0;
+										if ((relay)&&(relay->type == json_object)) {
+											for(unsigned int rk=0;rk<relay->u.object.length;++rk) {
+												if ((!strcmp(relay->u.object.values[rk].name,"address"))&&(relay->u.object.values[rk].value->type == json_string))
+													address = relay->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(relay->u.object.values[rk].name,"phyAddress"))&&(relay->u.object.values[rk].value->type == json_string))
+													phyAddress = relay->u.object.values[rk].value->u.string.ptr;
+											}
+										}
+										if ((address)&&(phyAddress))
+											nodeIdToPhyAddress[Address(address)] = InetAddress(phyAddress);
+									}
 
-					for(uint32_t k=ipRangeStart,l=0;(k<=ipRangeEnd)&&(l < 1000000);++k,++l) {
-						uint32_t ip = (ipRangeLen > 0) ? (ipRangeStart + (ipTrialCounter % ipRangeLen)) : ipRangeStart;
-						++ipTrialCounter;
+									sqlite3_reset(_sDeleteRelaysForNetwork);
+									sqlite3_bind_text(_sDeleteRelaysForNetwork,1,nwids,16,SQLITE_STATIC);
+									sqlite3_step(_sDeleteRelaysForNetwork);
 
-						for(std::vector< std::pair<uint32_t,int> >::const_iterator r(routedNetworks.begin());r!=routedNetworks.end();++r) {
-							if ((ip & (0xffffffff << (32 - r->second))) == r->first) {
-								// IP is included in a routed network, so check if it's allocated
+									for(std::map<Address,InetAddress>::iterator rl(nodeIdToPhyAddress.begin());rl!=nodeIdToPhyAddress.end();++rl) {
+										sqlite3_reset(_sCreateRelay);
+										sqlite3_bind_text(_sCreateRelay,1,nwids,16,SQLITE_STATIC);
+										std::string a(rl->first.toString()),b(rl->second.toString()); // don't destroy strings until sqlite3_step()
+										sqlite3_bind_text(_sCreateRelay,2,a.c_str(),-1,SQLITE_STATIC);
+										sqlite3_bind_text(_sCreateRelay,3,b.c_str(),-1,SQLITE_STATIC);
+										sqlite3_step(_sCreateRelay);
+									}
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"gateways")) {
+								sqlite3_reset(_sDeleteGateways);
+								sqlite3_bind_text(_sDeleteGateways,1,nwids,16,SQLITE_STATIC);
+								sqlite3_step(_sDeleteGateways);
+								if (j->u.object.values[k].value->type == json_array) {
+									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
+										json_value *gateway = j->u.object.values[k].value->u.array.values[kk];
+										if ((gateway)&&(gateway->type == json_string)) {
+											InetAddress gwip(gateway->u.string.ptr);
+											sqlite3_reset(_sCreateGateway);
+											sqlite3_bind_text(_sCreateGateway,1,nwids,16,SQLITE_STATIC);
+											sqlite3_bind_int(_sCreateGateway,4,(int)gwip.metric());
+											if (gwip.ss_family == AF_INET) {
+												char ipBlob[16];
+												memset(ipBlob,0,12);
+												memcpy(ipBlob + 12,gwip.rawIpData(),4);
+												sqlite3_bind_blob(_sCreateGateway,2,(const void *)ipBlob,16,SQLITE_STATIC);
+												sqlite3_bind_int(_sCreateGateway,3,4);
+												sqlite3_step(_sCreateGateway);
+											} else if (gwip.ss_family == AF_INET6) {
+												sqlite3_bind_blob(_sCreateGateway,2,gwip.rawIpData(),16,SQLITE_STATIC);
+												sqlite3_bind_int(_sCreateGateway,3,6);
+												sqlite3_step(_sCreateGateway);
+											}
+										}
+									}
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"ipLocalRoutes")) {
+								sqlite3_reset(_sDeleteLocalRoutes);
+								sqlite3_bind_text(_sDeleteLocalRoutes,1,nwids,16,SQLITE_STATIC);
+								sqlite3_bind_int(_sDeleteLocalRoutes,2,(int)ZT_IP_ASSIGNMENT_TYPE_NETWORK);
+								sqlite3_step(_sDeleteLocalRoutes);
+								if (j->u.object.values[k].value->type == json_array) {
+									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
+										json_value *localRoute = j->u.object.values[k].value->u.array.values[kk];
+										if ((localRoute)&&(localRoute->type == json_string)) {
+											InetAddress lr(localRoute->u.string.ptr);
+											if (lr.ss_family == AF_INET) {
+												char ipBlob[16];
+												memset(ipBlob,0,12);
+												memcpy(ipBlob + 12,lr.rawIpData(),4);
+												sqlite3_reset(_sAllocateIp);
+												sqlite3_bind_text(_sAllocateIp,1,nwids,16,SQLITE_STATIC);
+												sqlite3_bind_null(_sAllocateIp,2);
+												sqlite3_bind_int(_sAllocateIp,3,(int)ZT_IP_ASSIGNMENT_TYPE_NETWORK);
+												sqlite3_bind_blob(_sAllocateIp,4,(const void *)ipBlob,16,SQLITE_STATIC);
+												sqlite3_bind_int(_sAllocateIp,5,lr.netmaskBits());
+												sqlite3_bind_int(_sAllocateIp,6,4);
+												sqlite3_step(_sAllocateIp);
+											} else if (lr.ss_family == AF_INET6) {
+												sqlite3_reset(_sAllocateIp);
+												sqlite3_bind_text(_sAllocateIp,1,nwids,16,SQLITE_STATIC);
+												sqlite3_bind_null(_sAllocateIp,2);
+												sqlite3_bind_int(_sAllocateIp,3,(int)ZT_IP_ASSIGNMENT_TYPE_NETWORK);
+												sqlite3_bind_blob(_sAllocateIp,4,lr.rawIpData(),16,SQLITE_STATIC);
+												sqlite3_bind_int(_sAllocateIp,5,lr.netmaskBits());
+												sqlite3_bind_int(_sAllocateIp,6,6);
+												sqlite3_step(_sAllocateIp);
+											}
+										}
+									}
+								}
+							} else if (!strcmp(j->u.object.values[k].name,"ipAssignmentPools")) {
+								if (j->u.object.values[k].value->type == json_array) {
+									std::vector< std::pair<InetAddress,InetAddress> > pools;
+									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
+										json_value *pool = j->u.object.values[k].value->u.array.values[kk];
+										const char *iprs = (const char *)0;
+										const char *ipre = (const char *)0;
+										if ((pool)&&(pool->type == json_object)) {
+											for(unsigned int rk=0;rk<pool->u.object.length;++rk) {
+												if ((!strcmp(pool->u.object.values[rk].name,"ipRangeStart"))&&(pool->u.object.values[rk].value->type == json_string))
+													iprs = pool->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(pool->u.object.values[rk].name,"ipRangeEnd"))&&(pool->u.object.values[rk].value->type == json_string))
+													ipre = pool->u.object.values[rk].value->u.string.ptr;
+											}
+										}
+										if ((iprs)&&(ipre)) {
+											InetAddress iprs2(iprs);
+											InetAddress ipre2(ipre);
+											if (iprs2.ss_family == ipre2.ss_family) {
+												iprs2.setPort(0);
+												ipre2.setPort(0);
+												pools.push_back(std::pair<InetAddress,InetAddress>(iprs2,ipre2));
+											}
+										}
+									}
+									std::sort(pools.begin(),pools.end());
+									pools.erase(std::unique(pools.begin(),pools.end()),pools.end());
 
-								uint32_t ipBlob[4];
-								ipBlob[0] = 0; ipBlob[1] = 0; ipBlob[2] = 0; ipBlob[3] = Utils::hton(ip);
+									sqlite3_reset(_sDeleteIpAssignmentPoolsForNetwork);
+									sqlite3_bind_text(_sDeleteIpAssignmentPoolsForNetwork,1,nwids,16,SQLITE_STATIC);
+									sqlite3_step(_sDeleteIpAssignmentPoolsForNetwork);
 
-								sqlite3_reset(_sCheckIfIpIsAllocated);
-								sqlite3_bind_text(_sCheckIfIpIsAllocated,1,network.id,16,SQLITE_STATIC);
-								sqlite3_bind_blob(_sCheckIfIpIsAllocated,2,(const void *)ipBlob,16,SQLITE_STATIC);
-								sqlite3_bind_int(_sCheckIfIpIsAllocated,3,4); // 4 == IPv4
-								sqlite3_bind_int(_sCheckIfIpIsAllocated,4,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
-								if (sqlite3_step(_sCheckIfIpIsAllocated) != SQLITE_ROW) {
-									// No rows returned, so the IP is available
-									sqlite3_reset(_sAllocateIp);
-									sqlite3_bind_text(_sAllocateIp,1,network.id,16,SQLITE_STATIC);
-									sqlite3_bind_text(_sAllocateIp,2,member.nodeId,10,SQLITE_STATIC);
-									sqlite3_bind_int(_sAllocateIp,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
-									sqlite3_bind_blob(_sAllocateIp,4,(const void *)ipBlob,16,SQLITE_STATIC);
-									sqlite3_bind_int(_sAllocateIp,5,r->second); // IP netmask bits from matching route
-									sqlite3_bind_int(_sAllocateIp,6,4); // 4 == IPv4
-									if (sqlite3_step(_sAllocateIp) == SQLITE_DONE) {
-										char tmp[32];
-										Utils::snprintf(tmp,sizeof(tmp),"%d.%d.%d.%d/%d",(int)((ip >> 24) & 0xff),(int)((ip >> 16) & 0xff),(int)((ip >> 8) & 0xff),(int)(ip & 0xff),r->second);
-										if (v4s.length())
-											v4s.push_back(',');
-										v4s.append(tmp);
-										haveStaticIpAssignment = true; // break outer loop
+									for(std::vector< std::pair<InetAddress,InetAddress> >::const_iterator p(pools.begin());p!=pools.end();++p) {
+										char ipBlob1[16],ipBlob2[16];
+										sqlite3_reset(_sCreateIpAssignmentPool);
+										sqlite3_bind_text(_sCreateIpAssignmentPool,1,nwids,16,SQLITE_STATIC);
+										if (p->first.ss_family == AF_INET) {
+											memset(ipBlob1,0,12);
+											memcpy(ipBlob1 + 12,p->first.rawIpData(),4);
+											memset(ipBlob2,0,12);
+											memcpy(ipBlob2 + 12,p->second.rawIpData(),4);
+											sqlite3_bind_blob(_sCreateIpAssignmentPool,2,(const void *)ipBlob1,16,SQLITE_STATIC);
+											sqlite3_bind_blob(_sCreateIpAssignmentPool,3,(const void *)ipBlob2,16,SQLITE_STATIC);
+											sqlite3_bind_int(_sCreateIpAssignmentPool,4,4);
+										} else if (p->first.ss_family == AF_INET6) {
+											sqlite3_bind_blob(_sCreateIpAssignmentPool,2,p->first.rawIpData(),16,SQLITE_STATIC);
+											sqlite3_bind_blob(_sCreateIpAssignmentPool,3,p->second.rawIpData(),16,SQLITE_STATIC);
+											sqlite3_bind_int(_sCreateIpAssignmentPool,4,6);
+										} else continue;
+										sqlite3_step(_sCreateIpAssignmentPool);
 									}
 								}
+							} else if (!strcmp(j->u.object.values[k].name,"rules")) {
+								if (j->u.object.values[k].value->type == json_array) {
+									sqlite3_reset(_sDeleteRulesForNetwork);
+									sqlite3_bind_text(_sDeleteRulesForNetwork,1,nwids,16,SQLITE_STATIC);
+									sqlite3_step(_sDeleteRulesForNetwork);
 
-								break; // stop checking routed networks
-							}
-						}
+									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
+										json_value *rj = j->u.object.values[k].value->u.array.values[kk];
+										if ((rj)&&(rj->type == json_object)) {
+											struct { // NULL pointers indicate missing or NULL -- wildcards
+												const json_int_t *ruleNo;
+												const char *nodeId;
+												const char *sourcePort;
+												const char *destPort;
+												const json_int_t *vlanId;
+												const json_int_t *vlanPcp;
+												const json_int_t *etherType;
+												const char *macSource;
+												const char *macDest;
+												const char *ipSource;
+												const char *ipDest;
+												const json_int_t *ipTos;
+												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));
 
-						if (haveStaticIpAssignment)
-							break;
+											for(unsigned int rk=0;rk<rj->u.object.length;++rk) {
+												if ((!strcmp(rj->u.object.values[rk].name,"ruleNo"))&&(rj->u.object.values[rk].value->type == json_integer))
+													rule.ruleNo = &(rj->u.object.values[rk].value->u.integer);
+												else if ((!strcmp(rj->u.object.values[rk].name,"nodeId"))&&(rj->u.object.values[rk].value->type == json_string))
+													rule.nodeId = rj->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(rj->u.object.values[rk].name,"sourcePort"))&&(rj->u.object.values[rk].value->type == json_string))
+													rule.sourcePort = rj->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(rj->u.object.values[rk].name,"destPort"))&&(rj->u.object.values[rk].value->type == json_string))
+													rule.destPort = rj->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(rj->u.object.values[rk].name,"vlanId"))&&(rj->u.object.values[rk].value->type == json_integer))
+													rule.vlanId = &(rj->u.object.values[rk].value->u.integer);
+												else if ((!strcmp(rj->u.object.values[rk].name,"vlanPcp"))&&(rj->u.object.values[rk].value->type == json_integer))
+													rule.vlanPcp = &(rj->u.object.values[rk].value->u.integer);
+												else if ((!strcmp(rj->u.object.values[rk].name,"etherType"))&&(rj->u.object.values[rk].value->type == json_integer))
+													rule.etherType = &(rj->u.object.values[rk].value->u.integer);
+												else if ((!strcmp(rj->u.object.values[rk].name,"macSource"))&&(rj->u.object.values[rk].value->type == json_string))
+													rule.macSource = rj->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(rj->u.object.values[rk].name,"macDest"))&&(rj->u.object.values[rk].value->type == json_string))
+													rule.macDest = rj->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(rj->u.object.values[rk].name,"ipSource"))&&(rj->u.object.values[rk].value->type == json_string))
+													rule.ipSource = rj->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(rj->u.object.values[rk].name,"ipDest"))&&(rj->u.object.values[rk].value->type == json_string))
+													rule.ipDest = rj->u.object.values[rk].value->u.string.ptr;
+												else if ((!strcmp(rj->u.object.values[rk].name,"ipTos"))&&(rj->u.object.values[rk].value->type == json_integer))
+													rule.ipTos = &(rj->u.object.values[rk].value->u.integer);
+												else if ((!strcmp(rj->u.object.values[rk].name,"ipProtocol"))&&(rj->u.object.values[rk].value->type == json_integer))
+													rule.ipProtocol = &(rj->u.object.values[rk].value->u.integer);
+												else if ((!strcmp(rj->u.object.values[rk].name,"ipSourcePort"))&&(rj->u.object.values[rk].value->type == json_integer))
+													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;
+											}
+
+											if ((rule.ruleNo)&&(rule.action)&&(rule.action[0])) {
+												char mactmp1[16],mactmp2[16];
+												sqlite3_reset(_sCreateRule);
+												sqlite3_bind_text(_sCreateRule,1,nwids,16,SQLITE_STATIC);
+												sqlite3_bind_int64(_sCreateRule,2,*rule.ruleNo);
+
+												// Optional values: null by default
+												for(int i=3;i<=18;++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.sourcePort)&&(strlen(rule.sourcePort) == 10)) sqlite3_bind_text(_sCreateRule,4,rule.sourcePort,10,SQLITE_STATIC);
+												if ((rule.destPort)&&(strlen(rule.destPort) == 10)) sqlite3_bind_text(_sCreateRule,5,rule.destPort,10,SQLITE_STATIC);
+												if (rule.vlanId) sqlite3_bind_int(_sCreateRule,6,(int)*rule.vlanId);
+												if (rule.vlanPcp) sqlite3_bind_int(_sCreateRule,7,(int)*rule.vlanPcp);
+												if (rule.etherType) sqlite3_bind_int(_sCreateRule,8,(int)*rule.etherType & (int)0xffff);
+												if (rule.macSource) {
+													MAC m(rule.macSource);
+													Utils::snprintf(mactmp1,sizeof(mactmp1),"%.12llx",(unsigned long long)m.toInt());
+													sqlite3_bind_text(_sCreateRule,9,mactmp1,-1,SQLITE_STATIC);
+												}
+												if (rule.macDest) {
+													MAC m(rule.macDest);
+													Utils::snprintf(mactmp2,sizeof(mactmp2),"%.12llx",(unsigned long long)m.toInt());
+													sqlite3_bind_text(_sCreateRule,10,mactmp2,-1,SQLITE_STATIC);
+												}
+												if (rule.ipSource) sqlite3_bind_text(_sCreateRule,11,rule.ipSource,-1,SQLITE_STATIC);
+												if (rule.ipDest) sqlite3_bind_text(_sCreateRule,12,rule.ipDest,-1,SQLITE_STATIC);
+												if (rule.ipTos) sqlite3_bind_int(_sCreateRule,13,(int)*rule.ipTos);
+												if (rule.ipProtocol) sqlite3_bind_int(_sCreateRule,14,(int)*rule.ipProtocol);
+												if (rule.ipSourcePort) sqlite3_bind_int(_sCreateRule,15,(int)*rule.ipSourcePort & (int)0xffff);
+												if (rule.ipDestPort) sqlite3_bind_int(_sCreateRule,16,(int)*rule.ipDestPort & (int)0xffff);
+												if (rule.flags) sqlite3_bind_int64(_sCreateRule,17,(int64_t)*rule.flags);
+												if (rule.invFlags) sqlite3_bind_int64(_sCreateRule,18,(int64_t)*rule.invFlags);
+
+												sqlite3_bind_text(_sCreateRule,19,rule.action,-1,SQLITE_STATIC);
+												sqlite3_step(_sCreateRule);
+											}
+										}
+									}
+								}
+							}
+
+							if (stmt) {
+								sqlite3_bind_text(stmt,2,nwids,16,SQLITE_STATIC);
+								sqlite3_step(stmt);
+								sqlite3_finalize(stmt);
+							}
+						}
 					}
+					json_value_free(j);
 				}
-			}
-
-			if (v4s.length())
-				netconf[ZT_NETWORKCONFIG_DICT_KEY_IPV4_STATIC] = v4s;
-		}
 
-		// TODO: IPv6 auto-assign once it's supported in UI
+				sqlite3_reset(_sSetNetworkRevision);
+				sqlite3_bind_int64(_sSetNetworkRevision,1,revision += 1);
+				sqlite3_bind_text(_sSetNetworkRevision,2,nwids,16,SQLITE_STATIC);
+				sqlite3_step(_sSetNetworkRevision);
 
-		if (network.isPrivate) {
-			CertificateOfMembership com(OSUtils::now(),ZT_NETWORK_AUTOCONF_DELAY + (ZT_NETWORK_AUTOCONF_DELAY / 2),nwid,identity.address());
-			if (com.sign(signingId)) // basically can't fail unless our identity is invalid
-				netconf[ZT_NETWORKCONFIG_DICT_KEY_CERTIFICATE_OF_MEMBERSHIP] = com.toString();
-			else {
-				netconf["error"] = "unable to sign COM";
-				return NETCONF_QUERY_INTERNAL_SERVER_ERROR;
+				return _doCPGet(path_copy,urlArgs,headers,body,responseBody,responseContentType);
 			}
-		}
 
-		if (!netconf.sign(signingId,OSUtils::now())) {
-			netconf["error"] = "unable to sign netconf dictionary";
-			return NETCONF_QUERY_INTERNAL_SERVER_ERROR;
-		}
-	}
+		} // else 404
 
-	return NetworkController::NETCONF_QUERY_OK;
-}
+	} // else 404
 
-unsigned int SqliteNetworkController::handleControlPlaneHttpGET(
-	const std::vector<std::string> &path,
-	const std::map<std::string,std::string> &urlArgs,
-	const std::map<std::string,std::string> &headers,
-	const std::string &body,
-	std::string &responseBody,
-	std::string &responseContentType)
-{
-	Mutex::Lock _l(_lock);
-	return _doCPGet(path,urlArgs,headers,body,responseBody,responseContentType);
+	return 404;
 }
 
-unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
+unsigned int SqliteNetworkController::handleControlPlaneHttpDELETE(
 	const std::vector<std::string> &path,
 	const std::map<std::string,std::string> &urlArgs,
 	const std::map<std::string,std::string> &headers,
@@ -746,1056 +895,913 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
 			char nwids[24];
 			Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
 
-			int64_t revision = 0;
-			sqlite3_reset(_sGetNetworkRevision);
-			sqlite3_bind_text(_sGetNetworkRevision,1,nwids,16,SQLITE_STATIC);
-			bool networkExists = false;
-			if (sqlite3_step(_sGetNetworkRevision) == SQLITE_ROW) {
-				networkExists = true;
-				revision = sqlite3_column_int64(_sGetNetworkRevision,0);
-			}
+			sqlite3_reset(_sGetNetworkById);
+			sqlite3_bind_text(_sGetNetworkById,1,nwids,16,SQLITE_STATIC);
+			if (sqlite3_step(_sGetNetworkById) != SQLITE_ROW)
+				return 404;
 
 			if (path.size() >= 3) {
 
-				if (!networkExists)
-					return 404;
-
 				if ((path.size() == 4)&&(path[2] == "member")&&(path[3].length() == 10)) {
 					uint64_t address = Utils::hexStrToU64(path[3].c_str());
 					char addrs[24];
 					Utils::snprintf(addrs,sizeof(addrs),"%.10llx",address);
 
-					int64_t addToNetworkRevision = 0;
-
-					int64_t memberRowId = 0;
 					sqlite3_reset(_sGetMember);
 					sqlite3_bind_text(_sGetMember,1,nwids,16,SQLITE_STATIC);
 					sqlite3_bind_text(_sGetMember,2,addrs,10,SQLITE_STATIC);
-					bool memberExists = false;
-					if (sqlite3_step(_sGetMember) == SQLITE_ROW) {
-						memberExists = true;
-						memberRowId = sqlite3_column_int64(_sGetMember,0);
-					}
+					if (sqlite3_step(_sGetMember) != SQLITE_ROW)
+						return 404;
 
-					if (!memberExists) {
-						sqlite3_reset(_sCreateMember);
-						sqlite3_bind_text(_sCreateMember,1,nwids,16,SQLITE_STATIC);
-						sqlite3_bind_text(_sCreateMember,2,addrs,10,SQLITE_STATIC);
-						sqlite3_bind_int(_sCreateMember,3,0);
-						sqlite3_bind_text(_sCreateMember,4,nwids,16,SQLITE_STATIC);
-						if (sqlite3_step(_sCreateMember) != SQLITE_DONE)
+					sqlite3_reset(_sDeleteIpAllocations);
+					sqlite3_bind_text(_sDeleteIpAllocations,1,nwids,16,SQLITE_STATIC);
+					sqlite3_bind_text(_sDeleteIpAllocations,2,addrs,10,SQLITE_STATIC);
+					sqlite3_bind_int(_sDeleteIpAllocations,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
+					if (sqlite3_step(_sDeleteIpAllocations) == SQLITE_DONE) {
+						sqlite3_reset(_sDeleteMember);
+						sqlite3_bind_text(_sDeleteMember,1,nwids,16,SQLITE_STATIC);
+						sqlite3_bind_text(_sDeleteMember,2,addrs,10,SQLITE_STATIC);
+						if (sqlite3_step(_sDeleteMember) != SQLITE_DONE)
 							return 500;
-						memberRowId = (int64_t)sqlite3_last_insert_rowid(_db);
+					} else return 500;
 
-						sqlite3_reset(_sIncrementMemberRevisionCounter);
-						sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,nwids,16,SQLITE_STATIC);
-						sqlite3_step(_sIncrementMemberRevisionCounter);
-						addToNetworkRevision = 1;
-					}
+					return 200;
+				}
 
-					json_value *j = json_parse(body.c_str(),body.length());
-					if (j) {
-						if (j->type == json_object) {
-							for(unsigned int k=0;k<j->u.object.length;++k) {
+			} else {
 
-								if (!strcmp(j->u.object.values[k].name,"authorized")) {
-									if (j->u.object.values[k].value->type == json_boolean) {
-										sqlite3_reset(_sUpdateMemberAuthorized);
-										sqlite3_bind_int(_sUpdateMemberAuthorized,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
-										sqlite3_bind_text(_sUpdateMemberAuthorized,2,nwids,16,SQLITE_STATIC);
-										sqlite3_bind_int64(_sUpdateMemberAuthorized,3,memberRowId);
-										if (sqlite3_step(_sUpdateMemberAuthorized) != SQLITE_DONE)
-											return 500;
+				sqlite3_reset(_sDeleteNetwork);
+				sqlite3_bind_text(_sDeleteNetwork,1,nwids,16,SQLITE_STATIC);
+				return ((sqlite3_step(_sDeleteNetwork) == SQLITE_DONE) ? 200 : 500);
 
-										sqlite3_reset(_sIncrementMemberRevisionCounter);
-										sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,nwids,16,SQLITE_STATIC);
-										sqlite3_step(_sIncrementMemberRevisionCounter);
-										addToNetworkRevision = 1;
-									}
-								} else if (!strcmp(j->u.object.values[k].name,"activeBridge")) {
-									if (j->u.object.values[k].value->type == json_boolean) {
-										sqlite3_reset(_sUpdateMemberActiveBridge);
-										sqlite3_bind_int(_sUpdateMemberActiveBridge,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
-										sqlite3_bind_text(_sUpdateMemberActiveBridge,2,nwids,16,SQLITE_STATIC);
-										sqlite3_bind_int64(_sUpdateMemberActiveBridge,3,memberRowId);
-										if (sqlite3_step(_sUpdateMemberActiveBridge) != SQLITE_DONE)
-											return 500;
+			}
+		} // else 404
 
-										sqlite3_reset(_sIncrementMemberRevisionCounter);
-										sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,nwids,16,SQLITE_STATIC);
-										sqlite3_step(_sIncrementMemberRevisionCounter);
-										addToNetworkRevision = 1;
-									}
-								} else if (!strcmp(j->u.object.values[k].name,"ipAssignments")) {
-									if (j->u.object.values[k].value->type == json_array) {
-										sqlite3_reset(_sDeleteIpAllocations);
-										sqlite3_bind_text(_sDeleteIpAllocations,1,nwids,16,SQLITE_STATIC);
-										sqlite3_bind_text(_sDeleteIpAllocations,2,addrs,10,SQLITE_STATIC);
-										sqlite3_bind_int(_sDeleteIpAllocations,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
-										if (sqlite3_step(_sDeleteIpAllocations) != SQLITE_DONE)
-											return 500;
-										for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
-											json_value *ipalloc = j->u.object.values[k].value->u.array.values[kk];
-											if (ipalloc->type == json_string) {
-												InetAddress a(ipalloc->u.string.ptr);
-												char ipBlob[16];
-												int ipVersion = 0;
-												switch(a.ss_family) {
-													case AF_INET:
-														if ((a.netmaskBits() > 0)&&(a.netmaskBits() <= 32)) {
-															memset(ipBlob,0,12);
-															memcpy(ipBlob + 12,a.rawIpData(),4);
-															ipVersion = 4;
-														}
-														break;
-													case AF_INET6:
-														if ((a.netmaskBits() > 0)&&(a.netmaskBits() <= 128)) {
-															memcpy(ipBlob,a.rawIpData(),16);
-															ipVersion = 6;
-														}
-														break;
-												}
-												if (ipVersion > 0) {
-													sqlite3_reset(_sAllocateIp);
-													sqlite3_bind_text(_sAllocateIp,1,nwids,16,SQLITE_STATIC);
-													sqlite3_bind_text(_sAllocateIp,2,addrs,10,SQLITE_STATIC);
-													sqlite3_bind_int(_sAllocateIp,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
-													sqlite3_bind_blob(_sAllocateIp,4,(const void *)ipBlob,16,SQLITE_STATIC);
-													sqlite3_bind_int(_sAllocateIp,5,(int)a.netmaskBits());
-													sqlite3_bind_int(_sAllocateIp,6,ipVersion);
-													if (sqlite3_step(_sAllocateIp) != SQLITE_DONE)
-														return 500;
-												}
-											}
-										}
-										addToNetworkRevision = 1;
-									}
-								} else if (!strcmp(j->u.object.values[k].name,"identity")) {
-									// Identity is technically an immutable field, but if the member's Node has
-									// no identity we allow it to be populated. This is primarily for migrating
-									// node data from another controller.
-									json_value *idstr = j->u.object.values[k].value;
-									if (idstr->type == json_string) {
-										bool alreadyHaveIdentity = false;
+	} // else 404
 
-										sqlite3_reset(_sGetNodeIdentity);
-										sqlite3_bind_text(_sGetNodeIdentity,1,addrs,10,SQLITE_STATIC);
-										if (sqlite3_step(_sGetNodeIdentity) == SQLITE_ROW) {
-											const char *tmp2 = (const char *)sqlite3_column_text(_sGetNodeIdentity,0);
-											if ((tmp2)&&(tmp2[0]))
-												alreadyHaveIdentity = true;
-										}
+	return 404;
+}
 
-										if (!alreadyHaveIdentity) {
-											try {
-												Identity id2(idstr->u.string.ptr);
-												if (id2) {
-													std::string idstr2(id2.toString(false)); // object must persist until after sqlite3_step() for SQLITE_STATIC
-													sqlite3_reset(_sCreateOrReplaceNode);
-													sqlite3_bind_text(_sCreateOrReplaceNode,1,addrs,10,SQLITE_STATIC);
-													sqlite3_bind_text(_sCreateOrReplaceNode,2,idstr2.c_str(),-1,SQLITE_STATIC);
-													sqlite3_step(_sCreateOrReplaceNode);
-												}
-											} catch ( ... ) {} // ignore invalid identities
-										}
-									}
+unsigned int SqliteNetworkController::_doCPGet(
+	const std::vector<std::string> &path,
+	const std::map<std::string,std::string> &urlArgs,
+	const std::map<std::string,std::string> &headers,
+	const std::string &body,
+	std::string &responseBody,
+	std::string &responseContentType)
+{
+	// Assumes _lock is locked
+	char json[65536];
+
+	if ((path.size() > 0)&&(path[0] == "network")) {
+
+		if ((path.size() >= 2)&&(path[1].length() == 16)) {
+			uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
+			char nwids[24];
+			Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
+
+			if (path.size() >= 3) {
+				// /network/<nwid>/...
+
+				if (path[2] == "member") {
+
+					if (path.size() >= 4) {
+						// Get specific member info
+
+						uint64_t address = Utils::hexStrToU64(path[3].c_str());
+						char addrs[24];
+						Utils::snprintf(addrs,sizeof(addrs),"%.10llx",address);
+
+						sqlite3_reset(_sGetMember2);
+						sqlite3_bind_text(_sGetMember2,1,nwids,16,SQLITE_STATIC);
+						sqlite3_bind_text(_sGetMember2,2,addrs,10,SQLITE_STATIC);
+						if (sqlite3_step(_sGetMember2) == SQLITE_ROW) {
+							const char *memberIdStr = (const char *)sqlite3_column_text(_sGetMember2,3);
+
+							// If testSingingId is included in the URL or X-ZT1-TestSigningId in the headers
+							// and if it contains an identity with a secret portion, the resturned JSON
+							// will contain an extra field called _testConf. This will contain several
+							// fields that report the result of doNetworkConfigRequest() for this member.
+							std::string testFields;
+							{
+								Identity testOutputSigningId;
+								std::map<std::string,std::string>::const_iterator sid(urlArgs.find("testSigningId"));
+								if (sid != urlArgs.end()) {
+									testOutputSigningId.fromString(sid->second.c_str());
+								} else {
+									sid = headers.find("x-zt1-testsigningid");
+									if (sid != headers.end())
+										testOutputSigningId.fromString(sid->second.c_str());
 								}
 
+								if ((testOutputSigningId.hasPrivate())&&(memberIdStr)) {
+									Dictionary testNetconf;
+									NetworkController::ResultCode rc = this->_doNetworkConfigRequest(
+										InetAddress(),
+										testOutputSigningId,
+										Identity(memberIdStr),
+										nwid,
+										Dictionary(), // TODO: allow passing of meta-data for testing
+										testNetconf);
+									char rcs[16];
+									Utils::snprintf(rcs,sizeof(rcs),"%d,\n",(int)rc);
+									testFields.append("\t\"_test\": {\n");
+									testFields.append("\t\t\"resultCode\": "); testFields.append(rcs);
+									testFields.append("\t\t\"result\": \""); testFields.append(_jsonEscape(testNetconf.toString().c_str()).c_str()); testFields.append("\"");
+									testFields.append("\t}\n");
+								}
 							}
-						}
-						json_value_free(j);
-					}
 
-					if ((addToNetworkRevision > 0)&&(revision > 0)) {
-						sqlite3_reset(_sSetNetworkRevision);
-						sqlite3_bind_int64(_sSetNetworkRevision,1,revision + addToNetworkRevision);
-						sqlite3_bind_text(_sSetNetworkRevision,2,nwids,16,SQLITE_STATIC);
-						sqlite3_step(_sSetNetworkRevision);
-					}
+							Utils::snprintf(json,sizeof(json),
+								"{\n%s"
+								"\t\"nwid\": \"%s\",\n"
+								"\t\"address\": \"%s\",\n"
+								"\t\"controllerInstanceId\": \"%s\",\n"
+								"\t\"authorized\": %s,\n"
+								"\t\"activeBridge\": %s,\n"
+								"\t\"memberRevision\": %llu,\n"
+								"\t\"clock\": %llu,\n"
+								"\t\"identity\": \"%s\",\n"
+								"\t\"ipAssignments\": [",
+								testFields.c_str(),
+								nwids,
+								addrs,
+								_instanceId.c_str(),
+								(sqlite3_column_int(_sGetMember2,0) > 0) ? "true" : "false",
+								(sqlite3_column_int(_sGetMember2,1) > 0) ? "true" : "false",
+								(unsigned long long)sqlite3_column_int64(_sGetMember2,2),
+								(unsigned long long)OSUtils::now(),
+								_jsonEscape(memberIdStr).c_str());
+							responseBody = json;
 
-					return _doCPGet(path,urlArgs,headers,body,responseBody,responseContentType);
-				} // else 404
+							sqlite3_reset(_sGetIpAssignmentsForNode2);
+							sqlite3_bind_text(_sGetIpAssignmentsForNode2,1,nwids,16,SQLITE_STATIC);
+							sqlite3_bind_text(_sGetIpAssignmentsForNode2,2,addrs,10,SQLITE_STATIC);
+							sqlite3_bind_int(_sGetIpAssignmentsForNode2,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
+							bool firstIp = true;
+							while (sqlite3_step(_sGetIpAssignmentsForNode2) == SQLITE_ROW) {
+								int ipversion = sqlite3_column_int(_sGetIpAssignmentsForNode2,2);
+								char ipBlob[16];
+								memcpy(ipBlob,(const void *)sqlite3_column_blob(_sGetIpAssignmentsForNode2,0),16);
+								InetAddress ip(
+									(const void *)(ipversion == 6 ? ipBlob : &ipBlob[12]),
+									(ipversion == 6 ? 16 : 4),
+									(unsigned int)sqlite3_column_int(_sGetIpAssignmentsForNode2,1)
+								);
+								responseBody.append(firstIp ? "\"" : ",\"");
+								firstIp = false;
+								responseBody.append(_jsonEscape(ip.toString()));
+								responseBody.push_back('"');
+							}
 
-			} else {
-				std::vector<std::string> path_copy(path);
+							responseBody.append("],\n\t\"recentLog\": [");
 
-				if (!networkExists) {
-					if (path[1].substr(10) == "______") {
-						// A special POST /network/##########______ feature lets users create a network
-						// with an arbitrary unused network number at this controller.
-						nwid = 0;
+							sqlite3_reset(_sGetRecentMemberLog);
+							sqlite3_bind_text(_sGetRecentMemberLog,1,nwids,16,SQLITE_STATIC);
+							sqlite3_bind_text(_sGetRecentMemberLog,2,addrs,10,SQLITE_STATIC);
+							bool firstLog = true;
+							while (sqlite3_step(_sGetRecentMemberLog) == SQLITE_ROW) {
+								responseBody.append(firstLog ? "{" : ",{");
+								firstLog = false;
+								responseBody.append("\"ts\":");
+								responseBody.append(reinterpret_cast<const char *>(sqlite3_column_text(_sGetRecentMemberLog,0)));
+								responseBody.append((sqlite3_column_int(_sGetRecentMemberLog,1) == 0) ? ",\"authorized\":false,\"version\":" : ",\"authorized\":true,\"version\":");
+								const char *ver = reinterpret_cast<const char *>(sqlite3_column_text(_sGetRecentMemberLog,2));
+								if ((ver)&&(ver[0])) {
+									responseBody.push_back('"');
+									responseBody.append(_jsonEscape(ver));
+									responseBody.append("\",\"fromAddr\":");
+								} else responseBody.append("null,\"fromAddr\":");
+								const char *fa = reinterpret_cast<const char *>(sqlite3_column_text(_sGetRecentMemberLog,3));
+								if ((fa)&&(fa[0])) {
+									responseBody.push_back('"');
+									responseBody.append(_jsonEscape(fa));
+									responseBody.append("\"}");
+								} else responseBody.append("null}");
+							}
 
-						uint64_t nwidPrefix = (Utils::hexStrToU64(path[1].substr(0,10).c_str()) << 24) & 0xffffffffff000000ULL;
-						uint64_t nwidPostfix = 0;
-						Utils::getSecureRandom(&nwidPostfix,sizeof(nwidPostfix));
-						uint64_t nwidOriginalPostfix = nwidPostfix;
-						do {
-							uint64_t tryNwid = nwidPrefix | (nwidPostfix & 0xffffffULL);
-							if (!nwidPostfix)
-								tryNwid |= 1;
-							Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)tryNwid);
+							responseBody.append("]\n}\n");
 
-							sqlite3_reset(_sGetNetworkRevision);
-							sqlite3_bind_text(_sGetNetworkRevision,1,nwids,16,SQLITE_STATIC);
-							if (sqlite3_step(_sGetNetworkRevision) != SQLITE_ROW) {
-								nwid = tryNwid;
-								break;
-							}
+							responseContentType = "application/json";
+							return 200;
+						} // else 404
 
-							++nwidPostfix;
-						} while (nwidPostfix != nwidOriginalPostfix);
+					} else {
+						// List members
 
-						// 503 means we have no more free IDs for this prefix. You shouldn't host anywhere
-						// near 16 million networks on the same controller, so shouldn't happen.
-						if (!nwid)
-							return 503;
-					}
+						sqlite3_reset(_sListNetworkMembers);
+						sqlite3_bind_text(_sListNetworkMembers,1,nwids,16,SQLITE_STATIC);
+						responseBody.append("{");
+						bool firstMember = true;
+						while (sqlite3_step(_sListNetworkMembers) == SQLITE_ROW) {
+							responseBody.append(firstMember ? "\"" : ",\"");
+							firstMember = false;
+							responseBody.append((const char *)sqlite3_column_text(_sListNetworkMembers,0));
+							responseBody.append("\":");
+							responseBody.append((const char *)sqlite3_column_text(_sListNetworkMembers,1));
+						}
+						responseBody.push_back('}');
+						responseContentType = "application/json";
+						return 200;
 
-					sqlite3_reset(_sCreateNetwork);
-					sqlite3_bind_text(_sCreateNetwork,1,nwids,16,SQLITE_STATIC);
-					sqlite3_bind_text(_sCreateNetwork,2,"",0,SQLITE_STATIC);
-					sqlite3_bind_int64(_sCreateNetwork,3,(long long)OSUtils::now());
-					if (sqlite3_step(_sCreateNetwork) != SQLITE_DONE)
-						return 500;
-					path_copy[1].assign(nwids);
-				}
+					}
 
-				json_value *j = json_parse(body.c_str(),body.length());
-				if (j) {
-					if (j->type == json_object) {
-						for(unsigned int k=0;k<j->u.object.length;++k) {
-							sqlite3_stmt *stmt = (sqlite3_stmt *)0;
+				} // else 404
 
-							if (!strcmp(j->u.object.values[k].name,"name")) {
-								if ((j->u.object.values[k].value->type == json_string)&&(j->u.object.values[k].value->u.string.ptr[0])) {
-									if (sqlite3_prepare_v2(_db,"UPDATE Network SET \"name\" = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
-										sqlite3_bind_text(stmt,1,j->u.object.values[k].value->u.string.ptr,-1,SQLITE_STATIC);
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"private")) {
-								if (j->u.object.values[k].value->type == json_boolean) {
-									if (sqlite3_prepare_v2(_db,"UPDATE Network SET \"private\" = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
-										sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"enableBroadcast")) {
-								if (j->u.object.values[k].value->type == json_boolean) {
-									if (sqlite3_prepare_v2(_db,"UPDATE Network SET enableBroadcast = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
-										sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"allowPassiveBridging")) {
-								if (j->u.object.values[k].value->type == json_boolean) {
-									if (sqlite3_prepare_v2(_db,"UPDATE Network SET allowPassiveBridging = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
-										sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"v4AssignMode")) {
-								if (j->u.object.values[k].value->type == json_string) {
-									if (sqlite3_prepare_v2(_db,"UPDATE Network SET v4AssignMode = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
-										sqlite3_bind_text(stmt,1,j->u.object.values[k].value->u.string.ptr,-1,SQLITE_STATIC);
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"v6AssignMode")) {
-								if (j->u.object.values[k].value->type == json_string) {
-									if (sqlite3_prepare_v2(_db,"UPDATE Network SET v6AssignMode = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
-										sqlite3_bind_text(stmt,1,j->u.object.values[k].value->u.string.ptr,-1,SQLITE_STATIC);
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"multicastLimit")) {
-								if (j->u.object.values[k].value->type == json_integer) {
-									if (sqlite3_prepare_v2(_db,"UPDATE Network SET multicastLimit = ? WHERE id = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
-										sqlite3_bind_int(stmt,1,(int)j->u.object.values[k].value->u.integer);
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"relays")) {
-								if (j->u.object.values[k].value->type == json_array) {
-									std::map<Address,InetAddress> nodeIdToPhyAddress;
-									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
-										json_value *relay = j->u.object.values[k].value->u.array.values[kk];
-										const char *address = (const char *)0;
-										const char *phyAddress = (const char *)0;
-										if ((relay)&&(relay->type == json_object)) {
-											for(unsigned int rk=0;rk<relay->u.object.length;++rk) {
-												if ((!strcmp(relay->u.object.values[rk].name,"address"))&&(relay->u.object.values[rk].value->type == json_string))
-													address = relay->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(relay->u.object.values[rk].name,"phyAddress"))&&(relay->u.object.values[rk].value->type == json_string))
-													phyAddress = relay->u.object.values[rk].value->u.string.ptr;
-											}
-										}
-										if ((address)&&(phyAddress))
-											nodeIdToPhyAddress[Address(address)] = InetAddress(phyAddress);
-									}
-
-									sqlite3_reset(_sDeleteRelaysForNetwork);
-									sqlite3_bind_text(_sDeleteRelaysForNetwork,1,nwids,16,SQLITE_STATIC);
-									sqlite3_step(_sDeleteRelaysForNetwork);
-
-									for(std::map<Address,InetAddress>::iterator rl(nodeIdToPhyAddress.begin());rl!=nodeIdToPhyAddress.end();++rl) {
-										sqlite3_reset(_sCreateRelay);
-										sqlite3_bind_text(_sCreateRelay,1,nwids,16,SQLITE_STATIC);
-										std::string a(rl->first.toString()),b(rl->second.toString()); // don't destroy strings until sqlite3_step()
-										sqlite3_bind_text(_sCreateRelay,2,a.c_str(),-1,SQLITE_STATIC);
-										sqlite3_bind_text(_sCreateRelay,3,b.c_str(),-1,SQLITE_STATIC);
-										sqlite3_step(_sCreateRelay);
-									}
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"gateways")) {
-								sqlite3_reset(_sDeleteGateways);
-								sqlite3_bind_text(_sDeleteGateways,1,nwids,16,SQLITE_STATIC);
-								sqlite3_step(_sDeleteGateways);
-								if (j->u.object.values[k].value->type == json_array) {
-									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
-										json_value *gateway = j->u.object.values[k].value->u.array.values[kk];
-										if ((gateway)&&(gateway->type == json_string)) {
-											InetAddress gwip(gateway->u.string.ptr);
-											sqlite3_reset(_sCreateGateway);
-											sqlite3_bind_text(_sCreateGateway,1,nwids,16,SQLITE_STATIC);
-											sqlite3_bind_int(_sCreateGateway,4,(int)gwip.metric());
-											if (gwip.ss_family == AF_INET) {
-												char ipBlob[16];
-												memset(ipBlob,0,12);
-												memcpy(ipBlob + 12,gwip.rawIpData(),4);
-												sqlite3_bind_blob(_sCreateGateway,2,(const void *)ipBlob,16,SQLITE_STATIC);
-												sqlite3_bind_int(_sCreateGateway,3,4);
-												sqlite3_step(_sCreateGateway);
-											} else if (gwip.ss_family == AF_INET6) {
-												sqlite3_bind_blob(_sCreateGateway,2,gwip.rawIpData(),16,SQLITE_STATIC);
-												sqlite3_bind_int(_sCreateGateway,3,6);
-												sqlite3_step(_sCreateGateway);
-											}
-										}
-									}
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"ipLocalRoutes")) {
-								sqlite3_reset(_sDeleteLocalRoutes);
-								sqlite3_bind_text(_sDeleteLocalRoutes,1,nwids,16,SQLITE_STATIC);
-								sqlite3_bind_int(_sDeleteLocalRoutes,2,(int)ZT_IP_ASSIGNMENT_TYPE_NETWORK);
-								sqlite3_step(_sDeleteLocalRoutes);
-								if (j->u.object.values[k].value->type == json_array) {
-									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
-										json_value *localRoute = j->u.object.values[k].value->u.array.values[kk];
-										if ((localRoute)&&(localRoute->type == json_string)) {
-											InetAddress lr(localRoute->u.string.ptr);
-											if (lr.ss_family == AF_INET) {
-												char ipBlob[16];
-												memset(ipBlob,0,12);
-												memcpy(ipBlob + 12,lr.rawIpData(),4);
-												sqlite3_reset(_sAllocateIp);
-												sqlite3_bind_text(_sAllocateIp,1,nwids,16,SQLITE_STATIC);
-												sqlite3_bind_null(_sAllocateIp,2);
-												sqlite3_bind_int(_sAllocateIp,3,(int)ZT_IP_ASSIGNMENT_TYPE_NETWORK);
-												sqlite3_bind_blob(_sAllocateIp,4,(const void *)ipBlob,16,SQLITE_STATIC);
-												sqlite3_bind_int(_sAllocateIp,5,lr.netmaskBits());
-												sqlite3_bind_int(_sAllocateIp,6,4);
-												sqlite3_step(_sAllocateIp);
-											} else if (lr.ss_family == AF_INET6) {
-												sqlite3_reset(_sAllocateIp);
-												sqlite3_bind_text(_sAllocateIp,1,nwids,16,SQLITE_STATIC);
-												sqlite3_bind_null(_sAllocateIp,2);
-												sqlite3_bind_int(_sAllocateIp,3,(int)ZT_IP_ASSIGNMENT_TYPE_NETWORK);
-												sqlite3_bind_blob(_sAllocateIp,4,lr.rawIpData(),16,SQLITE_STATIC);
-												sqlite3_bind_int(_sAllocateIp,5,lr.netmaskBits());
-												sqlite3_bind_int(_sAllocateIp,6,6);
-												sqlite3_step(_sAllocateIp);
-											}
-										}
-									}
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"ipAssignmentPools")) {
-								if (j->u.object.values[k].value->type == json_array) {
-									std::vector< std::pair<InetAddress,InetAddress> > pools;
-									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
-										json_value *pool = j->u.object.values[k].value->u.array.values[kk];
-										const char *iprs = (const char *)0;
-										const char *ipre = (const char *)0;
-										if ((pool)&&(pool->type == json_object)) {
-											for(unsigned int rk=0;rk<pool->u.object.length;++rk) {
-												if ((!strcmp(pool->u.object.values[rk].name,"ipRangeStart"))&&(pool->u.object.values[rk].value->type == json_string))
-													iprs = pool->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(pool->u.object.values[rk].name,"ipRangeEnd"))&&(pool->u.object.values[rk].value->type == json_string))
-													ipre = pool->u.object.values[rk].value->u.string.ptr;
-											}
-										}
-										if ((iprs)&&(ipre)) {
-											InetAddress iprs2(iprs);
-											InetAddress ipre2(ipre);
-											if (iprs2.ss_family == ipre2.ss_family) {
-												iprs2.setPort(0);
-												ipre2.setPort(0);
-												pools.push_back(std::pair<InetAddress,InetAddress>(iprs2,ipre2));
-											}
-										}
-									}
-									std::sort(pools.begin(),pools.end());
-									pools.erase(std::unique(pools.begin(),pools.end()),pools.end());
+			} else {
+				// get network info
+				sqlite3_reset(_sGetNetworkById);
+				sqlite3_bind_text(_sGetNetworkById,1,nwids,16,SQLITE_STATIC);
+				if (sqlite3_step(_sGetNetworkById) == SQLITE_ROW) {
+					Utils::snprintf(json,sizeof(json),
+						"{\n"
+						"\t\"nwid\": \"%s\",\n"
+						"\t\"controllerInstanceId\": \"%s\",\n"
+						"\t\"clock\": %llu,\n"
+						"\t\"name\": \"%s\",\n"
+						"\t\"private\": %s,\n"
+						"\t\"enableBroadcast\": %s,\n"
+						"\t\"allowPassiveBridging\": %s,\n"
+						"\t\"v4AssignMode\": \"%s\",\n"
+						"\t\"v6AssignMode\": \"%s\",\n"
+						"\t\"multicastLimit\": %d,\n"
+						"\t\"creationTime\": %llu,\n"
+						"\t\"revision\": %llu,\n"
+						"\t\"memberRevisionCounter\": %llu,\n"
+						"\t\"authorizedMemberCount\": %llu,\n"
+						"\t\"relays\": [",
+						nwids,
+						_instanceId.c_str(),
+						(unsigned long long)OSUtils::now(),
+						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,0)).c_str(),
+						(sqlite3_column_int(_sGetNetworkById,1) > 0) ? "true" : "false",
+						(sqlite3_column_int(_sGetNetworkById,2) > 0) ? "true" : "false",
+						(sqlite3_column_int(_sGetNetworkById,3) > 0) ? "true" : "false",
+						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,4)).c_str(),
+						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,5)).c_str(),
+						sqlite3_column_int(_sGetNetworkById,6),
+						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,7),
+						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,8),
+						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,9),
+						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,10));
+					responseBody = json;
 
-									sqlite3_reset(_sDeleteIpAssignmentPoolsForNetwork);
-									sqlite3_bind_text(_sDeleteIpAssignmentPoolsForNetwork,1,nwids,16,SQLITE_STATIC);
-									sqlite3_step(_sDeleteIpAssignmentPoolsForNetwork);
+					sqlite3_reset(_sGetRelays);
+					sqlite3_bind_text(_sGetRelays,1,nwids,16,SQLITE_STATIC);
+					bool firstRelay = true;
+					while (sqlite3_step(_sGetRelays) == SQLITE_ROW) {
+						responseBody.append(firstRelay ? "\n\t\t" : ",\n\t\t");
+						firstRelay = false;
+						responseBody.append("{\"address\":\"");
+						responseBody.append((const char *)sqlite3_column_text(_sGetRelays,0));
+						responseBody.append("\",\"phyAddress\":\"");
+						responseBody.append(_jsonEscape((const char *)sqlite3_column_text(_sGetRelays,1)));
+						responseBody.append("\"}");
+					}
 
-									for(std::vector< std::pair<InetAddress,InetAddress> >::const_iterator p(pools.begin());p!=pools.end();++p) {
-										char ipBlob1[16],ipBlob2[16];
-										sqlite3_reset(_sCreateIpAssignmentPool);
-										sqlite3_bind_text(_sCreateIpAssignmentPool,1,nwids,16,SQLITE_STATIC);
-										if (p->first.ss_family == AF_INET) {
-											memset(ipBlob1,0,12);
-											memcpy(ipBlob1 + 12,p->first.rawIpData(),4);
-											memset(ipBlob2,0,12);
-											memcpy(ipBlob2 + 12,p->second.rawIpData(),4);
-											sqlite3_bind_blob(_sCreateIpAssignmentPool,2,(const void *)ipBlob1,16,SQLITE_STATIC);
-											sqlite3_bind_blob(_sCreateIpAssignmentPool,3,(const void *)ipBlob2,16,SQLITE_STATIC);
-											sqlite3_bind_int(_sCreateIpAssignmentPool,4,4);
-										} else if (p->first.ss_family == AF_INET6) {
-											sqlite3_bind_blob(_sCreateIpAssignmentPool,2,p->first.rawIpData(),16,SQLITE_STATIC);
-											sqlite3_bind_blob(_sCreateIpAssignmentPool,3,p->second.rawIpData(),16,SQLITE_STATIC);
-											sqlite3_bind_int(_sCreateIpAssignmentPool,4,6);
-										} else continue;
-										sqlite3_step(_sCreateIpAssignmentPool);
-									}
-								}
-							} else if (!strcmp(j->u.object.values[k].name,"rules")) {
-								if (j->u.object.values[k].value->type == json_array) {
-									sqlite3_reset(_sDeleteRulesForNetwork);
-									sqlite3_bind_text(_sDeleteRulesForNetwork,1,nwids,16,SQLITE_STATIC);
-									sqlite3_step(_sDeleteRulesForNetwork);
+					responseBody.append("],\n\t\"gateways\": [");
 
-									for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
-										json_value *rj = j->u.object.values[k].value->u.array.values[kk];
-										if ((rj)&&(rj->type == json_object)) {
-											struct { // NULL pointers indicate missing or NULL -- wildcards
-												const json_int_t *ruleNo;
-												const char *nodeId;
-												const char *sourcePort;
-												const char *destPort;
-												const json_int_t *vlanId;
-												const json_int_t *vlanPcp;
-												const json_int_t *etherType;
-												const char *macSource;
-												const char *macDest;
-												const char *ipSource;
-												const char *ipDest;
-												const json_int_t *ipTos;
-												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));
+					sqlite3_reset(_sGetGateways);
+					sqlite3_bind_text(_sGetGateways,1,nwids,16,SQLITE_STATIC);
+					bool firstGateway = true;
+					while (sqlite3_step(_sGetGateways) == SQLITE_ROW) {
+						char tmp[128];
+						const unsigned char *ip = (const unsigned char *)sqlite3_column_blob(_sGetGateways,0);
+						switch(sqlite3_column_int(_sGetGateways,1)) { // ipVersion
+							case 4:
+								Utils::snprintf(tmp,sizeof(tmp),"%s%d.%d.%d.%d/%d\"",
+									(firstGateway) ? "\"" : ",\"",
+									(int)ip[12],
+									(int)ip[13],
+									(int)ip[14],
+									(int)ip[15],
+									(int)sqlite3_column_int(_sGetGateways,2)); // metric
+								break;
+							case 6:
+								Utils::snprintf(tmp,sizeof(tmp),"%s%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x/%d\"",
+									(firstGateway) ? "\"" : ",\"",
+									(int)ip[0],
+									(int)ip[1],
+									(int)ip[2],
+									(int)ip[3],
+									(int)ip[4],
+									(int)ip[5],
+									(int)ip[6],
+									(int)ip[7],
+									(int)ip[8],
+									(int)ip[9],
+									(int)ip[10],
+									(int)ip[11],
+									(int)ip[12],
+									(int)ip[13],
+									(int)ip[14],
+									(int)ip[15],
+									(int)sqlite3_column_int(_sGetGateways,2)); // metric
+								break;
+						}
+						responseBody.append(tmp);
+						firstGateway = false;
+					}
 
-											for(unsigned int rk=0;rk<rj->u.object.length;++rk) {
-												if ((!strcmp(rj->u.object.values[rk].name,"ruleNo"))&&(rj->u.object.values[rk].value->type == json_integer))
-													rule.ruleNo = &(rj->u.object.values[rk].value->u.integer);
-												else if ((!strcmp(rj->u.object.values[rk].name,"nodeId"))&&(rj->u.object.values[rk].value->type == json_string))
-													rule.nodeId = rj->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(rj->u.object.values[rk].name,"sourcePort"))&&(rj->u.object.values[rk].value->type == json_string))
-													rule.sourcePort = rj->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(rj->u.object.values[rk].name,"destPort"))&&(rj->u.object.values[rk].value->type == json_string))
-													rule.destPort = rj->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(rj->u.object.values[rk].name,"vlanId"))&&(rj->u.object.values[rk].value->type == json_integer))
-													rule.vlanId = &(rj->u.object.values[rk].value->u.integer);
-												else if ((!strcmp(rj->u.object.values[rk].name,"vlanPcp"))&&(rj->u.object.values[rk].value->type == json_integer))
-													rule.vlanPcp = &(rj->u.object.values[rk].value->u.integer);
-												else if ((!strcmp(rj->u.object.values[rk].name,"etherType"))&&(rj->u.object.values[rk].value->type == json_integer))
-													rule.etherType = &(rj->u.object.values[rk].value->u.integer);
-												else if ((!strcmp(rj->u.object.values[rk].name,"macSource"))&&(rj->u.object.values[rk].value->type == json_string))
-													rule.macSource = rj->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(rj->u.object.values[rk].name,"macDest"))&&(rj->u.object.values[rk].value->type == json_string))
-													rule.macDest = rj->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(rj->u.object.values[rk].name,"ipSource"))&&(rj->u.object.values[rk].value->type == json_string))
-													rule.ipSource = rj->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(rj->u.object.values[rk].name,"ipDest"))&&(rj->u.object.values[rk].value->type == json_string))
-													rule.ipDest = rj->u.object.values[rk].value->u.string.ptr;
-												else if ((!strcmp(rj->u.object.values[rk].name,"ipTos"))&&(rj->u.object.values[rk].value->type == json_integer))
-													rule.ipTos = &(rj->u.object.values[rk].value->u.integer);
-												else if ((!strcmp(rj->u.object.values[rk].name,"ipProtocol"))&&(rj->u.object.values[rk].value->type == json_integer))
-													rule.ipProtocol = &(rj->u.object.values[rk].value->u.integer);
-												else if ((!strcmp(rj->u.object.values[rk].name,"ipSourcePort"))&&(rj->u.object.values[rk].value->type == json_integer))
-													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;
-											}
+					responseBody.append("],\n\t\"ipLocalRoutes\": [");
 
-											if ((rule.ruleNo)&&(rule.action)&&(rule.action[0])) {
-												char mactmp1[16],mactmp2[16];
-												sqlite3_reset(_sCreateRule);
-												sqlite3_bind_text(_sCreateRule,1,nwids,16,SQLITE_STATIC);
-												sqlite3_bind_int64(_sCreateRule,2,*rule.ruleNo);
+					sqlite3_reset(_sGetLocalRoutes);
+					sqlite3_bind_text(_sGetLocalRoutes,1,nwids,16,SQLITE_STATIC);
+					sqlite3_bind_int(_sGetLocalRoutes,2,(int)ZT_IP_ASSIGNMENT_TYPE_NETWORK);
+					bool firstLocalRoute = true;
+					while (sqlite3_step(_sGetLocalRoutes) == SQLITE_ROW) {
+						char tmp[128];
+						const unsigned char *ip = (const unsigned char *)sqlite3_column_blob(_sGetLocalRoutes,0);
+						switch (sqlite3_column_int(_sGetLocalRoutes,2)) {
+							case 4:
+								Utils::snprintf(tmp,sizeof(tmp),"%s%d.%d.%d.%d/%d\"",
+									(firstLocalRoute) ? "\"" : ",\"",
+									(int)ip[12],
+									(int)ip[13],
+									(int)ip[14],
+									(int)ip[15],
+									(int)sqlite3_column_int(_sGetLocalRoutes,1)); // netmask bits
+								break;
+							case 6:
+								Utils::snprintf(tmp,sizeof(tmp),"%s%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x/%d\"",
+									(firstLocalRoute) ? "\"" : ",\"",
+									(int)ip[0],
+									(int)ip[1],
+									(int)ip[2],
+									(int)ip[3],
+									(int)ip[4],
+									(int)ip[5],
+									(int)ip[6],
+									(int)ip[7],
+									(int)ip[8],
+									(int)ip[9],
+									(int)ip[10],
+									(int)ip[11],
+									(int)ip[12],
+									(int)ip[13],
+									(int)ip[14],
+									(int)ip[15],
+									(int)sqlite3_column_int(_sGetLocalRoutes,1)); // netmask bits
+								break;
+						}
+						responseBody.append(tmp);
+						firstLocalRoute = false;
+					}
 
-												// Optional values: null by default
-												for(int i=3;i<=18;++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.sourcePort)&&(strlen(rule.sourcePort) == 10)) sqlite3_bind_text(_sCreateRule,4,rule.sourcePort,10,SQLITE_STATIC);
-												if ((rule.destPort)&&(strlen(rule.destPort) == 10)) sqlite3_bind_text(_sCreateRule,5,rule.destPort,10,SQLITE_STATIC);
-												if (rule.vlanId) sqlite3_bind_int(_sCreateRule,6,(int)*rule.vlanId);
-												if (rule.vlanPcp) sqlite3_bind_int(_sCreateRule,7,(int)*rule.vlanPcp);
-												if (rule.etherType) sqlite3_bind_int(_sCreateRule,8,(int)*rule.etherType & (int)0xffff);
-												if (rule.macSource) {
-													MAC m(rule.macSource);
-													Utils::snprintf(mactmp1,sizeof(mactmp1),"%.12llx",(unsigned long long)m.toInt());
-													sqlite3_bind_text(_sCreateRule,9,mactmp1,-1,SQLITE_STATIC);
-												}
-												if (rule.macDest) {
-													MAC m(rule.macDest);
-													Utils::snprintf(mactmp2,sizeof(mactmp2),"%.12llx",(unsigned long long)m.toInt());
-													sqlite3_bind_text(_sCreateRule,10,mactmp2,-1,SQLITE_STATIC);
-												}
-												if (rule.ipSource) sqlite3_bind_text(_sCreateRule,11,rule.ipSource,-1,SQLITE_STATIC);
-												if (rule.ipDest) sqlite3_bind_text(_sCreateRule,12,rule.ipDest,-1,SQLITE_STATIC);
-												if (rule.ipTos) sqlite3_bind_int(_sCreateRule,13,(int)*rule.ipTos);
-												if (rule.ipProtocol) sqlite3_bind_int(_sCreateRule,14,(int)*rule.ipProtocol);
-												if (rule.ipSourcePort) sqlite3_bind_int(_sCreateRule,15,(int)*rule.ipSourcePort & (int)0xffff);
-												if (rule.ipDestPort) sqlite3_bind_int(_sCreateRule,16,(int)*rule.ipDestPort & (int)0xffff);
-												if (rule.flags) sqlite3_bind_int64(_sCreateRule,17,(int64_t)*rule.flags);
-												if (rule.invFlags) sqlite3_bind_int64(_sCreateRule,18,(int64_t)*rule.invFlags);
+					responseBody.append("],\n\t\"ipAssignmentPools\": [");
 
-												sqlite3_bind_text(_sCreateRule,19,rule.action,-1,SQLITE_STATIC);
-												sqlite3_step(_sCreateRule);
-											}
-										}
-									}
-								}
+					sqlite3_reset(_sGetIpAssignmentPools2);
+					sqlite3_bind_text(_sGetIpAssignmentPools2,1,nwids,16,SQLITE_STATIC);
+					bool firstIpAssignmentPool = true;
+					while (sqlite3_step(_sGetIpAssignmentPools2) == SQLITE_ROW) {
+						const char *ipRangeStartB = reinterpret_cast<const char *>(sqlite3_column_blob(_sGetIpAssignmentPools2,0));
+						const char *ipRangeEndB = reinterpret_cast<const char *>(sqlite3_column_blob(_sGetIpAssignmentPools2,1));
+						if ((ipRangeStartB)&&(ipRangeEndB)) {
+							InetAddress ipps,ippe;
+							int ipVersion = sqlite3_column_int(_sGetIpAssignmentPools2,2);
+							if (ipVersion == 4) {
+								ipps.set((const void *)(ipRangeStartB + 12),4,0);
+								ippe.set((const void *)(ipRangeEndB + 12),4,0);
+							} else if (ipVersion == 6) {
+								ipps.set((const void *)ipRangeStartB,16,0);
+								ippe.set((const void *)ipRangeEndB,16,0);
 							}
-
-							if (stmt) {
-								sqlite3_bind_text(stmt,2,nwids,16,SQLITE_STATIC);
-								sqlite3_step(stmt);
-								sqlite3_finalize(stmt);
+							if (ipps) {
+								responseBody.append(firstIpAssignmentPool ? "\n\t\t" : ",\n\t\t");
+								firstIpAssignmentPool = false;
+								Utils::snprintf(json,sizeof(json),"{\"ipRangeStart\":\"%s\",\"ipRangeEnd\":\"%s\"}",
+									_jsonEscape(ipps.toIpString()).c_str(),
+									_jsonEscape(ippe.toIpString()).c_str());
+								responseBody.append(json);
 							}
 						}
 					}
-					json_value_free(j);
-				}
 
-				sqlite3_reset(_sSetNetworkRevision);
-				sqlite3_bind_int64(_sSetNetworkRevision,1,revision += 1);
-				sqlite3_bind_text(_sSetNetworkRevision,2,nwids,16,SQLITE_STATIC);
-				sqlite3_step(_sSetNetworkRevision);
+					responseBody.append("],\n\t\"rules\": [");
 
-				return _doCPGet(path_copy,urlArgs,headers,body,responseBody,responseContentType);
-			}
+					sqlite3_reset(_sListRules);
+					sqlite3_bind_text(_sListRules,1,nwids,16,SQLITE_STATIC);
+					bool firstRule = true;
+					while (sqlite3_step(_sListRules) == SQLITE_ROW) {
+						responseBody.append(firstRule ? "\n\t{\n" : ",{\n");
+						firstRule = false;
+						Utils::snprintf(json,sizeof(json),"\t\t\"ruleNo\": %lld,\n",sqlite3_column_int64(_sListRules,0));
+						responseBody.append(json);
+						if (sqlite3_column_type(_sListRules,1) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"nodeId\": \"%s\",\n",(const char *)sqlite3_column_text(_sListRules,1));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,2) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"sourcePort\": \"%s\",\n",(const char *)sqlite3_column_text(_sListRules,2));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,3) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"destPort\": \"%s\",\n",(const char *)sqlite3_column_text(_sListRules,3));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,4) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"vlanId\": %d,\n",sqlite3_column_int(_sListRules,4));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,5) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"vlanPcp\": %d,\n",sqlite3_column_int(_sListRules,5));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,6) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"etherType\": %d,\n",sqlite3_column_int(_sListRules,6));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,7) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"macSource\": \"%s\",\n",MAC((const char *)sqlite3_column_text(_sListRules,7)).toString().c_str());
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,8) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"macDest\": \"%s\",\n",MAC((const char *)sqlite3_column_text(_sListRules,8)).toString().c_str());
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,9) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipSource\": \"%s\",\n",_jsonEscape((const char *)sqlite3_column_text(_sListRules,9)).c_str());
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,10) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipDest\": \"%s\",\n",_jsonEscape((const char *)sqlite3_column_text(_sListRules,10)).c_str());
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,11) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipTos\": %d,\n",sqlite3_column_int(_sListRules,11));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,12) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipProtocol\": %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\"ipSourcePort\": %d,\n",sqlite3_column_int(_sListRules,13));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,14) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipDestPort\": %d,\n",sqlite3_column_int(_sListRules,14));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,15) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"flags\": %lu,\n",(unsigned long)sqlite3_column_int64(_sListRules,15));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,16) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"invFlags\": %lu,\n",(unsigned long)sqlite3_column_int64(_sListRules,16));
+							responseBody.append(json);
+						}
+						responseBody.append("\t\t\"action\": \"");
+						responseBody.append(_jsonEscape( (sqlite3_column_type(_sListRules,17) == SQLITE_NULL) ? "drop" : (const char *)sqlite3_column_text(_sListRules,17) ));
+						responseBody.append("\"\n\t}");
+					}
 
+					responseBody.append("]\n}\n");
+					responseContentType = "application/json";
+					return 200;
+				} // else 404
+			}
+		} else if (path.size() == 1) {
+			// list networks
+			sqlite3_reset(_sListNetworks);
+			responseContentType = "application/json";
+			responseBody = "[";
+			bool first = true;
+			while (sqlite3_step(_sListNetworks) == SQLITE_ROW) {
+				if (first) {
+					first = false;
+					responseBody.push_back('"');
+				} else responseBody.append(",\"");
+				responseBody.append((const char *)sqlite3_column_text(_sListNetworks,0));
+				responseBody.push_back('"');
+			}
+			responseBody.push_back(']');
+			return 200;
 		} // else 404
 
-	} // else 404
+	} else {
+		// GET /controller returns status and API version if controller is supported
+		Utils::snprintf(json,sizeof(json),"{\n\t\"controller\": true,\n\t\"apiVersion\": %d,\n\t\"clock\": %llu,\n\t\"instanceId\": \"%s\"\n}\n",ZT_NETCONF_CONTROLLER_API_VERSION,(unsigned long long)OSUtils::now(),_instanceId.c_str());
+		responseBody = json;
+		responseContentType = "application/json";
+		return 200;
+	}
 
 	return 404;
 }
 
-unsigned int SqliteNetworkController::handleControlPlaneHttpDELETE(
-	const std::vector<std::string> &path,
-	const std::map<std::string,std::string> &urlArgs,
-	const std::map<std::string,std::string> &headers,
-	const std::string &body,
-	std::string &responseBody,
-	std::string &responseContentType)
+NetworkController::ResultCode SqliteNetworkController::_doNetworkConfigRequest(const InetAddress &fromAddr,const Identity &signingId,const Identity &identity,uint64_t nwid,const Dictionary &metaData,Dictionary &netconf)
 {
-	if (path.empty())
-		return 404;
-	Mutex::Lock _l(_lock);
-
-	if (path[0] == "network") {
-
-		if ((path.size() >= 2)&&(path[1].length() == 16)) {
-			uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
-			char nwids[24];
-			Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
-
-			sqlite3_reset(_sGetNetworkById);
-			sqlite3_bind_text(_sGetNetworkById,1,nwids,16,SQLITE_STATIC);
-			if (sqlite3_step(_sGetNetworkById) != SQLITE_ROW)
-				return 404;
-
-			if (path.size() >= 3) {
-
-				if ((path.size() == 4)&&(path[2] == "member")&&(path[3].length() == 10)) {
-					uint64_t address = Utils::hexStrToU64(path[3].c_str());
-					char addrs[24];
-					Utils::snprintf(addrs,sizeof(addrs),"%.10llx",address);
-
-					sqlite3_reset(_sGetMember);
-					sqlite3_bind_text(_sGetMember,1,nwids,16,SQLITE_STATIC);
-					sqlite3_bind_text(_sGetMember,2,addrs,10,SQLITE_STATIC);
-					if (sqlite3_step(_sGetMember) != SQLITE_ROW)
-						return 404;
-
-					sqlite3_reset(_sDeleteIpAllocations);
-					sqlite3_bind_text(_sDeleteIpAllocations,1,nwids,16,SQLITE_STATIC);
-					sqlite3_bind_text(_sDeleteIpAllocations,2,addrs,10,SQLITE_STATIC);
-					sqlite3_bind_int(_sDeleteIpAllocations,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
-					if (sqlite3_step(_sDeleteIpAllocations) == SQLITE_DONE) {
-						sqlite3_reset(_sDeleteMember);
-						sqlite3_bind_text(_sDeleteMember,1,nwids,16,SQLITE_STATIC);
-						sqlite3_bind_text(_sDeleteMember,2,addrs,10,SQLITE_STATIC);
-						if (sqlite3_step(_sDeleteMember) != SQLITE_DONE)
-							return 500;
-					} else return 500;
+	// Assumes _lock is locked
 
-					return 200;
-				}
+	// Decode some stuff from metaData
+	const unsigned int clientMajorVersion = (unsigned int)metaData.getHexUInt(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MAJOR_VERSION,0);
+	const unsigned int clientMinorVersion = (unsigned int)metaData.getHexUInt(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MINOR_VERSION,0);
+	const unsigned int clientRevision = (unsigned int)metaData.getHexUInt(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_REVISION,0);
+	const bool clientIs104 = (Utils::compareVersion(clientMajorVersion,clientMinorVersion,clientRevision,1,0,4) >= 0);
 
-			} else {
+	// Note: we can't reuse prepared statements that return const char * pointers without
+	// making our own copy in e.g. a std::string first.
 
-				sqlite3_reset(_sDeleteNetwork);
-				sqlite3_bind_text(_sDeleteNetwork,1,nwids,16,SQLITE_STATIC);
-				return ((sqlite3_step(_sDeleteNetwork) == SQLITE_DONE) ? 200 : 500);
+	if ((!signingId)||(!signingId.hasPrivate())) {
+		netconf["error"] = "signing identity invalid or lacks private key";
+		return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
+	}
+	if (signingId.address().toInt() != (nwid >> 24)) {
+		netconf["error"] = "signing identity address does not match most significant 40 bits of network ID";
+		return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
+	}
 
-			}
-		} // else 404
+	// Check rate limit
 
-	} // else 404
+	{
+		uint64_t &lrt = _lastRequestTime[std::pair<Address,uint64_t>(identity.address(),nwid)];
+		uint64_t lrt2 = lrt;
+		if (((lrt = OSUtils::now()) - lrt2) <= ZT_NETCONF_MIN_REQUEST_PERIOD)
+			return NetworkController::NETCONF_QUERY_IGNORE;
+	}
 
-	return 404;
-}
+	NetworkRecord network;
+	memset(&network,0,sizeof(network));
+	Utils::snprintf(network.id,sizeof(network.id),"%.16llx",(unsigned long long)nwid);
 
-unsigned int SqliteNetworkController::_doCPGet(
-	const std::vector<std::string> &path,
-	const std::map<std::string,std::string> &urlArgs,
-	const std::map<std::string,std::string> &headers,
-	const std::string &body,
-	std::string &responseBody,
-	std::string &responseContentType)
-{
-	// Assumes _lock is locked
-	char json[16384];
+	MemberRecord member;
+	memset(&member,0,sizeof(member));
+	Utils::snprintf(member.nodeId,sizeof(member.nodeId),"%.10llx",(unsigned long long)identity.address().toInt());
 
-	if ((path.size() > 0)&&(path[0] == "network")) {
+	// Create Node record or do full identity check if we already have one
 
-		if ((path.size() >= 2)&&(path[1].length() == 16)) {
-			uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
-			char nwids[24];
-			Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
+	sqlite3_reset(_sGetNodeIdentity);
+	sqlite3_bind_text(_sGetNodeIdentity,1,member.nodeId,10,SQLITE_STATIC);
+	if (sqlite3_step(_sGetNodeIdentity) == SQLITE_ROW) {
+		try {
+			Identity alreadyKnownIdentity((const char *)sqlite3_column_text(_sGetNodeIdentity,0));
+			if (alreadyKnownIdentity != identity)
+				return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
+		} catch ( ... ) { // identity stored in database is not valid or is NULL
+			return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
+		}
+	} else {
+		std::string idstr(identity.toString(false));
+		sqlite3_reset(_sCreateOrReplaceNode);
+		sqlite3_bind_text(_sCreateOrReplaceNode,1,member.nodeId,10,SQLITE_STATIC);
+		sqlite3_bind_text(_sCreateOrReplaceNode,2,idstr.c_str(),-1,SQLITE_STATIC);
+		if (sqlite3_step(_sCreateOrReplaceNode) != SQLITE_DONE) {
+			netconf["error"] = "unable to create new Node record";
+			return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
+		}
+	}
 
-			if (path.size() >= 3) {
-				// /network/<nwid>/...
+	// Fetch Network record
 
-				if (path[2] == "member") {
+	sqlite3_reset(_sGetNetworkById);
+	sqlite3_bind_text(_sGetNetworkById,1,network.id,16,SQLITE_STATIC);
+	if (sqlite3_step(_sGetNetworkById) == SQLITE_ROW) {
+		network.name = (const char *)sqlite3_column_text(_sGetNetworkById,0);
+		network.isPrivate = (sqlite3_column_int(_sGetNetworkById,1) > 0);
+		network.enableBroadcast = (sqlite3_column_int(_sGetNetworkById,2) > 0);
+		network.allowPassiveBridging = (sqlite3_column_int(_sGetNetworkById,3) > 0);
+		network.v4AssignMode = (const char *)sqlite3_column_text(_sGetNetworkById,4);
+		network.v6AssignMode = (const char *)sqlite3_column_text(_sGetNetworkById,5);
+		network.multicastLimit = sqlite3_column_int(_sGetNetworkById,6);
+		network.creationTime = (uint64_t)sqlite3_column_int64(_sGetNetworkById,7);
+		network.revision = (uint64_t)sqlite3_column_int64(_sGetNetworkById,8);
+		network.memberRevisionCounter = (uint64_t)sqlite3_column_int64(_sGetNetworkById,9);
+	} else {
+		return NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND;
+	}
 
-					if (path.size() >= 4) {
-						// Get specific member info
+	// Fetch Member record
 
-						uint64_t address = Utils::hexStrToU64(path[3].c_str());
-						char addrs[24];
-						Utils::snprintf(addrs,sizeof(addrs),"%.10llx",address);
+	bool foundMember = false;
+	sqlite3_reset(_sGetMember);
+	sqlite3_bind_text(_sGetMember,1,network.id,16,SQLITE_STATIC);
+	sqlite3_bind_text(_sGetMember,2,member.nodeId,10,SQLITE_STATIC);
+	if (sqlite3_step(_sGetMember) == SQLITE_ROW) {
+		foundMember = true;
+		member.rowid = (int64_t)sqlite3_column_int64(_sGetMember,0);
+		member.authorized = (sqlite3_column_int(_sGetMember,1) > 0);
+		member.activeBridge = (sqlite3_column_int(_sGetMember,2) > 0);
+	}
 
-						sqlite3_reset(_sGetMember2);
-						sqlite3_bind_text(_sGetMember2,1,nwids,16,SQLITE_STATIC);
-						sqlite3_bind_text(_sGetMember2,2,addrs,10,SQLITE_STATIC);
-						if (sqlite3_step(_sGetMember2) == SQLITE_ROW) {
-							const char *memberIdStr = (const char *)sqlite3_column_text(_sGetMember2,3);
+	// Create Member record for unknown nodes, auto-authorizing if network is public
 
-							// If testSingingId is included in the URL or X-ZT1-TestSigningId in the headers
-							// and if it contains an identity with a secret portion, the resturned JSON
-							// will contain an extra field called _testConf. This will contain several
-							// fields that report the result of doNetworkConfigRequest() for this member.
-							std::string testFields;
-							{
-								Identity testOutputSigningId;
-								std::map<std::string,std::string>::const_iterator sid(urlArgs.find("testSigningId"));
-								if (sid != urlArgs.end()) {
-									testOutputSigningId.fromString(sid->second.c_str());
-								} else {
-									sid = headers.find("x-zt1-testsigningid");
-									if (sid != headers.end())
-										testOutputSigningId.fromString(sid->second.c_str());
-								}
+	if (!foundMember) {
+		member.authorized = (network.isPrivate ? false : true);
+		member.activeBridge = false;
+		sqlite3_reset(_sCreateMember);
+		sqlite3_bind_text(_sCreateMember,1,network.id,16,SQLITE_STATIC);
+		sqlite3_bind_text(_sCreateMember,2,member.nodeId,10,SQLITE_STATIC);
+		sqlite3_bind_int(_sCreateMember,3,(member.authorized ? 1 : 0));
+		sqlite3_bind_text(_sCreateMember,4,network.id,16,SQLITE_STATIC);
+		if (sqlite3_step(_sCreateMember) != SQLITE_DONE) {
+			netconf["error"] = "unable to create new member record";
+			return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
+		}
+		member.rowid = (int64_t)sqlite3_last_insert_rowid(_db);
 
-								if ((testOutputSigningId.hasPrivate())&&(memberIdStr)) {
-									Dictionary testNetconf;
-									NetworkController::ResultCode rc = this->doNetworkConfigRequest(
-										InetAddress(),
-										testOutputSigningId,
-										Identity(memberIdStr),
-										nwid,
-										Dictionary(), // TODO: allow passing of meta-data for testing
-										testNetconf);
-									char rcs[16];
-									Utils::snprintf(rcs,sizeof(rcs),"%d,\n",(int)rc);
-									testFields.append("\t\"_test\": {\n");
-									testFields.append("\t\t\"resultCode\": "); testFields.append(rcs);
-									testFields.append("\t\t\"result\": \""); testFields.append(_jsonEscape(testNetconf.toString().c_str()).c_str()); testFields.append("\"");
-									testFields.append("\t}\n");
-								}
-							}
+		sqlite3_reset(_sIncrementMemberRevisionCounter);
+		sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,network.id,16,SQLITE_STATIC);
+		sqlite3_step(_sIncrementMemberRevisionCounter);
+	}
 
-							Utils::snprintf(json,sizeof(json),
-								"{\n%s"
-								"\t\"nwid\": \"%s\",\n"
-								"\t\"address\": \"%s\",\n"
-								"\t\"controllerInstanceId\": \"%s\",\n"
-								"\t\"authorized\": %s,\n"
-								"\t\"activeBridge\": %s,\n"
-								"\t\"memberRevision\": %llu,\n"
-								"\t\"clock\": %llu,\n"
-								"\t\"identity\": \"%s\",\n"
-								"\t\"ipAssignments\": [",
-								testFields.c_str(),
-								nwids,
-								addrs,
-								_instanceId.c_str(),
-								(sqlite3_column_int(_sGetMember2,0) > 0) ? "true" : "false",
-								(sqlite3_column_int(_sGetMember2,1) > 0) ? "true" : "false",
-								(unsigned long long)sqlite3_column_int64(_sGetMember2,2),
-								(unsigned long long)OSUtils::now(),
-								_jsonEscape(memberIdStr).c_str());
-							responseBody = json;
+	// Add log entry
+	{
+		char ver[16];
+		std::string fa;
+		if (fromAddr) {
+			fa = fromAddr.toString();
+			if (fa.length() > 64)
+				fa = fa.substr(0,64);
+		}
+		sqlite3_reset(_sPutLog);
+		sqlite3_bind_text(_sPutLog,1,network.id,16,SQLITE_STATIC);
+		sqlite3_bind_text(_sPutLog,2,member.nodeId,10,SQLITE_STATIC);
+		sqlite3_bind_int64(_sPutLog,3,(long long)OSUtils::now());
+		sqlite3_bind_int(_sPutLog,4,member.authorized ? 1 : 0);
+		if ((clientMajorVersion > 0)||(clientMinorVersion > 0)||(clientRevision > 0)) {
+			Utils::snprintf(ver,sizeof(ver),"%u.%u.%u",clientMajorVersion,clientMinorVersion,clientRevision);
+			sqlite3_bind_text(_sPutLog,5,ver,-1,SQLITE_STATIC);
+		} else sqlite3_bind_null(_sPutLog,5);
+		if (fa.length() > 0)
+			sqlite3_bind_text(_sPutLog,6,fa.c_str(),-1,SQLITE_STATIC);
+		else sqlite3_bind_null(_sPutLog,6);
+		sqlite3_step(_sPutLog);
+	}
 
-							sqlite3_reset(_sGetIpAssignmentsForNode2);
-							sqlite3_bind_text(_sGetIpAssignmentsForNode2,1,nwids,16,SQLITE_STATIC);
-							sqlite3_bind_text(_sGetIpAssignmentsForNode2,2,addrs,10,SQLITE_STATIC);
-							sqlite3_bind_int(_sGetIpAssignmentsForNode2,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
-							bool firstIp = true;
-							while (sqlite3_step(_sGetIpAssignmentsForNode2) == SQLITE_ROW) {
-								int ipversion = sqlite3_column_int(_sGetIpAssignmentsForNode2,2);
-								char ipBlob[16];
-								memcpy(ipBlob,(const void *)sqlite3_column_blob(_sGetIpAssignmentsForNode2,0),16);
-								InetAddress ip(
-									(const void *)(ipversion == 6 ? ipBlob : &ipBlob[12]),
-									(ipversion == 6 ? 16 : 4),
-									(unsigned int)sqlite3_column_int(_sGetIpAssignmentsForNode2,1)
-								);
-								responseBody.append(firstIp ? "\"" : ",\"");
-								firstIp = false;
-								responseBody.append(_jsonEscape(ip.toString()));
-								responseBody.push_back('"');
-							}
+	// Check member authorization
 
-							responseBody.append("],\n\t\"recentLog\": [");
+	if (!member.authorized)
+		return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
 
-							sqlite3_reset(_sGetRecentMemberLog);
-							sqlite3_bind_text(_sGetRecentMemberLog,1,nwids,16,SQLITE_STATIC);
-							sqlite3_bind_text(_sGetRecentMemberLog,2,addrs,10,SQLITE_STATIC);
-							bool firstLog = true;
-							while (sqlite3_step(_sGetRecentMemberLog) == SQLITE_ROW) {
-								responseBody.append(firstLog ? "{" : ",{");
-								firstLog = false;
-								responseBody.append("\"ts\":");
-								responseBody.append(reinterpret_cast<const char *>(sqlite3_column_text(_sGetRecentMemberLog,0)));
-								responseBody.append((sqlite3_column_int(_sGetRecentMemberLog,1) == 0) ? ",\"authorized\":false,\"version\":" : ",\"authorized\":true,\"version\":");
-								const char *ver = reinterpret_cast<const char *>(sqlite3_column_text(_sGetRecentMemberLog,2));
-								if ((ver)&&(ver[0])) {
-									responseBody.push_back('"');
-									responseBody.append(_jsonEscape(ver));
-									responseBody.append("\",\"fromAddr\":");
-								} else responseBody.append("null,\"fromAddr\":");
-								const char *fa = reinterpret_cast<const char *>(sqlite3_column_text(_sGetRecentMemberLog,3));
-								if ((fa)&&(fa[0])) {
-									responseBody.push_back('"');
-									responseBody.append(_jsonEscape(fa));
-									responseBody.append("\"}");
-								} else responseBody.append("null}");
-							}
+	// Create and sign netconf
 
-							responseBody.append("]\n}\n");
+	netconf.clear();
+	{
+		char tss[24],rs[24];
+		Utils::snprintf(tss,sizeof(tss),"%.16llx",(unsigned long long)OSUtils::now());
+		Utils::snprintf(rs,sizeof(rs),"%.16llx",(unsigned long long)network.revision);
+		netconf[ZT_NETWORKCONFIG_DICT_KEY_TIMESTAMP] = tss;
+		netconf[ZT_NETWORKCONFIG_DICT_KEY_REVISION] = rs;
+		netconf[ZT_NETWORKCONFIG_DICT_KEY_NETWORK_ID] = network.id;
+		netconf[ZT_NETWORKCONFIG_DICT_KEY_ISSUED_TO] = member.nodeId;
+		netconf[ZT_NETWORKCONFIG_DICT_KEY_PRIVATE] = network.isPrivate ? "1" : "0";
+		netconf[ZT_NETWORKCONFIG_DICT_KEY_NAME] = (network.name) ? network.name : "";
+		netconf[ZT_NETWORKCONFIG_DICT_KEY_ENABLE_BROADCAST] = network.enableBroadcast ? "1" : "0";
+		netconf[ZT_NETWORKCONFIG_DICT_KEY_ALLOW_PASSIVE_BRIDGING] = network.allowPassiveBridging ? "1" : "0";
 
-							responseContentType = "application/json";
-							return 200;
-						} // else 404
+		{	// TODO: right now only etherTypes are supported in rules
+			std::vector<int> allowedEtherTypes;
+			sqlite3_reset(_sGetEtherTypesFromRuleTable);
+			sqlite3_bind_text(_sGetEtherTypesFromRuleTable,1,network.id,16,SQLITE_STATIC);
+			while (sqlite3_step(_sGetEtherTypesFromRuleTable) == SQLITE_ROW) {
+				if (sqlite3_column_type(_sGetEtherTypesFromRuleTable,0) == SQLITE_NULL) {
+					allowedEtherTypes.clear();
+					allowedEtherTypes.push_back(0); // NULL 'allow' matches ANY
+					break;
+				} else {
+					int et = sqlite3_column_int(_sGetEtherTypesFromRuleTable,0);
+					if ((et >= 0)&&(et <= 0xffff))
+						allowedEtherTypes.push_back(et);
+				}
+			}
+			std::sort(allowedEtherTypes.begin(),allowedEtherTypes.end());
+			allowedEtherTypes.erase(std::unique(allowedEtherTypes.begin(),allowedEtherTypes.end()),allowedEtherTypes.end());
+			std::string allowedEtherTypesCsv;
+			for(std::vector<int>::const_iterator i(allowedEtherTypes.begin());i!=allowedEtherTypes.end();++i) {
+				if (allowedEtherTypesCsv.length())
+					allowedEtherTypesCsv.push_back(',');
+				char tmp[16];
+				Utils::snprintf(tmp,sizeof(tmp),"%.4x",(unsigned int)*i);
+				allowedEtherTypesCsv.append(tmp);
+			}
+			netconf[ZT_NETWORKCONFIG_DICT_KEY_ALLOWED_ETHERNET_TYPES] = allowedEtherTypesCsv;
+		}
 
-					} else {
-						// List members
+		if (network.multicastLimit > 0) {
+			char ml[16];
+			Utils::snprintf(ml,sizeof(ml),"%lx",(unsigned long)network.multicastLimit);
+			netconf[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_LIMIT] = ml;
+		}
 
-						sqlite3_reset(_sListNetworkMembers);
-						sqlite3_bind_text(_sListNetworkMembers,1,nwids,16,SQLITE_STATIC);
-						responseBody.append("{");
-						bool firstMember = true;
-						while (sqlite3_step(_sListNetworkMembers) == SQLITE_ROW) {
-							responseBody.append(firstMember ? "\"" : ",\"");
-							firstMember = false;
-							responseBody.append((const char *)sqlite3_column_text(_sListNetworkMembers,0));
-							responseBody.append("\":");
-							responseBody.append((const char *)sqlite3_column_text(_sListNetworkMembers,1));
-						}
-						responseBody.push_back('}');
-						responseContentType = "application/json";
-						return 200;
+		{
+			std::string activeBridges;
+			sqlite3_reset(_sGetActiveBridges);
+			sqlite3_bind_text(_sGetActiveBridges,1,network.id,16,SQLITE_STATIC);
+			while (sqlite3_step(_sGetActiveBridges) == SQLITE_ROW) {
+				const char *ab = (const char *)sqlite3_column_text(_sGetActiveBridges,0);
+				if ((ab)&&(strlen(ab) == 10)) {
+					if (activeBridges.length())
+						activeBridges.push_back(',');
+					activeBridges.append(ab);
+				}
+				if (activeBridges.length() > 1024) // sanity check -- you can't have too many active bridges at the moment
+					break;
+			}
+			if (activeBridges.length())
+				netconf[ZT_NETWORKCONFIG_DICT_KEY_ACTIVE_BRIDGES] = activeBridges;
+		}
 
+		{
+			std::string relays;
+			sqlite3_reset(_sGetRelays);
+			sqlite3_bind_text(_sGetRelays,1,network.id,16,SQLITE_STATIC);
+			while (sqlite3_step(_sGetRelays) == SQLITE_ROW) {
+				const char *n = (const char *)sqlite3_column_text(_sGetRelays,0);
+				const char *a = (const char *)sqlite3_column_text(_sGetRelays,1);
+				if ((n)&&(a)) {
+					Address node(n);
+					InetAddress addr(a);
+					if ((node)&&(addr)) {
+						if (relays.length())
+							relays.push_back(',');
+						relays.append(node.toString());
+						relays.push_back(';');
+						relays.append(addr.toString());
 					}
+				}
+			}
+			if (relays.length())
+				netconf[ZT_NETWORKCONFIG_DICT_KEY_RELAYS] = relays;
+		}
 
-				} // else 404
+		{
+			char tmp[128];
+			std::string gateways;
+			sqlite3_reset(_sGetGateways);
+			sqlite3_bind_text(_sGetGateways,1,network.id,16,SQLITE_STATIC);
+			while (sqlite3_step(_sGetGateways) == SQLITE_ROW) {
+				const unsigned char *ip = (const unsigned char *)sqlite3_column_blob(_sGetGateways,0);
+				switch(sqlite3_column_int(_sGetGateways,1)) { // ipVersion
+					case 4:
+						Utils::snprintf(tmp,sizeof(tmp),"%s%d.%d.%d.%d/%d",
+							(gateways.length() > 0) ? "," : "",
+							(int)ip[12],
+							(int)ip[13],
+							(int)ip[14],
+							(int)ip[15],
+							(int)sqlite3_column_int(_sGetGateways,2)); // metric
+						gateways.append(tmp);
+						break;
+					case 6:
+						Utils::snprintf(tmp,sizeof(tmp),"%s%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x/%d",
+							(gateways.length() > 0) ? "," : "",
+							(int)ip[0],
+							(int)ip[1],
+							(int)ip[2],
+							(int)ip[3],
+							(int)ip[4],
+							(int)ip[5],
+							(int)ip[6],
+							(int)ip[7],
+							(int)ip[8],
+							(int)ip[9],
+							(int)ip[10],
+							(int)ip[11],
+							(int)ip[12],
+							(int)ip[13],
+							(int)ip[14],
+							(int)ip[15],
+							(int)sqlite3_column_int(_sGetGateways,2)); // metric
+						gateways.append(tmp);
+						break;
+				}
+			}
+			if (gateways.length())
+				netconf[ZT_NETWORKCONFIG_DICT_KEY_GATEWAYS] = gateways;
+		}
 
-			} else {
-				// get network info
-				sqlite3_reset(_sGetNetworkById);
-				sqlite3_bind_text(_sGetNetworkById,1,nwids,16,SQLITE_STATIC);
-				if (sqlite3_step(_sGetNetworkById) == SQLITE_ROW) {
-					Utils::snprintf(json,sizeof(json),
-						"{\n"
-						"\t\"nwid\": \"%s\",\n"
-						"\t\"controllerInstanceId\": \"%s\",\n"
-						"\t\"clock\": %llu,\n"
-						"\t\"name\": \"%s\",\n"
-						"\t\"private\": %s,\n"
-						"\t\"enableBroadcast\": %s,\n"
-						"\t\"allowPassiveBridging\": %s,\n"
-						"\t\"v4AssignMode\": \"%s\",\n"
-						"\t\"v6AssignMode\": \"%s\",\n"
-						"\t\"multicastLimit\": %d,\n"
-						"\t\"creationTime\": %llu,\n"
-						"\t\"revision\": %llu,\n"
-						"\t\"memberRevisionCounter\": %llu,\n"
-						"\t\"authorizedMemberCount\": %llu,\n"
-						"\t\"relays\": [",
-						nwids,
-						_instanceId.c_str(),
-						(unsigned long long)OSUtils::now(),
-						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,0)).c_str(),
-						(sqlite3_column_int(_sGetNetworkById,1) > 0) ? "true" : "false",
-						(sqlite3_column_int(_sGetNetworkById,2) > 0) ? "true" : "false",
-						(sqlite3_column_int(_sGetNetworkById,3) > 0) ? "true" : "false",
-						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,4)).c_str(),
-						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,5)).c_str(),
-						sqlite3_column_int(_sGetNetworkById,6),
-						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,7),
-						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,8),
-						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,9),
-						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,10));
-					responseBody = json;
+		if ((network.v4AssignMode)&&(!strcmp(network.v4AssignMode,"zt"))) {
+			std::string v4s;
 
-					sqlite3_reset(_sGetRelays);
-					sqlite3_bind_text(_sGetRelays,1,nwids,16,SQLITE_STATIC);
-					bool firstRelay = true;
-					while (sqlite3_step(_sGetRelays) == SQLITE_ROW) {
-						responseBody.append(firstRelay ? "\n\t\t" : ",\n\t\t");
-						firstRelay = false;
-						responseBody.append("{\"address\":\"");
-						responseBody.append((const char *)sqlite3_column_text(_sGetRelays,0));
-						responseBody.append("\",\"phyAddress\":\"");
-						responseBody.append(_jsonEscape((const char *)sqlite3_column_text(_sGetRelays,1)));
-						responseBody.append("\"}");
-					}
+			// Get existing IPv4 IP assignments and network routes -- keep routes in a
+			// vector for use in auto-assign if we need them.
+			std::vector< std::pair<uint32_t,int> > routedNetworks;
+			bool haveStaticIpAssignment = false;
+			sqlite3_reset(_sGetIpAssignmentsForNode);
+			sqlite3_bind_text(_sGetIpAssignmentsForNode,1,network.id,16,SQLITE_STATIC);
+			sqlite3_bind_text(_sGetIpAssignmentsForNode,2,member.nodeId,10,SQLITE_STATIC);
+			sqlite3_bind_int(_sGetIpAssignmentsForNode,3,4); // 4 == IPv4
+			while (sqlite3_step(_sGetIpAssignmentsForNode) == SQLITE_ROW) {
+				const unsigned char *ip = (const unsigned char *)sqlite3_column_blob(_sGetIpAssignmentsForNode,1);
+				if ((!ip)||(sqlite3_column_bytes(_sGetIpAssignmentsForNode,1) != 16))
+					continue;
+				int ipNetmaskBits = sqlite3_column_int(_sGetIpAssignmentsForNode,2);
+				if ((ipNetmaskBits <= 0)||(ipNetmaskBits > 32))
+					continue;
 
-					responseBody.append("],\n\t\"gateways\": [");
+				const IpAssignmentType ipt = (IpAssignmentType)sqlite3_column_int(_sGetIpAssignmentsForNode,0);
+				switch(ipt) {
+					case ZT_IP_ASSIGNMENT_TYPE_ADDRESS:
+						haveStaticIpAssignment = true;
+						break;
+					case ZT_IP_ASSIGNMENT_TYPE_NETWORK:
+						routedNetworks.push_back(std::pair<uint32_t,int>(Utils::ntoh(*(reinterpret_cast<const uint32_t *>(ip + 12))),ipNetmaskBits));
+						break;
+					default:
+						continue;
+				}
 
-					sqlite3_reset(_sGetGateways);
-					sqlite3_bind_text(_sGetGateways,1,nwids,16,SQLITE_STATIC);
-					bool firstGateway = true;
-					while (sqlite3_step(_sGetGateways) == SQLITE_ROW) {
-						char tmp[128];
-						const unsigned char *ip = (const unsigned char *)sqlite3_column_blob(_sGetGateways,0);
-						switch(sqlite3_column_int(_sGetGateways,1)) { // ipVersion
-							case 4:
-								Utils::snprintf(tmp,sizeof(tmp),"%s%d.%d.%d.%d/%d\"",
-									(firstGateway) ? "\"" : ",\"",
-									(int)ip[12],
-									(int)ip[13],
-									(int)ip[14],
-									(int)ip[15],
-									(int)sqlite3_column_int(_sGetGateways,2)); // metric
-								break;
-							case 6:
-								Utils::snprintf(tmp,sizeof(tmp),"%s%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x/%d\"",
-									(firstGateway) ? "\"" : ",\"",
-									(int)ip[0],
-									(int)ip[1],
-									(int)ip[2],
-									(int)ip[3],
-									(int)ip[4],
-									(int)ip[5],
-									(int)ip[6],
-									(int)ip[7],
-									(int)ip[8],
-									(int)ip[9],
-									(int)ip[10],
-									(int)ip[11],
-									(int)ip[12],
-									(int)ip[13],
-									(int)ip[14],
-									(int)ip[15],
-									(int)sqlite3_column_int(_sGetGateways,2)); // metric
-								break;
-						}
-						responseBody.append(tmp);
-						firstGateway = false;
-					}
+				// 1.0.4 or newer clients support network routes in addition to IPs.
+				// Older clients only support IP address / netmask entries.
+				if ((clientIs104)||(ipt == ZT_IP_ASSIGNMENT_TYPE_ADDRESS)) {
+					char tmp[32];
+					Utils::snprintf(tmp,sizeof(tmp),"%d.%d.%d.%d/%d",(int)ip[12],(int)ip[13],(int)ip[14],(int)ip[15],ipNetmaskBits);
+					if (v4s.length())
+						v4s.push_back(',');
+					v4s.append(tmp);
+				}
+			}
 
-					responseBody.append("],\n\t\"ipLocalRoutes\": [");
+			if (!haveStaticIpAssignment) {
+				// Attempt to auto-assign an IPv4 address from an available routed pool
+				sqlite3_reset(_sGetIpAssignmentPools);
+				sqlite3_bind_text(_sGetIpAssignmentPools,1,network.id,16,SQLITE_STATIC);
+				sqlite3_bind_int(_sGetIpAssignmentPools,2,4); // 4 == IPv4
 
-					sqlite3_reset(_sGetLocalRoutes);
-					sqlite3_bind_text(_sGetLocalRoutes,1,nwids,16,SQLITE_STATIC);
-					sqlite3_bind_int(_sGetLocalRoutes,2,(int)ZT_IP_ASSIGNMENT_TYPE_NETWORK);
-					bool firstLocalRoute = true;
-					while (sqlite3_step(_sGetLocalRoutes) == SQLITE_ROW) {
-						char tmp[128];
-						const unsigned char *ip = (const unsigned char *)sqlite3_column_blob(_sGetLocalRoutes,0);
-						switch (sqlite3_column_int(_sGetLocalRoutes,2)) {
-							case 4:
-								Utils::snprintf(tmp,sizeof(tmp),"%s%d.%d.%d.%d/%d\"",
-									(firstLocalRoute) ? "\"" : ",\"",
-									(int)ip[12],
-									(int)ip[13],
-									(int)ip[14],
-									(int)ip[15],
-									(int)sqlite3_column_int(_sGetLocalRoutes,1)); // netmask bits
-								break;
-							case 6:
-								Utils::snprintf(tmp,sizeof(tmp),"%s%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x/%d\"",
-									(firstLocalRoute) ? "\"" : ",\"",
-									(int)ip[0],
-									(int)ip[1],
-									(int)ip[2],
-									(int)ip[3],
-									(int)ip[4],
-									(int)ip[5],
-									(int)ip[6],
-									(int)ip[7],
-									(int)ip[8],
-									(int)ip[9],
-									(int)ip[10],
-									(int)ip[11],
-									(int)ip[12],
-									(int)ip[13],
-									(int)ip[14],
-									(int)ip[15],
-									(int)sqlite3_column_int(_sGetLocalRoutes,1)); // netmask bits
-								break;
-						}
-						responseBody.append(tmp);
-						firstLocalRoute = false;
-					}
+				while (sqlite3_step(_sGetIpAssignmentPools) == SQLITE_ROW) {
+					const unsigned char *ipRangeStartB = reinterpret_cast<const unsigned char *>(sqlite3_column_blob(_sGetIpAssignmentPools,0));
+					const unsigned char *ipRangeEndB = reinterpret_cast<const unsigned char *>(sqlite3_column_blob(_sGetIpAssignmentPools,1));
+					if ((!ipRangeStartB)||(!ipRangeEndB)||(sqlite3_column_bytes(_sGetIpAssignmentPools,0) != 16)||(sqlite3_column_bytes(_sGetIpAssignmentPools,1) != 16))
+						continue;
 
-					responseBody.append("],\n\t\"ipAssignmentPools\": [");
+					uint32_t ipRangeStart = Utils::ntoh(*(reinterpret_cast<const uint32_t *>(ipRangeStartB + 12)));
+					uint32_t ipRangeEnd = Utils::ntoh(*(reinterpret_cast<const uint32_t *>(ipRangeEndB + 12)));
+					if (ipRangeEnd < ipRangeStart)
+						continue;
+					uint32_t ipRangeLen = ipRangeEnd - ipRangeStart;
 
-					sqlite3_reset(_sGetIpAssignmentPools2);
-					sqlite3_bind_text(_sGetIpAssignmentPools2,1,nwids,16,SQLITE_STATIC);
-					bool firstIpAssignmentPool = true;
-					while (sqlite3_step(_sGetIpAssignmentPools2) == SQLITE_ROW) {
-						const char *ipRangeStartB = reinterpret_cast<const char *>(sqlite3_column_blob(_sGetIpAssignmentPools2,0));
-						const char *ipRangeEndB = reinterpret_cast<const char *>(sqlite3_column_blob(_sGetIpAssignmentPools2,1));
-						if ((ipRangeStartB)&&(ipRangeEndB)) {
-							InetAddress ipps,ippe;
-							int ipVersion = sqlite3_column_int(_sGetIpAssignmentPools2,2);
-							if (ipVersion == 4) {
-								ipps.set((const void *)(ipRangeStartB + 12),4,0);
-								ippe.set((const void *)(ipRangeEndB + 12),4,0);
-							} else if (ipVersion == 6) {
-								ipps.set((const void *)ipRangeStartB,16,0);
-								ippe.set((const void *)ipRangeEndB,16,0);
-							}
-							if (ipps) {
-								responseBody.append(firstIpAssignmentPool ? "\n\t\t" : ",\n\t\t");
-								firstIpAssignmentPool = false;
-								Utils::snprintf(json,sizeof(json),"{\"ipRangeStart\":\"%s\",\"ipRangeEnd\":\"%s\"}",
-									_jsonEscape(ipps.toIpString()).c_str(),
-									_jsonEscape(ippe.toIpString()).c_str());
-								responseBody.append(json);
+					// Start with the LSB of the member's address
+					uint32_t ipTrialCounter = (uint32_t)(identity.address().toInt() & 0xffffffff);
+
+					for(uint32_t k=ipRangeStart,l=0;(k<=ipRangeEnd)&&(l < 1000000);++k,++l) {
+						uint32_t ip = (ipRangeLen > 0) ? (ipRangeStart + (ipTrialCounter % ipRangeLen)) : ipRangeStart;
+						++ipTrialCounter;
+
+						for(std::vector< std::pair<uint32_t,int> >::const_iterator r(routedNetworks.begin());r!=routedNetworks.end();++r) {
+							if ((ip & (0xffffffff << (32 - r->second))) == r->first) {
+								// IP is included in a routed network, so check if it's allocated
+
+								uint32_t ipBlob[4];
+								ipBlob[0] = 0; ipBlob[1] = 0; ipBlob[2] = 0; ipBlob[3] = Utils::hton(ip);
+
+								sqlite3_reset(_sCheckIfIpIsAllocated);
+								sqlite3_bind_text(_sCheckIfIpIsAllocated,1,network.id,16,SQLITE_STATIC);
+								sqlite3_bind_blob(_sCheckIfIpIsAllocated,2,(const void *)ipBlob,16,SQLITE_STATIC);
+								sqlite3_bind_int(_sCheckIfIpIsAllocated,3,4); // 4 == IPv4
+								sqlite3_bind_int(_sCheckIfIpIsAllocated,4,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
+								if (sqlite3_step(_sCheckIfIpIsAllocated) != SQLITE_ROW) {
+									// No rows returned, so the IP is available
+									sqlite3_reset(_sAllocateIp);
+									sqlite3_bind_text(_sAllocateIp,1,network.id,16,SQLITE_STATIC);
+									sqlite3_bind_text(_sAllocateIp,2,member.nodeId,10,SQLITE_STATIC);
+									sqlite3_bind_int(_sAllocateIp,3,(int)ZT_IP_ASSIGNMENT_TYPE_ADDRESS);
+									sqlite3_bind_blob(_sAllocateIp,4,(const void *)ipBlob,16,SQLITE_STATIC);
+									sqlite3_bind_int(_sAllocateIp,5,r->second); // IP netmask bits from matching route
+									sqlite3_bind_int(_sAllocateIp,6,4); // 4 == IPv4
+									if (sqlite3_step(_sAllocateIp) == SQLITE_DONE) {
+										char tmp[32];
+										Utils::snprintf(tmp,sizeof(tmp),"%d.%d.%d.%d/%d",(int)((ip >> 24) & 0xff),(int)((ip >> 16) & 0xff),(int)((ip >> 8) & 0xff),(int)(ip & 0xff),r->second);
+										if (v4s.length())
+											v4s.push_back(',');
+										v4s.append(tmp);
+										haveStaticIpAssignment = true; // break outer loop
+									}
+								}
+
+								break; // stop checking routed networks
 							}
 						}
+
+						if (haveStaticIpAssignment)
+							break;
 					}
+				}
+			}
 
-					responseBody.append("],\n\t\"rules\": [");
+			if (v4s.length())
+				netconf[ZT_NETWORKCONFIG_DICT_KEY_IPV4_STATIC] = v4s;
+		}
 
-					sqlite3_reset(_sListRules);
-					sqlite3_bind_text(_sListRules,1,nwids,16,SQLITE_STATIC);
-					bool firstRule = true;
-					while (sqlite3_step(_sListRules) == SQLITE_ROW) {
-						responseBody.append(firstRule ? "\n\t{\n" : ",{\n");
-						firstRule = false;
-						Utils::snprintf(json,sizeof(json),"\t\t\"ruleNo\": %lld,\n",sqlite3_column_int64(_sListRules,0));
-						responseBody.append(json);
-						if (sqlite3_column_type(_sListRules,1) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"nodeId\": \"%s\",\n",(const char *)sqlite3_column_text(_sListRules,1));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,2) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"sourcePort\": \"%s\",\n",(const char *)sqlite3_column_text(_sListRules,2));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,3) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"destPort\": \"%s\",\n",(const char *)sqlite3_column_text(_sListRules,3));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,4) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"vlanId\": %d,\n",sqlite3_column_int(_sListRules,4));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,5) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"vlanPcp\": %d,\n",sqlite3_column_int(_sListRules,5));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,6) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"etherType\": %d,\n",sqlite3_column_int(_sListRules,6));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,7) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"macSource\": \"%s\",\n",MAC((const char *)sqlite3_column_text(_sListRules,7)).toString().c_str());
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,8) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"macDest\": \"%s\",\n",MAC((const char *)sqlite3_column_text(_sListRules,8)).toString().c_str());
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,9) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipSource\": \"%s\",\n",_jsonEscape((const char *)sqlite3_column_text(_sListRules,9)).c_str());
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,10) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipDest\": \"%s\",\n",_jsonEscape((const char *)sqlite3_column_text(_sListRules,10)).c_str());
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,11) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipTos\": %d,\n",sqlite3_column_int(_sListRules,11));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,12) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipProtocol\": %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\"ipSourcePort\": %d,\n",sqlite3_column_int(_sListRules,13));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,14) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipDestPort\": %d,\n",sqlite3_column_int(_sListRules,14));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,15) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"flags\": %lu,\n",(unsigned long)sqlite3_column_int64(_sListRules,15));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,16) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"invFlags\": %lu,\n",(unsigned long)sqlite3_column_int64(_sListRules,16));
-							responseBody.append(json);
-						}
-						responseBody.append("\t\t\"action\": \"");
-						responseBody.append(_jsonEscape( (sqlite3_column_type(_sListRules,17) == SQLITE_NULL) ? "drop" : (const char *)sqlite3_column_text(_sListRules,17) ));
-						responseBody.append("\"\n\t}");
-					}
+		// TODO: IPv6 auto-assign once it's supported in UI
 
-					responseBody.append("]\n}\n");
-					responseContentType = "application/json";
-					return 200;
-				} // else 404
-			}
-		} else if (path.size() == 1) {
-			// list networks
-			sqlite3_reset(_sListNetworks);
-			responseContentType = "application/json";
-			responseBody = "[";
-			bool first = true;
-			while (sqlite3_step(_sListNetworks) == SQLITE_ROW) {
-				if (first) {
-					first = false;
-					responseBody.push_back('"');
-				} else responseBody.append(",\"");
-				responseBody.append((const char *)sqlite3_column_text(_sListNetworks,0));
-				responseBody.push_back('"');
+		if (network.isPrivate) {
+			CertificateOfMembership com(OSUtils::now(),ZT_NETWORK_AUTOCONF_DELAY + (ZT_NETWORK_AUTOCONF_DELAY / 2),nwid,identity.address());
+			if (com.sign(signingId)) // basically can't fail unless our identity is invalid
+				netconf[ZT_NETWORKCONFIG_DICT_KEY_CERTIFICATE_OF_MEMBERSHIP] = com.toString();
+			else {
+				netconf["error"] = "unable to sign COM";
+				return NETCONF_QUERY_INTERNAL_SERVER_ERROR;
 			}
-			responseBody.push_back(']');
-			return 200;
-		} // else 404
+		}
 
-	} else {
-		// GET /controller returns status and API version if controller is supported
-		Utils::snprintf(json,sizeof(json),"{\n\t\"controller\": true,\n\t\"apiVersion\": %d,\n\t\"clock\": %llu,\n\t\"instanceId\": \"%s\"\n}\n",ZT_NETCONF_CONTROLLER_API_VERSION,(unsigned long long)OSUtils::now(),_instanceId.c_str());
-		responseBody = json;
-		responseContentType = "application/json";
-		return 200;
+		if (!netconf.sign(signingId,OSUtils::now())) {
+			netconf["error"] = "unable to sign netconf dictionary";
+			return NETCONF_QUERY_INTERNAL_SERVER_ERROR;
+		}
 	}
 
-	return 404;
+	return NetworkController::NETCONF_QUERY_OK;
 }
 
 } // namespace ZeroTier

+ 7 - 0
controller/SqliteNetworkController.hpp

@@ -93,6 +93,13 @@ private:
 		const std::string &body,
 		std::string &responseBody,
 		std::string &responseContentType);
+	NetworkController::ResultCode _doNetworkConfigRequest(
+		const InetAddress &fromAddr,
+		const Identity &signingId,
+		const Identity &identity,
+		uint64_t nwid,
+		const Dictionary &metaData,
+		Dictionary &netconf);
 
 	std::string _dbPath;
 	std::string _instanceId;