From 53c7f61f985bdaba2aff3f055e21a8b4f63a1b2c Mon Sep 17 00:00:00 2001 From: Kees Bos Date: Sun, 5 Jul 2015 13:27:27 +0200 Subject: Fix for output of empty (no members) network --- controller/SqliteNetworkController.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'controller/SqliteNetworkController.cpp') diff --git a/controller/SqliteNetworkController.cpp b/controller/SqliteNetworkController.cpp index 700b3561..79bf75b6 100644 --- a/controller/SqliteNetworkController.cpp +++ b/controller/SqliteNetworkController.cpp @@ -1394,8 +1394,11 @@ unsigned int SqliteNetworkController::_doCPGet( 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((responseBody.length() > 0) ? ",\"" : "{\""); + 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)); -- cgit v1.2.3 From e2a2993b186c521f9521d1a9adeb150d27c15629 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 22 Jul 2015 14:01:49 -0700 Subject: Add a Log table to log queries for debugging and security logging. No JSON API support for querying the log yet, but will probably come via /network/###/member/###/log/... or something. --- controller/SqliteNetworkController.cpp | 32 +++++++++++++++++++++++++++++--- controller/SqliteNetworkController.hpp | 3 +++ controller/schema.sql | 11 +++++++++++ controller/schema.sql.c | 11 +++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) (limited to 'controller/SqliteNetworkController.cpp') diff --git a/controller/SqliteNetworkController.cpp b/controller/SqliteNetworkController.cpp index 79bf75b6..85dbed00 100644 --- a/controller/SqliteNetworkController.cpp +++ b/controller/SqliteNetworkController.cpp @@ -142,6 +142,7 @@ SqliteNetworkController::SqliteNetworkController(const char *dbPath) : // Prepare statement will fail if Config table doesn't exist, which means our DB // needs to be initialized. if (sqlite3_exec(_db,ZT_NETCONF_SCHEMA_SQL"INSERT INTO Config (k,v) VALUES ('schemaVersion',"ZT_NETCONF_SQLITE_SCHEMA_VERSION_STR");",0,0,0) != SQLITE_OK) { + //printf("%s\n",sqlite3_errmsg(_db)); sqlite3_close(_db); throw std::runtime_error("SqliteNetworkController cannot initialize database and/or insert schemaVersion into Config table"); } @@ -199,16 +200,20 @@ SqliteNetworkController::SqliteNetworkController(const char *dbPath) : ||(sqlite3_prepare_v2(_db,"DELETE FROM Member WHERE networkId = ? AND nodeId = ?",-1,&_sDeleteMember,(const char **)0) != SQLITE_OK) /* Gateway */ - ||(sqlite3_prepare_v2(_db,"SELECT ip,ipVersion,metric FROM Gateway WHERE networkId = ? ORDER BY metric ASC",-1,&_sGetGateways,(const char **)0) != SQLITE_OK) + ||(sqlite3_prepare_v2(_db,"SELECT \"ip\",ipVersion,metric FROM Gateway WHERE networkId = ? ORDER BY metric ASC",-1,&_sGetGateways,(const char **)0) != SQLITE_OK) ||(sqlite3_prepare_v2(_db,"DELETE FROM Gateway WHERE networkId = ?",-1,&_sDeleteGateways,(const char **)0) != SQLITE_OK) - ||(sqlite3_prepare_v2(_db,"INSERT INTO Gateway (networkId,ip,ipVersion,metric) VALUES (?,?,?,?)",-1,&_sCreateGateway,(const char **)0) != SQLITE_OK) + ||(sqlite3_prepare_v2(_db,"INSERT INTO Gateway (networkId,\"ip\",ipVersion,metric) VALUES (?,?,?,?)",-1,&_sCreateGateway,(const char **)0) != SQLITE_OK) + + /* Log */ + ||(sqlite3_prepare_v2(_db,"INSERT INTO \"Log\" (networkId,nodeId,\"ts\",\"authorized\",fromAddr) VALUES (?,?,?,?,?)",-1,&_sPutLog,(const char **)0) != SQLITE_OK) + ||(sqlite3_prepare_v2(_db,"SELECT \"ts\",\"authorized\",fromAddr FROM \"Log\" WHERE networkId = ? AND nodeId = ? AND \"ts\" >= ? ORDER BY \"ts\" ASC",-1,&_sGetMemberLog,(const char **)0) != SQLITE_OK) /* Config */ ||(sqlite3_prepare_v2(_db,"SELECT \"v\" FROM \"Config\" WHERE \"k\" = ?",-1,&_sGetConfig,(const char **)0) != SQLITE_OK) ||(sqlite3_prepare_v2(_db,"INSERT OR REPLACE INTO \"Config\" (\"k\",\"v\") VALUES (?,?)",-1,&_sSetConfig,(const char **)0) != SQLITE_OK) ) { - //printf("!!! %s\n",sqlite3_errmsg(_db)); + //printf("%s\n",sqlite3_errmsg(_db)); sqlite3_close(_db); throw std::runtime_error("SqliteNetworkController unable to initialize one or more prepared statements"); } @@ -283,6 +288,8 @@ SqliteNetworkController::~SqliteNetworkController() sqlite3_finalize(_sIncrementMemberRevisionCounter); sqlite3_finalize(_sGetConfig); sqlite3_finalize(_sSetConfig); + sqlite3_finalize(_sPutLog); + sqlite3_finalize(_sGetMemberLog); sqlite3_close(_db); } } @@ -387,6 +394,25 @@ NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(co sqlite3_step(_sIncrementMemberRevisionCounter); } + // Add log entry + { + 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 (fa.length() > 0) + sqlite3_bind_text(_sPutLog,5,fa.c_str(),-1,SQLITE_STATIC); + else sqlite3_bind_null(_sPutLog,5); + sqlite3_step(_sPutLog); + } + // Check member authorization if (!member.authorized) diff --git a/controller/SqliteNetworkController.hpp b/controller/SqliteNetworkController.hpp index 8b39f7d9..bae11519 100644 --- a/controller/SqliteNetworkController.hpp +++ b/controller/SqliteNetworkController.hpp @@ -97,6 +97,7 @@ private: std::string _dbPath; std::string _instanceId; + sqlite3 *_db; sqlite3_stmt *_sGetNetworkById; @@ -141,6 +142,8 @@ private: sqlite3_stmt *_sIncrementMemberRevisionCounter; sqlite3_stmt *_sGetConfig; sqlite3_stmt *_sSetConfig; + sqlite3_stmt *_sPutLog; + sqlite3_stmt *_sGetMemberLog; Mutex _lock; }; diff --git a/controller/schema.sql b/controller/schema.sql index e85785b7..024a5229 100644 --- a/controller/schema.sql +++ b/controller/schema.sql @@ -65,6 +65,17 @@ CREATE TABLE Member ( CREATE INDEX Member_networkId_activeBridge ON Member(networkId, activeBridge); CREATE INDEX Member_networkId_memberRevision ON Member(networkId, memberRevision); +CREATE TABLE Log ( + networkId char(16) NOT NULL, + nodeId char(10) NOT NULL, + ts integer NOT NULL, + authorized integer NOT NULL, + fromAddr varchar(64) +); + +CREATE INDEX Log_networkId_nodeId ON Log(networkId, nodeId); +CREATE INDEX Log_ts ON Log(ts); + CREATE TABLE Relay ( networkId char(16) NOT NULL REFERENCES Network(id) ON DELETE CASCADE, address char(10) NOT NULL, diff --git a/controller/schema.sql.c b/controller/schema.sql.c index efeb280c..ac0213bc 100644 --- a/controller/schema.sql.c +++ b/controller/schema.sql.c @@ -66,6 +66,17 @@ "CREATE INDEX Member_networkId_activeBridge ON Member(networkId, activeBridge);\n"\ "CREATE INDEX Member_networkId_memberRevision ON Member(networkId, memberRevision);\n"\ "\n"\ +"CREATE TABLE Log (\n"\ +" networkId char(16) NOT NULL,\n"\ +" nodeId char(10) NOT NULL,\n"\ +" ts integer NOT NULL,\n"\ +" authorized integer NOT NULL,\n"\ +" fromAddr varchar(64)\n"\ +");\n"\ +"\n"\ +"CREATE INDEX Log_networkId_nodeId ON Log(networkId, nodeId);\n"\ +"CREATE INDEX Log_ts ON Log(ts);\n"\ +"\n"\ "CREATE TABLE Relay (\n"\ " networkId char(16) NOT NULL REFERENCES Network(id) ON DELETE CASCADE,\n"\ " address char(10) NOT NULL,\n"\ -- cgit v1.2.3 From 3ba54c7e3559359abd8d4734aa969829309a9dab Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Thu, 23 Jul 2015 09:50:10 -0700 Subject: Eliminate some poorly thought out optimizations from the netconf/controller interaction, and go ahead and bump version to 1.0.4. For a while in 1.0.3 -dev I was trying to optimize out repeated network controller requests by using a ratcheting mechanism. If the client received a network config that was indeed different from the one it had, it would respond by instantlly requesting it again. Not sure what I was thinking. It's fundamentally unsafe to respond to a message with another message of the same type -- it risks a race condition. In this case that's exactly what could happen. It just isn't worth the added complexity to avoid a tiny, tiny amount of network overhead, so I've taken this whole path out. A few extra bytes every two minutes isn't worth fretting about, but as I recall the reason for this optimization was to save CPU on the controller. This can be achieved by just caching responses in memory *there* and serving those same responses back out if they haven't changed. I think I developed that 'ratcheting' stuff before I went full time on this. It's hard to develop stuff like this without hours of sustained focus. --- controller/SqliteNetworkController.cpp | 33 +++++++++++++++++---------------- node/IncomingPacket.cpp | 31 +++++++------------------------ node/Network.cpp | 13 ++++++++++++- node/NetworkConfig.cpp | 23 ++++++++--------------- node/NetworkConfig.hpp | 26 +++++++------------------- node/NetworkController.hpp | 7 +++---- version.h | 2 +- 7 files changed, 55 insertions(+), 80 deletions(-) (limited to 'controller/SqliteNetworkController.cpp') diff --git a/controller/SqliteNetworkController.cpp b/controller/SqliteNetworkController.cpp index 85dbed00..f6489640 100644 --- a/controller/SqliteNetworkController.cpp +++ b/controller/SqliteNetworkController.cpp @@ -296,6 +296,12 @@ SqliteNetworkController::~SqliteNetworkController() NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(const InetAddress &fromAddr,const Identity &signingId,const Identity &identity,uint64_t nwid,const Dictionary &metaData,uint64_t haveRevision,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); // Note: we can't reuse prepared statements that return const char * pointers without @@ -418,13 +424,6 @@ NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(co if (!member.authorized) return NetworkController::NETCONF_QUERY_ACCESS_DENIED; - // If netconf is unchanged from client reported revision, just tell client they're up to date - - // Temporarily disabled -- old version didn't do this, and we'll go ahead and - // test more thoroughly before enabling this optimization. - //if ((haveRevision > 0)&&(haveRevision == network.revision)) - // return NetworkController::NETCONF_QUERY_OK_BUT_NOT_NEWER; - // Create and sign netconf netconf.clear(); @@ -581,7 +580,8 @@ NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(co if ((ipNetmaskBits <= 0)||(ipNetmaskBits > 32)) continue; - switch((IpAssignmentType)sqlite3_column_int(_sGetIpAssignmentsForNode,0)) { + const IpAssignmentType ipt = (IpAssignmentType)sqlite3_column_int(_sGetIpAssignmentsForNode,0); + switch(ipt) { case ZT_IP_ASSIGNMENT_TYPE_ADDRESS: haveStaticIpAssignment = true; break; @@ -592,13 +592,15 @@ NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(co continue; } - // We send both routes and IP assignments -- client knows which is - // which by whether address ends in all zeroes after netmask. - 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); + // 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); + } } if (!haveStaticIpAssignment) { @@ -1388,7 +1390,6 @@ unsigned int SqliteNetworkController::_doCPGet( const char *result = ""; switch(this->doNetworkConfigRequest(InetAddress(),sigid,memid,nwid,Dictionary(),hr,netconf)) { case NetworkController::NETCONF_QUERY_OK: result = "OK"; break; - case NetworkController::NETCONF_QUERY_OK_BUT_NOT_NEWER: result = "OK_BUT_NOT_NEWER"; break; case NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND: result = "OBJECT_NOT_FOUND"; break; case NetworkController::NETCONF_QUERY_ACCESS_DENIED: result = "ACCESS_DENIED"; break; case NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR: result = "INTERNAL_SERVER_ERROR"; break; diff --git a/node/IncomingPacket.cpp b/node/IncomingPacket.cpp index ae99352e..c3d8cc6d 100644 --- a/node/IncomingPacket.cpp +++ b/node/IncomingPacket.cpp @@ -392,28 +392,7 @@ bool IncomingPacket::_doOK(const RuntimeEnvironment *RR,const SharedPtr &p const unsigned int dictlen = at(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT_LEN); const std::string dict((const char *)field(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT,dictlen),dictlen); if (dict.length()) { - if (nw->setConfiguration(Dictionary(dict)) == 2) { // 2 == accepted and actually new - /* If this configuration was indeed new, we do another - * controller request with its revision. We do this in - * order to (a) tell the network controller we got it (it - * won't send a duplicate if ts == current), and (b) - * get another one if the controller is changing rapidly - * until we finally have the final version. - * - * Note that we don't do this for network controllers with - * versions <= 1.0.3, since those regenerate a new controller - * with a new revision every time. In that case this double - * confirmation would create a race condition. */ - const SharedPtr nc(nw->config2()); - if ((peer->atLeastVersion(1,0,3))&&(nc)&&(nc->revision() > 0)) { - Packet outp(peer->address(),RR->identity.address(),Packet::VERB_NETWORK_CONFIG_REQUEST); - outp.append((uint64_t)nw->id()); - outp.append((uint16_t)0); // no meta-data - outp.append((uint64_t)nc->revision()); - outp.armor(peer->key(),true); - RR->node->putPacket(_remoteAddress,outp.data(),outp.size()); - } - } + nw->setConfiguration(Dictionary(dict)); TRACE("got network configuration for network %.16llx from %s",(unsigned long long)nw->id(),source().toString().c_str()); } } @@ -692,6 +671,7 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons if (RR->localNetworkController) { Dictionary netconf; switch(RR->localNetworkController->doNetworkConfigRequest((h > 0) ? InetAddress() : _remoteAddress,RR->identity,peer->identity(),nwid,metaData,haveRevision,netconf)) { + case NetworkController::NETCONF_QUERY_OK: { const std::string netconfStr(netconf.toString()); if (netconfStr.length() > 0xffff) { // sanity check since field ix 16-bit @@ -712,8 +692,7 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons } } } break; - case NetworkController::NETCONF_QUERY_OK_BUT_NOT_NEWER: // nothing to do -- netconf has not changed - break; + case NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND: { Packet outp(peer->address(),RR->identity.address(),Packet::VERB_ERROR); outp.append((unsigned char)Packet::VERB_NETWORK_CONFIG_REQUEST); @@ -723,6 +702,7 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons outp.armor(peer->key(),true); RR->node->putPacket(_remoteAddress,outp.data(),outp.size()); } break; + case NetworkController::NETCONF_QUERY_ACCESS_DENIED: { Packet outp(peer->address(),RR->identity.address(),Packet::VERB_ERROR); outp.append((unsigned char)Packet::VERB_NETWORK_CONFIG_REQUEST); @@ -732,12 +712,15 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons outp.armor(peer->key(),true); RR->node->putPacket(_remoteAddress,outp.data(),outp.size()); } break; + case NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR: TRACE("NETWORK_CONFIG_REQUEST failed: internal error: %s",netconf.get("error","(unknown)").c_str()); break; + default: TRACE("NETWORK_CONFIG_REQUEST failed: invalid return value from NetworkController::doNetworkConfigRequest()"); break; + } } else { Packet outp(peer->address(),RR->identity.address(),Packet::VERB_ERROR); diff --git a/node/Network.cpp b/node/Network.cpp index adc8e1b8..549219d7 100644 --- a/node/Network.cpp +++ b/node/Network.cpp @@ -38,6 +38,8 @@ #include "Buffer.hpp" #include "NetworkController.hpp" +#include "../version.h" + namespace ZeroTier { const ZeroTier::MulticastGroup Network::BROADCAST(ZeroTier::MAC(0xffffffffffffULL),0); @@ -255,9 +257,18 @@ void Network::requestConfiguration() } TRACE("requesting netconf for network %.16llx from controller %s",(unsigned long long)_id,controller().toString().c_str()); + + // TODO: in the future we will include things like join tokens here, etc. + Dictionary metaData; + metaData.setHex(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MAJOR_VERSION,ZEROTIER_ONE_VERSION_MAJOR); + metaData.setHex(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MINOR_VERSION,ZEROTIER_ONE_VERSION_MINOR); + metaData.setHex(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_REVISION,ZEROTIER_ONE_VERSION_REVISION); + std::string mds(metaData.toString()); + Packet outp(controller(),RR->identity.address(),Packet::VERB_NETWORK_CONFIG_REQUEST); outp.append((uint64_t)_id); - outp.append((uint16_t)0); // no meta-data + outp.append((uint16_t)mds.length()); + outp.append((const void *)mds.data(),(unsigned int)mds.length()); { Mutex::Lock _l(_lock); if (_config) diff --git a/node/NetworkConfig.cpp b/node/NetworkConfig.cpp index ba4d338b..7898646c 100644 --- a/node/NetworkConfig.cpp +++ b/node/NetworkConfig.cpp @@ -47,7 +47,6 @@ SharedPtr NetworkConfig::createTestNetworkConfig(const Address &s nc->_private = false; nc->_enableBroadcast = true; nc->_name = "ZT_TEST_NETWORK"; - nc->_description = "Built-in dummy test network"; // Make up a V4 IP from 'self' in the 10.0.0.0/8 range -- no // guarantee of uniqueness but collisions are unlikely. @@ -111,7 +110,6 @@ void NetworkConfig::_fromDictionary(const Dictionary &d) _name = d.get(ZT_NETWORKCONFIG_DICT_KEY_NAME); if (_name.length() > ZT1_MAX_NETWORK_SHORT_NAME_LENGTH) throw std::invalid_argument("network short name too long (max: 255 characters)"); - _description = d.get(ZT_NETWORKCONFIG_DICT_KEY_DESC,std::string()); // In dictionary IPs are split into V4 and V6 addresses, but we don't really // need that so merge them here. @@ -132,26 +130,22 @@ void NetworkConfig::_fromDictionary(const Dictionary &d) case AF_INET: if ((!addr.netmaskBits())||(addr.netmaskBits() > 32)) continue; - else if (addr.isNetwork()) { - // TODO: add route to network -- this is a route without an IP assignment - continue; - } break; case AF_INET6: if ((!addr.netmaskBits())||(addr.netmaskBits() > 128)) continue; - else if (addr.isNetwork()) { - // TODO: add route to network -- this is a route without an IP assignment - continue; - } break; default: // ignore unrecognized address types or junk/empty fields continue; } - _staticIps.push_back(addr); + if (addr.isNetwork()) + _localRoutes.push_back(addr); + else _staticIps.push_back(addr); } - if (_staticIps.size() > ZT1_MAX_ZT_ASSIGNED_ADDRESSES) - throw std::invalid_argument("too many ZT-assigned IP addresses or routes"); + if (_localRoutes.size() > ZT1_MAX_ZT_ASSIGNED_ADDRESSES) throw std::invalid_argument("too many ZT-assigned routes"); + if (_staticIps.size() > ZT1_MAX_ZT_ASSIGNED_ADDRESSES) throw std::invalid_argument("too many ZT-assigned IP addresses"); + std::sort(_localRoutes.begin(),_localRoutes.end()); + _localRoutes.erase(std::unique(_localRoutes.begin(),_localRoutes.end()),_localRoutes.end()); std::sort(_staticIps.begin(),_staticIps.end()); _staticIps.erase(std::unique(_staticIps.begin(),_staticIps.end()),_staticIps.end()); @@ -201,7 +195,7 @@ bool NetworkConfig::operator==(const NetworkConfig &nc) const if (_private != nc._private) return false; if (_enableBroadcast != nc._enableBroadcast) return false; if (_name != nc._name) return false; - if (_description != nc._description) return false; + if (_localRoutes != nc._localRoutes) return false; if (_staticIps != nc._staticIps) return false; if (_gateways != nc._gateways) return false; if (_activeBridges != nc._activeBridges) return false; @@ -211,4 +205,3 @@ bool NetworkConfig::operator==(const NetworkConfig &nc) const } } // namespace ZeroTier - diff --git a/node/NetworkConfig.hpp b/node/NetworkConfig.hpp index 5c7cdd7c..6111e65b 100644 --- a/node/NetworkConfig.hpp +++ b/node/NetworkConfig.hpp @@ -47,59 +47,48 @@ namespace ZeroTier { +// Fields for meta-data sent with network config requests +#define ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MAJOR_VERSION "majv" +#define ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MINOR_VERSION "minv" +#define ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_REVISION "revv" + // These dictionary keys are short so they don't take up much room in // netconf response packets. // integer(hex)[,integer(hex),...] #define ZT_NETWORKCONFIG_DICT_KEY_ALLOWED_ETHERNET_TYPES "et" - // network ID #define ZT_NETWORKCONFIG_DICT_KEY_NETWORK_ID "nwid" - // integer(hex) #define ZT_NETWORKCONFIG_DICT_KEY_TIMESTAMP "ts" - // integer(hex) #define ZT_NETWORKCONFIG_DICT_KEY_REVISION "r" - // address of member #define ZT_NETWORKCONFIG_DICT_KEY_ISSUED_TO "id" - // integer(hex) #define ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_LIMIT "ml" - // 0/1 #define ZT_NETWORKCONFIG_DICT_KEY_PRIVATE "p" - // text #define ZT_NETWORKCONFIG_DICT_KEY_NAME "n" - // text #define ZT_NETWORKCONFIG_DICT_KEY_DESC "d" - // IP/bits[,IP/bits,...] // Note that IPs that end in all zeroes are routes with no assignment in them. #define ZT_NETWORKCONFIG_DICT_KEY_IPV4_STATIC "v4s" - // IP/bits[,IP/bits,...] // Note that IPs that end in all zeroes are routes with no assignment in them. #define ZT_NETWORKCONFIG_DICT_KEY_IPV6_STATIC "v6s" - // serialized CertificateOfMembership #define ZT_NETWORKCONFIG_DICT_KEY_CERTIFICATE_OF_MEMBERSHIP "com" - // 0/1 #define ZT_NETWORKCONFIG_DICT_KEY_ENABLE_BROADCAST "eb" - // 0/1 #define ZT_NETWORKCONFIG_DICT_KEY_ALLOW_PASSIVE_BRIDGING "pb" - // node[,node,...] #define ZT_NETWORKCONFIG_DICT_KEY_ACTIVE_BRIDGES "ab" - // node;IP/port[,node;IP/port] #define ZT_NETWORKCONFIG_DICT_KEY_RELAYS "rl" - // IP/metric[,IP/metric,...] #define ZT_NETWORKCONFIG_DICT_KEY_GATEWAYS "gw" @@ -158,7 +147,7 @@ public: inline bool isPublic() const throw() { return (!_private); } inline bool isPrivate() const throw() { return _private; } inline const std::string &name() const throw() { return _name; } - inline const std::string &description() const throw() { return _description; } + inline const std::vector &localRoutes() const throw() { return _localRoutes; } inline const std::vector &staticIps() const throw() { return _staticIps; } inline const std::vector &gateways() const throw() { return _gateways; } inline const std::vector
&activeBridges() const throw() { return _activeBridges; } @@ -194,7 +183,7 @@ private: bool _private; bool _enableBroadcast; std::string _name; - std::string _description; + std::vector _localRoutes; std::vector _staticIps; std::vector _gateways; std::vector
_activeBridges; @@ -207,4 +196,3 @@ private: } // namespace ZeroTier #endif - diff --git a/node/NetworkController.hpp b/node/NetworkController.hpp index 265ee3d4..bef884de 100644 --- a/node/NetworkController.hpp +++ b/node/NetworkController.hpp @@ -52,10 +52,9 @@ public: enum ResultCode { NETCONF_QUERY_OK = 0, - NETCONF_QUERY_OK_BUT_NOT_NEWER = 1, - NETCONF_QUERY_OBJECT_NOT_FOUND = 2, - NETCONF_QUERY_ACCESS_DENIED = 3, - NETCONF_QUERY_INTERNAL_SERVER_ERROR = 4 + NETCONF_QUERY_OBJECT_NOT_FOUND = 1, + NETCONF_QUERY_ACCESS_DENIED = 2, + NETCONF_QUERY_INTERNAL_SERVER_ERROR = 3 }; NetworkController() {} diff --git a/version.h b/version.h index f7b253a7..62f8fb69 100644 --- a/version.h +++ b/version.h @@ -41,6 +41,6 @@ /** * Revision */ -#define ZEROTIER_ONE_VERSION_REVISION 3 +#define ZEROTIER_ONE_VERSION_REVISION 4 #endif -- cgit v1.2.3 From b3516c599bb0beb4b4827f28da472972344379c6 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Thu, 23 Jul 2015 10:10:17 -0700 Subject: Add a rate limiting circuit breaker to the network controller to prevent flooding attacks and race conditions. --- controller/SqliteNetworkController.cpp | 13 +++++++++++++ controller/SqliteNetworkController.hpp | 2 ++ node/IncomingPacket.cpp | 3 +++ node/NetworkController.hpp | 3 ++- 4 files changed, 20 insertions(+), 1 deletion(-) (limited to 'controller/SqliteNetworkController.cpp') diff --git a/controller/SqliteNetworkController.cpp b/controller/SqliteNetworkController.cpp index f6489640..bdf337ec 100644 --- a/controller/SqliteNetworkController.cpp +++ b/controller/SqliteNetworkController.cpp @@ -64,6 +64,10 @@ // API version reported via JSON control plane #define ZT_NETCONF_CONTROLLER_API_VERSION 1 +// Drop requests for a given peer and network ID that occur more frequently +// than this (ms). +#define ZT_NETCONF_MIN_REQUEST_PERIOD 5000 + namespace ZeroTier { namespace { @@ -316,6 +320,15 @@ NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(co return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR; } + // Check rate limit + + { + uint64_t &lrt = _lastRequestTime[std::pair(identity.address(),nwid)]; + uint64_t lrt2 = lrt; + if (((lrt = OSUtils::now()) - lrt2) <= ZT_NETCONF_MIN_REQUEST_PERIOD) + return NetworkController::NETCONF_QUERY_IGNORE; + } + NetworkRecord network; memset(&network,0,sizeof(network)); Utils::snprintf(network.id,sizeof(network.id),"%.16llx",(unsigned long long)nwid); diff --git a/controller/SqliteNetworkController.hpp b/controller/SqliteNetworkController.hpp index bae11519..002493ec 100644 --- a/controller/SqliteNetworkController.hpp +++ b/controller/SqliteNetworkController.hpp @@ -98,6 +98,8 @@ private: std::string _dbPath; std::string _instanceId; + std::map< std::pair,uint64_t > _lastRequestTime; + sqlite3 *_db; sqlite3_stmt *_sGetNetworkById; diff --git a/node/IncomingPacket.cpp b/node/IncomingPacket.cpp index c3d8cc6d..76c47933 100644 --- a/node/IncomingPacket.cpp +++ b/node/IncomingPacket.cpp @@ -717,6 +717,9 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons TRACE("NETWORK_CONFIG_REQUEST failed: internal error: %s",netconf.get("error","(unknown)").c_str()); break; + case NetworkController::NETCONF_QUERY_IGNORE: + break; + default: TRACE("NETWORK_CONFIG_REQUEST failed: invalid return value from NetworkController::doNetworkConfigRequest()"); break; diff --git a/node/NetworkController.hpp b/node/NetworkController.hpp index bef884de..ee481a62 100644 --- a/node/NetworkController.hpp +++ b/node/NetworkController.hpp @@ -54,7 +54,8 @@ public: NETCONF_QUERY_OK = 0, NETCONF_QUERY_OBJECT_NOT_FOUND = 1, NETCONF_QUERY_ACCESS_DENIED = 2, - NETCONF_QUERY_INTERNAL_SERVER_ERROR = 3 + NETCONF_QUERY_INTERNAL_SERVER_ERROR = 3, + NETCONF_QUERY_IGNORE = 4 }; NetworkController() {} -- cgit v1.2.3 From d647a587a1c920cdf58ce77872280e4e4ec9cca9 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Thu, 23 Jul 2015 17:18:20 -0700 Subject: (1) Fix updating of network revision counter on member change. (2) Go back to timestamp as certificate revision number. This is simpler and more robust than using the network revision number for this and forcing network revision fast-forward, which could cause some peers to fall off the horizon when you don't want them to. --- controller/SqliteNetworkController.cpp | 17 +++++++++++++++-- include/ZeroTierOne.h | 11 ----------- node/Constants.hpp | 4 ++-- 3 files changed, 17 insertions(+), 15 deletions(-) (limited to 'controller/SqliteNetworkController.cpp') diff --git a/controller/SqliteNetworkController.cpp b/controller/SqliteNetworkController.cpp index bdf337ec..b41c7ef5 100644 --- a/controller/SqliteNetworkController.cpp +++ b/controller/SqliteNetworkController.cpp @@ -66,7 +66,7 @@ // Drop requests for a given peer and network ID that occur more frequently // than this (ms). -#define ZT_NETCONF_MIN_REQUEST_PERIOD 5000 +#define ZT_NETCONF_MIN_REQUEST_PERIOD 1000 namespace ZeroTier { @@ -689,7 +689,7 @@ NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(co // TODO: IPv6 auto-assign once it's supported in UI if (network.isPrivate) { - CertificateOfMembership com(network.revision,ZT1_CERTIFICATE_OF_MEMBERSHIP_REVISION_MAX_DELTA,nwid,identity.address()); + 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 { @@ -757,6 +757,8 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST( 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); @@ -780,6 +782,7 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST( sqlite3_reset(_sIncrementMemberRevisionCounter); sqlite3_bind_text(_sIncrementMemberRevisionCounter,1,nwids,16,SQLITE_STATIC); sqlite3_step(_sIncrementMemberRevisionCounter); + addToNetworkRevision = 1; } json_value *j = json_parse(body.c_str(),body.length()); @@ -799,6 +802,7 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST( 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) { @@ -812,6 +816,7 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST( 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) { @@ -855,6 +860,7 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST( } } } + addToNetworkRevision = 1; } } @@ -863,6 +869,13 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST( 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); + } + return _doCPGet(path,urlArgs,headers,body,responseBody,responseContentType); } // else 404 diff --git a/include/ZeroTierOne.h b/include/ZeroTierOne.h index 7ae524a8..dc2243f2 100644 --- a/include/ZeroTierOne.h +++ b/include/ZeroTierOne.h @@ -105,17 +105,6 @@ extern "C" { */ #define ZT1_MAX_PEER_NETWORK_PATHS 4 -/** - * Maximum number of revisions over which a network COM can differ and still be in-horizon (agree) - * - * This is the default max delta for the revision field in COMs issued - * by network controllers, and is defined here for documentation purposes. - * When a network is changed so as to de-authorize a member, its revision - * should be incremented by this number. Otherwise all other changes that - * materially affect the network should result in increment by one. - */ -#define ZT1_CERTIFICATE_OF_MEMBERSHIP_REVISION_MAX_DELTA 16 - /** * Feature flag: ZeroTier One was built to be thread-safe -- concurrent processXXX() calls are okay */ diff --git a/node/Constants.hpp b/node/Constants.hpp index d15fef13..c192381c 100644 --- a/node/Constants.hpp +++ b/node/Constants.hpp @@ -158,7 +158,7 @@ /** * Maximum number of packet fragments we'll support - * + * * The actual spec allows 16, but this is the most we'll support right * now. Packets with more than this many fragments are dropped. */ @@ -216,7 +216,7 @@ /** * Maximum number of ZT hops allowed (this is not IP hops/TTL) - * + * * The protocol allows up to 7, but we limit it to something smaller. */ #define ZT_RELAY_MAX_HOPS 3 -- cgit v1.2.3 From d57ea671d7d10b242def3b6d43832906275adff9 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Fri, 24 Jul 2015 09:59:17 -0700 Subject: Add version to log. --- controller/SqliteNetworkController.cpp | 89 ++++++++++++++-------------------- controller/SqliteNetworkController.hpp | 1 + controller/schema.sql | 1 + controller/schema.sql.c | 1 + 4 files changed, 39 insertions(+), 53 deletions(-) (limited to 'controller/SqliteNetworkController.cpp') diff --git a/controller/SqliteNetworkController.cpp b/controller/SqliteNetworkController.cpp index b41c7ef5..50be5b34 100644 --- a/controller/SqliteNetworkController.cpp +++ b/controller/SqliteNetworkController.cpp @@ -209,8 +209,9 @@ SqliteNetworkController::SqliteNetworkController(const char *dbPath) : ||(sqlite3_prepare_v2(_db,"INSERT INTO Gateway (networkId,\"ip\",ipVersion,metric) VALUES (?,?,?,?)",-1,&_sCreateGateway,(const char **)0) != SQLITE_OK) /* Log */ - ||(sqlite3_prepare_v2(_db,"INSERT INTO \"Log\" (networkId,nodeId,\"ts\",\"authorized\",fromAddr) VALUES (?,?,?,?,?)",-1,&_sPutLog,(const char **)0) != SQLITE_OK) - ||(sqlite3_prepare_v2(_db,"SELECT \"ts\",\"authorized\",fromAddr FROM \"Log\" WHERE networkId = ? AND nodeId = ? AND \"ts\" >= ? ORDER BY \"ts\" ASC",-1,&_sGetMemberLog,(const char **)0) != SQLITE_OK) + ||(sqlite3_prepare_v2(_db,"INSERT INTO \"Log\" (networkId,nodeId,\"ts\",\"authorized\",\"version\",fromAddr) VALUES (?,?,?,?,?,?)",-1,&_sPutLog,(const char **)0) != SQLITE_OK) + ||(sqlite3_prepare_v2(_db,"SELECT \"ts\",\"authorized\",\"version\",fromAddr FROM \"Log\" WHERE networkId = ? AND nodeId = ? AND \"ts\" >= ? ORDER BY \"ts\" ASC",-1,&_sGetMemberLog,(const char **)0) != SQLITE_OK) + ||(sqlite3_prepare_v2(_db,"SELECT \"ts\",\"authorized\",\"version\",fromAddr FROM \"Log\" WHERE networkId = ? AND nodeId = ? ORDER BY \"ts\" DESC LIMIT 10",-1,&_sGetRecentMemberLog,(const char **)0) != SQLITE_OK) /* Config */ ||(sqlite3_prepare_v2(_db,"SELECT \"v\" FROM \"Config\" WHERE \"k\" = ?",-1,&_sGetConfig,(const char **)0) != SQLITE_OK) @@ -294,6 +295,7 @@ SqliteNetworkController::~SqliteNetworkController() sqlite3_finalize(_sSetConfig); sqlite3_finalize(_sPutLog); sqlite3_finalize(_sGetMemberLog); + sqlite3_finalize(_sGetRecentMemberLog); sqlite3_close(_db); } } @@ -415,6 +417,7 @@ NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(co // Add log entry { + char ver[16]; std::string fa; if (fromAddr) { fa = fromAddr.toString(); @@ -426,9 +429,13 @@ NetworkController::ResultCode SqliteNetworkController::doNetworkConfigRequest(co 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,5,fa.c_str(),-1,SQLITE_STATIC); - else sqlite3_bind_null(_sPutLog,5); + sqlite3_bind_text(_sPutLog,6,fa.c_str(),-1,SQLITE_STATIC); + else sqlite3_bind_null(_sPutLog,6); sqlite3_step(_sPutLog); } @@ -1386,57 +1393,33 @@ unsigned int SqliteNetworkController::_doCPGet( responseBody.push_back('"'); } - responseBody.append("]"); - - /* It's possible to get the actual netconf dictionary by including these - * three URL arguments. The member identity must be the string - * serialized identity of this member, and the signing identity must be - * the full secret identity of this network controller. The have revision - * is optional but would designate the revision our hypothetical client - * already has. - * - * This is primarily for testing and is not used in production. It makes - * it easy to test the entire network controller via its JSON API. - * - * If these arguments are included, three more object fields are returned: - * 'netconf', 'netconfResult', and 'netconfResultMessage'. These are all - * string fields and contain the actual netconf dictionary, the query - * result code, and any verbose message e.g. an error description. */ - std::map::const_iterator memids(urlArgs.find("memberIdentity")); - std::map::const_iterator sigids(urlArgs.find("signingIdentity")); - std::map::const_iterator hrs(urlArgs.find("haveRevision")); - if ((memids != urlArgs.end())&&(sigids != urlArgs.end())) { - Dictionary netconf; - Identity memid,sigid; - try { - if (memid.fromString(memids->second)&&sigid.fromString(sigids->second)&&sigid.hasPrivate()) { - uint64_t hr = 0; - if (hrs != urlArgs.end()) - hr = Utils::strToU64(hrs->second.c_str()); - const char *result = ""; - switch(this->doNetworkConfigRequest(InetAddress(),sigid,memid,nwid,Dictionary(),hr,netconf)) { - case NetworkController::NETCONF_QUERY_OK: result = "OK"; break; - case NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND: result = "OBJECT_NOT_FOUND"; break; - case NetworkController::NETCONF_QUERY_ACCESS_DENIED: result = "ACCESS_DENIED"; break; - case NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR: result = "INTERNAL_SERVER_ERROR"; break; - default: result = "(unrecognized result code)"; break; - } - responseBody.append(",\n\t\"netconf\": \""); - responseBody.append(_jsonEscape(netconf.toString().c_str())); - responseBody.append("\",\n\t\"netconfResult\": \""); - responseBody.append(result); - responseBody.append("\",\n\t\"netconfResultMessage\": \""); - responseBody.append(_jsonEscape(netconf["error"].c_str())); - responseBody.append("\""); - } else { - responseBody.append(",\n\t\"netconf\": \"\",\n\t\"netconfResult\": \"INTERNAL_SERVER_ERROR\",\n\t\"netconfResultMessage\": \"invalid member or signing identity\""); - } - } catch ( ... ) { - responseBody.append(",\n\t\"netconf\": \"\",\n\t\"netconfResult\": \"INTERNAL_SERVER_ERROR\",\n\t\"netconfResultMessage\": \"unexpected exception\""); - } + responseBody.append("],\n\t\"recentLog\": ["); + + 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(sqlite3_column_text(_sGetRecentMemberLog,0))); + responseBody.append((sqlite3_column_int(_sGetRecentMemberLog,1) == 0) ? ",\"authorized\":false,\"version\":" : ",\"authorized\":true,\"version\":"); + const char *ver = reinterpret_cast(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(sqlite3_column_text(_sGetRecentMemberLog,3)); + if ((fa)&&(fa[0])) { + responseBody.push_back('"'); + responseBody.append(_jsonEscape(fa)); + responseBody.append("\"}"); + } else responseBody.append("null}"); } - responseBody.append("\n}\n"); + responseBody.append("]\n}\n"); responseContentType = "application/json"; return 200; diff --git a/controller/SqliteNetworkController.hpp b/controller/SqliteNetworkController.hpp index 002493ec..adfe0991 100644 --- a/controller/SqliteNetworkController.hpp +++ b/controller/SqliteNetworkController.hpp @@ -146,6 +146,7 @@ private: sqlite3_stmt *_sSetConfig; sqlite3_stmt *_sPutLog; sqlite3_stmt *_sGetMemberLog; + sqlite3_stmt *_sGetRecentMemberLog; Mutex _lock; }; diff --git a/controller/schema.sql b/controller/schema.sql index 024a5229..398d63ac 100644 --- a/controller/schema.sql +++ b/controller/schema.sql @@ -70,6 +70,7 @@ CREATE TABLE Log ( nodeId char(10) NOT NULL, ts integer NOT NULL, authorized integer NOT NULL, + version varchar(16), fromAddr varchar(64) ); diff --git a/controller/schema.sql.c b/controller/schema.sql.c index ac0213bc..fa83f880 100644 --- a/controller/schema.sql.c +++ b/controller/schema.sql.c @@ -71,6 +71,7 @@ " nodeId char(10) NOT NULL,\n"\ " ts integer NOT NULL,\n"\ " authorized integer NOT NULL,\n"\ +" version varchar(16),\n"\ " fromAddr varchar(64)\n"\ ");\n"\ "\n"\ -- cgit v1.2.3