summaryrefslogtreecommitdiff
path: root/controller
diff options
context:
space:
mode:
Diffstat (limited to 'controller')
-rw-r--r--controller/EmbeddedNetworkController.cpp1656
-rw-r--r--controller/EmbeddedNetworkController.hpp213
-rw-r--r--controller/README.md253
3 files changed, 2122 insertions, 0 deletions
diff --git a/controller/EmbeddedNetworkController.cpp b/controller/EmbeddedNetworkController.cpp
new file mode 100644
index 00000000..d9ec76de
--- /dev/null
+++ b/controller/EmbeddedNetworkController.cpp
@@ -0,0 +1,1656 @@
+/*
+ * ZeroTier One - Network Virtualization Everywhere
+ * Copyright (C) 2011-2015 ZeroTier, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include <algorithm>
+#include <utility>
+#include <stdexcept>
+#include <set>
+#include <map>
+
+#include "../include/ZeroTierOne.h"
+#include "../node/Constants.hpp"
+
+#include "EmbeddedNetworkController.hpp"
+
+#include "../node/Node.hpp"
+#include "../node/Utils.hpp"
+#include "../node/CertificateOfMembership.hpp"
+#include "../node/NetworkConfig.hpp"
+#include "../node/Dictionary.hpp"
+#include "../node/InetAddress.hpp"
+#include "../node/MAC.hpp"
+#include "../node/Address.hpp"
+
+using json = nlohmann::json;
+
+// API version reported via JSON control plane
+#define ZT_NETCONF_CONTROLLER_API_VERSION 3
+
+// Number of requests to remember in member history
+#define ZT_NETCONF_DB_MEMBER_HISTORY_LENGTH 24
+
+// Min duration between requests for an address/nwid combo to prevent floods
+#define ZT_NETCONF_MIN_REQUEST_PERIOD 1000
+
+// Nodes are considered active if they've queried in less than this long
+#define ZT_NETCONF_NODE_ACTIVE_THRESHOLD (ZT_NETWORK_AUTOCONF_DELAY * 2)
+
+namespace ZeroTier {
+
+// JSON blob I/O
+static json _readJson(const std::string &path)
+{
+ std::string buf;
+ if (OSUtils::readFile(path.c_str(),buf)) {
+ try {
+ return json::parse(buf);
+ } catch ( ... ) {}
+ }
+ return json::object();
+}
+static bool _writeJson(const std::string &path,const json &obj)
+{
+ return OSUtils::writeFile(path.c_str(),obj.dump(2));
+}
+
+// Get JSON values as unsigned integers, strings, or booleans, doing type conversion if possible
+static uint64_t _jI(const json &jv,const uint64_t dfl)
+{
+ if (jv.is_number()) {
+ return (uint64_t)jv;
+ } else if (jv.is_string()) {
+ std::string s = jv;
+ return Utils::strToU64(s.c_str());
+ } else if (jv.is_boolean()) {
+ return ((bool)jv ? 1ULL : 0ULL);
+ }
+ return dfl;
+}
+static bool _jB(const json &jv,const bool dfl)
+{
+ if (jv.is_boolean()) {
+ return (bool)jv;
+ } else if (jv.is_number()) {
+ return ((uint64_t)jv > 0ULL);
+ } else if (jv.is_string()) {
+ std::string s = jv;
+ if (s.length() > 0) {
+ switch(s[0]) {
+ case 't':
+ case 'T':
+ case '1':
+ return true;
+ }
+ }
+ return false;
+ }
+ return dfl;
+}
+static std::string _jS(const json &jv,const char *dfl)
+{
+ if (jv.is_string()) {
+ return jv;
+ } else if (jv.is_number()) {
+ char tmp[64];
+ Utils::snprintf(tmp,sizeof(tmp),"%llu",(uint64_t)jv);
+ return tmp;
+ } else if (jv.is_boolean()) {
+ return ((bool)jv ? std::string("1") : std::string("0"));
+ }
+ return std::string((dfl) ? dfl : "");
+}
+
+static json _renderRule(ZT_VirtualNetworkRule &rule)
+{
+ char tmp[128];
+ json r = json::object();
+ const ZT_VirtualNetworkRuleType rt = (ZT_VirtualNetworkRuleType)(rule.t & 0x3f);
+
+ switch(rt) {
+ case ZT_NETWORK_RULE_ACTION_DROP:
+ r["type"] = "ACTION_DROP";
+ break;
+ case ZT_NETWORK_RULE_ACTION_ACCEPT:
+ r["type"] = "ACTION_ACCEPT";
+ break;
+ case ZT_NETWORK_RULE_ACTION_TEE:
+ r["type"] = "ACTION_TEE";
+ r["address"] = Address(rule.v.fwd.address).toString();
+ r["flags"] = (unsigned int)rule.v.fwd.flags;
+ r["length"] = (unsigned int)rule.v.fwd.length;
+ break;
+ case ZT_NETWORK_RULE_ACTION_WATCH:
+ r["type"] = "ACTION_WATCH";
+ r["address"] = Address(rule.v.fwd.address).toString();
+ r["flags"] = (unsigned int)rule.v.fwd.flags;
+ r["length"] = (unsigned int)rule.v.fwd.length;
+ break;
+ case ZT_NETWORK_RULE_ACTION_REDIRECT:
+ r["type"] = "ACTION_REDIRECT";
+ r["address"] = Address(rule.v.fwd.address).toString();
+ r["flags"] = (unsigned int)rule.v.fwd.flags;
+ break;
+ case ZT_NETWORK_RULE_ACTION_DEBUG_LOG:
+ r["type"] = "ACTION_DEBUG_LOG";
+ break;
+ default:
+ break;
+ }
+
+ if (r.size() == 0) {
+ switch(rt) {
+ case ZT_NETWORK_RULE_MATCH_SOURCE_ZEROTIER_ADDRESS:
+ r["type"] = "MATCH_SOURCE_ZEROTIER_ADDRESS";
+ r["zt"] = Address(rule.v.zt).toString();
+ break;
+ case ZT_NETWORK_RULE_MATCH_DEST_ZEROTIER_ADDRESS:
+ r["type"] = "MATCH_DEST_ZEROTIER_ADDRESS";
+ r["zt"] = Address(rule.v.zt).toString();
+ break;
+ case ZT_NETWORK_RULE_MATCH_VLAN_ID:
+ r["type"] = "MATCH_VLAN_ID";
+ r["vlanId"] = (unsigned int)rule.v.vlanId;
+ break;
+ case ZT_NETWORK_RULE_MATCH_VLAN_PCP:
+ r["type"] = "MATCH_VLAN_PCP";
+ r["vlanPcp"] = (unsigned int)rule.v.vlanPcp;
+ break;
+ case ZT_NETWORK_RULE_MATCH_VLAN_DEI:
+ r["type"] = "MATCH_VLAN_DEI";
+ r["vlanDei"] = (unsigned int)rule.v.vlanDei;
+ break;
+ case ZT_NETWORK_RULE_MATCH_MAC_SOURCE:
+ r["type"] = "MATCH_MAC_SOURCE";
+ Utils::snprintf(tmp,sizeof(tmp),"%.2x:%.2x:%.2x:%.2x:%.2x:%.2x",(unsigned int)rule.v.mac[0],(unsigned int)rule.v.mac[1],(unsigned int)rule.v.mac[2],(unsigned int)rule.v.mac[3],(unsigned int)rule.v.mac[4],(unsigned int)rule.v.mac[5]);
+ r["mac"] = tmp;
+ break;
+ case ZT_NETWORK_RULE_MATCH_MAC_DEST:
+ r["type"] = "MATCH_MAC_DEST";
+ Utils::snprintf(tmp,sizeof(tmp),"%.2x:%.2x:%.2x:%.2x:%.2x:%.2x",(unsigned int)rule.v.mac[0],(unsigned int)rule.v.mac[1],(unsigned int)rule.v.mac[2],(unsigned int)rule.v.mac[3],(unsigned int)rule.v.mac[4],(unsigned int)rule.v.mac[5]);
+ r["mac"] = tmp;
+ break;
+ case ZT_NETWORK_RULE_MATCH_IPV4_SOURCE:
+ r["type"] = "MATCH_IPV4_SOURCE";
+ r["ip"] = InetAddress(&(rule.v.ipv4.ip),4,(unsigned int)rule.v.ipv4.mask).toString();
+ break;
+ case ZT_NETWORK_RULE_MATCH_IPV4_DEST:
+ r["type"] = "MATCH_IPV4_DEST";
+ r["ip"] = InetAddress(&(rule.v.ipv4.ip),4,(unsigned int)rule.v.ipv4.mask).toString();
+ break;
+ case ZT_NETWORK_RULE_MATCH_IPV6_SOURCE:
+ r["type"] = "MATCH_IPV6_SOURCE";
+ r["ip"] = InetAddress(rule.v.ipv6.ip,16,(unsigned int)rule.v.ipv6.mask).toString();
+ break;
+ case ZT_NETWORK_RULE_MATCH_IPV6_DEST:
+ r["type"] = "MATCH_IPV6_DEST";
+ r["ip"] = InetAddress(rule.v.ipv6.ip,16,(unsigned int)rule.v.ipv6.mask).toString();
+ break;
+ case ZT_NETWORK_RULE_MATCH_IP_TOS:
+ r["type"] = "MATCH_IP_TOS";
+ r["ipTos"] = (unsigned int)rule.v.ipTos;
+ break;
+ case ZT_NETWORK_RULE_MATCH_IP_PROTOCOL:
+ r["type"] = "MATCH_IP_PROTOCOL";
+ r["ipProtocol"] = (unsigned int)rule.v.ipProtocol;
+ break;
+ case ZT_NETWORK_RULE_MATCH_ETHERTYPE:
+ r["type"] = "MATCH_ETHERTYPE";
+ r["etherType"] = (unsigned int)rule.v.etherType;
+ break;
+ case ZT_NETWORK_RULE_MATCH_ICMP:
+ r["type"] = "MATCH_ICMP";
+ r["icmpType"] = (unsigned int)rule.v.icmp.type;
+ if ((rule.v.icmp.flags & 0x01) != 0)
+ r["icmpCode"] = (unsigned int)rule.v.icmp.code;
+ else r["icmpCode"] = json();
+ break;
+ case ZT_NETWORK_RULE_MATCH_IP_SOURCE_PORT_RANGE:
+ r["type"] = "MATCH_IP_SOURCE_PORT_RANGE";
+ r["start"] = (unsigned int)rule.v.port[0];
+ r["end"] = (unsigned int)rule.v.port[1];
+ break;
+ case ZT_NETWORK_RULE_MATCH_IP_DEST_PORT_RANGE:
+ r["type"] = "MATCH_IP_DEST_PORT_RANGE";
+ r["start"] = (unsigned int)rule.v.port[0];
+ r["end"] = (unsigned int)rule.v.port[1];
+ break;
+ case ZT_NETWORK_RULE_MATCH_CHARACTERISTICS:
+ r["type"] = "MATCH_CHARACTERISTICS";
+ Utils::snprintf(tmp,sizeof(tmp),"%.16llx",rule.v.characteristics);
+ r["mask"] = tmp;
+ break;
+ case ZT_NETWORK_RULE_MATCH_FRAME_SIZE_RANGE:
+ r["type"] = "MATCH_FRAME_SIZE_RANGE";
+ r["start"] = (unsigned int)rule.v.frameSize[0];
+ r["end"] = (unsigned int)rule.v.frameSize[1];
+ break;
+ case ZT_NETWORK_RULE_MATCH_RANDOM:
+ r["type"] = "MATCH_RANDOM";
+ r["probability"] = (unsigned long)rule.v.randomProbability;
+ break;
+ case ZT_NETWORK_RULE_MATCH_TAGS_DIFFERENCE:
+ r["type"] = "MATCH_TAGS_DIFFERENCE";
+ r["id"] = rule.v.tag.id;
+ r["value"] = rule.v.tag.value;
+ break;
+ case ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_AND:
+ r["type"] = "MATCH_TAGS_BITWISE_AND";
+ r["id"] = rule.v.tag.id;
+ r["value"] = rule.v.tag.value;
+ break;
+ case ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_OR:
+ r["type"] = "MATCH_TAGS_BITWISE_OR";
+ r["id"] = rule.v.tag.id;
+ r["value"] = rule.v.tag.value;
+ break;
+ case ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_XOR:
+ r["type"] = "MATCH_TAGS_BITWISE_XOR";
+ r["id"] = rule.v.tag.id;
+ r["value"] = rule.v.tag.value;
+ break;
+ case ZT_NETWORK_RULE_MATCH_TAGS_EQUAL:
+ r["type"] = "MATCH_TAGS_EQUAL";
+ r["id"] = rule.v.tag.id;
+ r["value"] = rule.v.tag.value;
+ break;
+ default:
+ break;
+ }
+
+ if (r.size() > 0) {
+ r["not"] = ((rule.t & 0x80) != 0);
+ r["or"] = ((rule.t & 0x40) != 0);
+ }
+ }
+
+ return r;
+}
+
+static bool _parseRule(json &r,ZT_VirtualNetworkRule &rule)
+{
+ if (!r.is_object())
+ return false;
+
+ const std::string t(_jS(r["type"],""));
+ memset(&rule,0,sizeof(ZT_VirtualNetworkRule));
+
+ if (_jB(r["not"],false))
+ rule.t = 0x80;
+ else rule.t = 0x00;
+ if (_jB(r["or"],false))
+ rule.t |= 0x40;
+
+ if (t == "ACTION_DROP") {
+ rule.t |= ZT_NETWORK_RULE_ACTION_DROP;
+ return true;
+ } else if (t == "ACTION_ACCEPT") {
+ rule.t |= ZT_NETWORK_RULE_ACTION_ACCEPT;
+ return true;
+ } else if (t == "ACTION_TEE") {
+ rule.t |= ZT_NETWORK_RULE_ACTION_TEE;
+ rule.v.fwd.address = Utils::hexStrToU64(_jS(r["address"],"0").c_str()) & 0xffffffffffULL;
+ rule.v.fwd.flags = (uint32_t)(_jI(r["flags"],0ULL) & 0xffffffffULL);
+ rule.v.fwd.length = (uint16_t)(_jI(r["length"],0ULL) & 0xffffULL);
+ return true;
+ } else if (t == "ACTION_WATCH") {
+ rule.t |= ZT_NETWORK_RULE_ACTION_WATCH;
+ rule.v.fwd.address = Utils::hexStrToU64(_jS(r["address"],"0").c_str()) & 0xffffffffffULL;
+ rule.v.fwd.flags = (uint32_t)(_jI(r["flags"],0ULL) & 0xffffffffULL);
+ rule.v.fwd.length = (uint16_t)(_jI(r["length"],0ULL) & 0xffffULL);
+ return true;
+ } else if (t == "ACTION_REDIRECT") {
+ rule.t |= ZT_NETWORK_RULE_ACTION_REDIRECT;
+ rule.v.fwd.address = Utils::hexStrToU64(_jS(r["zt"],"0").c_str()) & 0xffffffffffULL;
+ rule.v.fwd.flags = (uint32_t)(_jI(r["flags"],0ULL) & 0xffffffffULL);
+ return true;
+ } else if (t == "ACTION_DEBUG_LOG") {
+ rule.t |= ZT_NETWORK_RULE_ACTION_DEBUG_LOG;
+ return true;
+ } else if (t == "MATCH_SOURCE_ZEROTIER_ADDRESS") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_SOURCE_ZEROTIER_ADDRESS;
+ rule.v.zt = Utils::hexStrToU64(_jS(r["zt"],"0").c_str()) & 0xffffffffffULL;
+ return true;
+ } else if (t == "MATCH_DEST_ZEROTIER_ADDRESS") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_DEST_ZEROTIER_ADDRESS;
+ rule.v.zt = Utils::hexStrToU64(_jS(r["zt"],"0").c_str()) & 0xffffffffffULL;
+ return true;
+ } else if (t == "MATCH_VLAN_ID") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_VLAN_ID;
+ rule.v.vlanId = (uint16_t)(_jI(r["vlanId"],0ULL) & 0xffffULL);
+ return true;
+ } else if (t == "MATCH_VLAN_PCP") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_VLAN_PCP;
+ rule.v.vlanPcp = (uint8_t)(_jI(r["vlanPcp"],0ULL) & 0xffULL);
+ return true;
+ } else if (t == "MATCH_VLAN_DEI") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_VLAN_DEI;
+ rule.v.vlanDei = (uint8_t)(_jI(r["vlanDei"],0ULL) & 0xffULL);
+ return true;
+ } else if (t == "MATCH_MAC_SOURCE") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_MAC_SOURCE;
+ const std::string mac(_jS(r["mac"],"0"));
+ Utils::unhex(mac.c_str(),(unsigned int)mac.length(),rule.v.mac,6);
+ return true;
+ } else if (t == "MATCH_MAC_DEST") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_MAC_DEST;
+ const std::string mac(_jS(r["mac"],"0"));
+ Utils::unhex(mac.c_str(),(unsigned int)mac.length(),rule.v.mac,6);
+ return true;
+ } else if (t == "MATCH_IPV4_SOURCE") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_IPV4_SOURCE;
+ InetAddress ip(_jS(r["ip"],"0.0.0.0"));
+ rule.v.ipv4.ip = reinterpret_cast<struct sockaddr_in *>(&ip)->sin_addr.s_addr;
+ rule.v.ipv4.mask = Utils::ntoh(reinterpret_cast<struct sockaddr_in *>(&ip)->sin_port) & 0xff;
+ if (rule.v.ipv4.mask > 32) rule.v.ipv4.mask = 32;
+ return true;
+ } else if (t == "MATCH_IPV4_DEST") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_IPV4_DEST;
+ InetAddress ip(_jS(r["ip"],"0.0.0.0"));
+ rule.v.ipv4.ip = reinterpret_cast<struct sockaddr_in *>(&ip)->sin_addr.s_addr;
+ rule.v.ipv4.mask = Utils::ntoh(reinterpret_cast<struct sockaddr_in *>(&ip)->sin_port) & 0xff;
+ if (rule.v.ipv4.mask > 32) rule.v.ipv4.mask = 32;
+ return true;
+ } else if (t == "MATCH_IPV6_SOURCE") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_IPV6_SOURCE;
+ InetAddress ip(_jS(r["ip"],"::0"));
+ memcpy(rule.v.ipv6.ip,reinterpret_cast<struct sockaddr_in6 *>(&ip)->sin6_addr.s6_addr,16);
+ rule.v.ipv6.mask = Utils::ntoh(reinterpret_cast<struct sockaddr_in6 *>(&ip)->sin6_port) & 0xff;
+ if (rule.v.ipv6.mask > 128) rule.v.ipv6.mask = 128;
+ return true;
+ } else if (t == "MATCH_IPV6_DEST") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_IPV6_DEST;
+ InetAddress ip(_jS(r["ip"],"::0"));
+ memcpy(rule.v.ipv6.ip,reinterpret_cast<struct sockaddr_in6 *>(&ip)->sin6_addr.s6_addr,16);
+ rule.v.ipv6.mask = Utils::ntoh(reinterpret_cast<struct sockaddr_in6 *>(&ip)->sin6_port) & 0xff;
+ if (rule.v.ipv6.mask > 128) rule.v.ipv6.mask = 128;
+ return true;
+ } else if (t == "MATCH_IP_TOS") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_IP_TOS;
+ rule.v.ipTos = (uint8_t)(_jI(r["ipTos"],0ULL) & 0xffULL);
+ return true;
+ } else if (t == "MATCH_IP_PROTOCOL") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_IP_PROTOCOL;
+ rule.v.ipProtocol = (uint8_t)(_jI(r["ipProtocol"],0ULL) & 0xffULL);
+ return true;
+ } else if (t == "MATCH_ETHERTYPE") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_ETHERTYPE;
+ rule.v.etherType = (uint16_t)(_jI(r["etherType"],0ULL) & 0xffffULL);
+ return true;
+ } else if (t == "MATCH_ICMP") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_ICMP;
+ rule.v.icmp.type = (uint8_t)(_jI(r["icmpType"],0ULL) & 0xffULL);
+ json &code = r["icmpCode"];
+ if (code.is_null()) {
+ rule.v.icmp.code = 0;
+ rule.v.icmp.flags = 0x00;
+ } else {
+ rule.v.icmp.code = (uint8_t)(_jI(code,0ULL) & 0xffULL);
+ rule.v.icmp.flags = 0x01;
+ }
+ return true;
+ } else if (t == "MATCH_IP_SOURCE_PORT_RANGE") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_IP_SOURCE_PORT_RANGE;
+ rule.v.port[0] = (uint16_t)(_jI(r["start"],0ULL) & 0xffffULL);
+ rule.v.port[1] = (uint16_t)(_jI(r["end"],(uint64_t)rule.v.port[0]) & 0xffffULL);
+ return true;
+ } else if (t == "MATCH_IP_DEST_PORT_RANGE") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_IP_DEST_PORT_RANGE;
+ rule.v.port[0] = (uint16_t)(_jI(r["start"],0ULL) & 0xffffULL);
+ rule.v.port[1] = (uint16_t)(_jI(r["end"],(uint64_t)rule.v.port[0]) & 0xffffULL);
+ return true;
+ } else if (t == "MATCH_CHARACTERISTICS") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_CHARACTERISTICS;
+ if (r.count("mask")) {
+ json &v = r["mask"];
+ if (v.is_number()) {
+ rule.v.characteristics = v;
+ } else {
+ std::string tmp = v;
+ rule.v.characteristics = Utils::hexStrToU64(tmp.c_str());
+ }
+ }
+ return true;
+ } else if (t == "MATCH_FRAME_SIZE_RANGE") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_FRAME_SIZE_RANGE;
+ rule.v.frameSize[0] = (uint16_t)(_jI(r["start"],0ULL) & 0xffffULL);
+ rule.v.frameSize[1] = (uint16_t)(_jI(r["end"],(uint64_t)rule.v.frameSize[0]) & 0xffffULL);
+ return true;
+ } else if (t == "MATCH_RANDOM") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_RANDOM;
+ rule.v.randomProbability = (uint32_t)(_jI(r["probability"],0ULL) & 0xffffffffULL);
+ } else if (t == "MATCH_TAGS_DIFFERENCE") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_TAGS_DIFFERENCE;
+ rule.v.tag.id = (uint32_t)(_jI(r["id"],0ULL) & 0xffffffffULL);
+ rule.v.tag.value = (uint32_t)(_jI(r["value"],0ULL) & 0xffffffffULL);
+ return true;
+ } else if (t == "MATCH_TAGS_BITWISE_AND") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_AND;
+ rule.v.tag.id = (uint32_t)(_jI(r["id"],0ULL) & 0xffffffffULL);
+ rule.v.tag.value = (uint32_t)(_jI(r["value"],0ULL) & 0xffffffffULL);
+ return true;
+ } else if (t == "MATCH_TAGS_BITWISE_OR") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_OR;
+ rule.v.tag.id = (uint32_t)(_jI(r["id"],0ULL) & 0xffffffffULL);
+ rule.v.tag.value = (uint32_t)(_jI(r["value"],0ULL) & 0xffffffffULL);
+ return true;
+ } else if (t == "MATCH_TAGS_BITWISE_XOR") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_TAGS_BITWISE_XOR;
+ rule.v.tag.id = (uint32_t)(_jI(r["id"],0ULL) & 0xffffffffULL);
+ rule.v.tag.value = (uint32_t)(_jI(r["value"],0ULL) & 0xffffffffULL);
+ return true;
+ } else if (t == "MATCH_TAGS_EQUAL") {
+ rule.t |= ZT_NETWORK_RULE_MATCH_TAGS_EQUAL;
+ rule.v.tag.id = (uint32_t)(_jI(r["id"],0ULL) & 0xffffffffULL);
+ rule.v.tag.value = (uint32_t)(_jI(r["value"],0ULL) & 0xffffffffULL);
+ return true;
+ }
+
+ return false;
+}
+
+EmbeddedNetworkController::EmbeddedNetworkController(Node *node,const char *dbPath) :
+ _node(node),
+ _path(dbPath),
+ _daemonRun(true)
+{
+ OSUtils::mkdir(dbPath);
+ OSUtils::lockDownFile(dbPath,true); // networks might contain auth tokens, etc., so restrict directory permissions
+ _daemon = Thread::start(this);
+}
+
+EmbeddedNetworkController::~EmbeddedNetworkController()
+{
+}
+
+void EmbeddedNetworkController::threadMain()
+ throw()
+{
+ uint64_t lastUpdatedNetworkMemberCache = 0;
+ while (_daemonRun) {
+ // Every 60 seconds we rescan the filesystem for network members and rebuild our cache
+ if ((OSUtils::now() - lastUpdatedNetworkMemberCache) >= 60000) {
+ const std::vector<std::string> networks(OSUtils::listSubdirectories((_path + ZT_PATH_SEPARATOR_S + "network").c_str()));
+ for(auto n=networks.begin();n!=networks.end();++n) {
+ if (n->length() == 16) {
+ const std::vector<std::string> members(OSUtils::listSubdirectories((*n + ZT_PATH_SEPARATOR_S + "member").c_str()));
+ std::map<Address,nlohmann::json> newCache;
+ for(auto m=members.begin();m!=members.end();++m) {
+ if (m->length() == ZT_ADDRESS_LENGTH_HEX) {
+ const Address maddr(*m);
+ try {
+ const json mj(_readJson((_path + ZT_PATH_SEPARATOR_S + "network" + ZT_PATH_SEPARATOR_S + *n + ZT_PATH_SEPARATOR_S + "member" + ZT_PATH_SEPARATOR_S + *m + ZT_PATH_SEPARATOR_S + "config.json")));
+ if ((mj.is_object())&&(mj.size() > 0)) {
+ newCache[maddr] = mj;
+ }
+ } catch ( ... ) {}
+ }
+ }
+ {
+ Mutex::Lock _l(_networkMemberCache_m);
+ _networkMemberCache[Utils::hexStrToU64(n->c_str())] = newCache;
+ }
+ }
+ }
+ lastUpdatedNetworkMemberCache = OSUtils::now();
+ }
+
+ Thread::sleep(25);
+ }
+}
+
+NetworkController::ResultCode EmbeddedNetworkController::doNetworkConfigRequest(const InetAddress &fromAddr,const Identity &signingId,const Identity &identity,uint64_t nwid,const Dictionary<ZT_NETWORKCONFIG_METADATA_DICT_CAPACITY> &metaData,NetworkConfig &nc)
+{
+ if (((!signingId)||(!signingId.hasPrivate()))||(signingId.address().toInt() != (nwid >> 24))) {
+ return NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR;
+ }
+
+ const uint64_t now = OSUtils::now();
+
+ // Check rate limit circuit breaker to prevent flooding
+ {
+ Mutex::Lock _l(_lastRequestTime_m);
+ uint64_t &lrt = _lastRequestTime[std::pair<uint64_t,uint64_t>(identity.address().toInt(),nwid)];
+ if ((now - lrt) <= ZT_NETCONF_MIN_REQUEST_PERIOD)
+ return NetworkController::NETCONF_QUERY_IGNORE;
+ lrt = now;
+ }
+
+ json network(_readJson(_networkJP(nwid,false)));
+ if (!network.size())
+ return NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND;
+
+ const std::string memberJP(_memberJP(nwid,identity.address(),true));
+ json member(_readJson(memberJP));
+ _initMember(member);
+
+ {
+ std::string haveIdStr(_jS(member["identity"],""));
+ if (haveIdStr.length() > 0) {
+ // If we already know this member's identity perform a full compare. This prevents
+ // a "collision" from being able to auth onto our network in place of an already
+ // known member.
+ try {
+ if (Identity(haveIdStr.c_str()) != identity)
+ return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
+ } catch ( ... ) {
+ return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
+ }
+ } else {
+ // If we do not yet know this member's identity, learn it.
+ member["identity"] = identity.toString(false);
+ }
+ }
+
+ // These are always the same, but make sure they are set
+ member["id"] = identity.address().toString();
+ member["address"] = member["id"];
+ member["nwid"] = network["id"];
+
+ // Determine whether and how member is authorized
+ const char *authorizedBy = (const char *)0;
+ if (_jB(member["authorized"],false)) {
+ authorizedBy = "memberIsAuthorized";
+ } else if (!_jB(network["private"],true)) {
+ authorizedBy = "networkIsPublic";
+ if (!member.count("authorized")) {
+ member["authorized"] = true;
+ json ah;
+ ah["a"] = true;
+ ah["by"] = authorizedBy;
+ ah["ts"] = now;
+ ah["ct"] = json();
+ ah["c"] = json();
+ member["authHistory"].push_back(ah);
+ member["lastModified"] = now;
+ json &revj = member["revision"];
+ member["revision"] = (revj.is_number() ? ((uint64_t)revj + 1ULL) : 1ULL);
+ }
+ } else {
+ char presentedAuth[512];
+ if (metaData.get(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_AUTH,presentedAuth,sizeof(presentedAuth)) > 0) {
+ presentedAuth[511] = (char)0; // sanity check
+
+ // Check for bearer token presented by member
+ if ((strlen(presentedAuth) > 6)&&(!strncmp(presentedAuth,"token:",6))) {
+ const char *const presentedToken = presentedAuth + 6;
+
+ json &authTokens = network["authTokens"];
+ if (authTokens.is_array()) {
+ for(unsigned long i=0;i<authTokens.size();++i) {
+ json &token = authTokens[i];
+ if (token.is_object()) {
+ const uint64_t expires = _jI(token["expires"],0ULL);
+ const uint64_t maxUses = _jI(token["maxUsesPerMember"],0ULL);
+ std::string tstr = _jS(token["token"],"");
+
+ if (((expires == 0ULL)||(expires > now))&&(tstr == presentedToken)) {
+ bool usable = (maxUses == 0);
+ if (!usable) {
+ uint64_t useCount = 0;
+ json &ahist = member["authHistory"];
+ if (ahist.is_array()) {
+ for(unsigned long j=0;j<ahist.size();++j) {
+ json &ah = ahist[j];
+ if ((_jS(ah["ct"],"") == "token")&&(_jS(ah["c"],"") == tstr)&&(_jB(ah["a"],false)))
+ ++useCount;
+ }
+ }
+ usable = (useCount < maxUses);
+ }
+ if (usable) {
+ authorizedBy = "token";
+ member["authorized"] = true;
+ json ah;
+ ah["a"] = true;
+ ah["by"] = authorizedBy;
+ ah["ts"] = now;
+ ah["ct"] = "token";
+ ah["c"] = tstr;
+ member["authHistory"].push_back(ah);
+ member["lastModified"] = now;
+ json &revj = member["revision"];
+ member["revision"] = (revj.is_number() ? ((uint64_t)revj + 1ULL) : 1ULL);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Log this request
+ {
+ json rlEntry = json::object();
+ rlEntry["ts"] = now;
+ rlEntry["authorized"] = (authorizedBy) ? true : false;
+ rlEntry["authorizedBy"] = (authorizedBy) ? authorizedBy : "";
+ rlEntry["clientMajorVersion"] = metaData.getUI(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MAJOR_VERSION,0);
+ rlEntry["clientMinorVersion"] = metaData.getUI(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MINOR_VERSION,0);
+ rlEntry["clientRevision"] = metaData.getUI(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_REVISION,0);
+ rlEntry["clientProtocolVersion"] = metaData.getUI(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_PROTOCOL_VERSION,0);
+ if (fromAddr)
+ rlEntry["fromAddr"] = fromAddr.toString();
+
+ json recentLog = json::array();
+ recentLog.push_back(rlEntry);
+ json &oldLog = member["recentLog"];
+ if (oldLog.is_array()) {
+ for(unsigned long i=0;i<oldLog.size();++i) {
+ recentLog.push_back(oldLog[i]);
+ if (recentLog.size() >= ZT_NETCONF_DB_MEMBER_HISTORY_LENGTH)
+ break;
+ }
+ }
+ member["recentLog"] = recentLog;
+ }
+
+ // If they are not authorized, STOP!
+ if (!authorizedBy) {
+ _writeJson(memberJP,member);
+ return NetworkController::NETCONF_QUERY_ACCESS_DENIED;
+ }
+
+ // -------------------------------------------------------------------------
+ // If we made it this far, they are authorized.
+ // -------------------------------------------------------------------------
+
+ _NetworkMemberInfo nmi;
+ _getNetworkMemberInfo(now,nwid,nmi);
+
+ // Compute credential TTL. This is the "moving window" for COM agreement and
+ // the global TTL for Capability and Tag objects. (The same value is used
+ // for both.) This is computed by reference to the last time we deauthorized
+ // a member, since within the time period since this event any temporal
+ // differences are not particularly relevant.
+ uint64_t credentialtmd = ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MIN_MAX_DELTA;
+ if (now > nmi.mostRecentDeauthTime)
+ credentialtmd += (now - nmi.mostRecentDeauthTime);
+ if (credentialtmd > ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MAX_MAX_DELTA)
+ credentialtmd = ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MAX_MAX_DELTA;
+
+ nc.networkId = nwid;
+ nc.type = _jB(network["private"],true) ? ZT_NETWORK_TYPE_PRIVATE : ZT_NETWORK_TYPE_PUBLIC;
+ nc.timestamp = now;
+ nc.credentialTimeMaxDelta = credentialtmd;
+ nc.revision = _jI(network["revision"],0ULL);
+ nc.issuedTo = identity.address();
+ if (_jB(network["enableBroadcast"],true)) nc.flags |= ZT_NETWORKCONFIG_FLAG_ENABLE_BROADCAST;
+ if (_jB(network["allowPassiveBridging"],false)) nc.flags |= ZT_NETWORKCONFIG_FLAG_ALLOW_PASSIVE_BRIDGING;
+ Utils::scopy(nc.name,sizeof(nc.name),_jS(network["name"],"").c_str());
+ nc.multicastLimit = (unsigned int)_jI(network["multicastLimit"],32ULL);
+
+ for(std::set<Address>::const_iterator ab(nmi.activeBridges.begin());ab!=nmi.activeBridges.end();++ab)
+ nc.addSpecialist(*ab,ZT_NETWORKCONFIG_SPECIALIST_TYPE_ACTIVE_BRIDGE);
+
+ json &v4AssignMode = network["v4AssignMode"];
+ json &v6AssignMode = network["v6AssignMode"];
+ json &ipAssignmentPools = network["ipAssignmentPools"];
+ json &routes = network["routes"];
+ json &rules = network["rules"];
+ json &capabilities = network["capabilities"];
+ json &memberCapabilities = member["capabilities"];
+ json &memberTags = member["tags"];
+
+ if (rules.is_array()) {
+ for(unsigned long i=0;i<rules.size();++i) {
+ if (nc.ruleCount >= ZT_MAX_NETWORK_RULES)
+ break;
+ if (_parseRule(rules[i],nc.rules[nc.ruleCount]))
+ ++nc.ruleCount;
+ }
+ }
+
+ if ((memberCapabilities.is_array())&&(memberCapabilities.size() > 0)&&(capabilities.is_array())) {
+ std::map< uint64_t,json * > capsById;
+ for(unsigned long i=0;i<capabilities.size();++i) {
+ json &cap = capabilities[i];
+ if (cap.is_object())
+ capsById[_jI(cap["id"],0ULL) & 0xffffffffULL] = &cap;
+ }
+
+ for(unsigned long i=0;i<memberCapabilities.size();++i) {
+ const uint64_t capId = _jI(memberCapabilities[i],0ULL) & 0xffffffffULL;
+ json *cap = capsById[capId];
+ if ((cap->is_object())&&(cap->size() > 0)) {
+ ZT_VirtualNetworkRule capr[ZT_MAX_CAPABILITY_RULES];
+ unsigned int caprc = 0;
+ json &caprj = (*cap)["rules"];
+ if ((caprj.is_array())&&(caprj.size() > 0)) {
+ for(unsigned long j=0;j<caprj.size();++j) {
+ if (caprc >= ZT_MAX_CAPABILITY_RULES)
+ break;
+ if (_parseRule(caprj[j],capr[caprc]))
+ ++caprc;
+ }
+ }
+ nc.capabilities[nc.capabilityCount] = Capability((uint32_t)capId,nwid,now,1,capr,caprc);
+ if (nc.capabilities[nc.capabilityCount].sign(signingId,identity.address()))
+ ++nc.capabilityCount;
+ if (nc.capabilityCount >= ZT_MAX_NETWORK_CAPABILITIES)
+ break;
+ }
+ }
+ }
+
+ if (memberTags.is_array()) {
+ std::map< uint32_t,uint32_t > tagsById;
+ for(unsigned long i=0;i<memberTags.size();++i) {
+ json &t = memberTags[i];
+ if ((t.is_array())&&(t.size() == 2))
+ tagsById[(uint32_t)(_jI(t[0],0ULL) & 0xffffffffULL)] = (uint32_t)(_jI(t[1],0ULL) & 0xffffffffULL);
+ }
+ for(std::map< uint32_t,uint32_t >::const_iterator t(tagsById.begin());t!=tagsById.end();++t) {
+ if (nc.tagCount >= ZT_MAX_NETWORK_TAGS)
+ break;
+ nc.tags[nc.tagCount] = Tag(nwid,now,identity.address(),t->first,t->second);
+ if (nc.tags[nc.tagCount].sign(signingId))
+ ++nc.tagCount;
+ }
+ }
+
+ if (routes.is_array()) {
+ for(unsigned long i=0;i<routes.size();++i) {
+ if (nc.routeCount >= ZT_MAX_NETWORK_ROUTES)
+ break;
+ json &route = routes[i];
+ json &target = route["target"];
+ json &via = route["via"];
+ if (target.is_string()) {
+ const InetAddress t(target.get<std::string>());
+ InetAddress v;
+ if (via.is_string()) v.fromString(via.get<std::string>());
+ if ((t.ss_family == AF_INET)||(t.ss_family == AF_INET6)) {
+ ZT_VirtualNetworkRoute *r = &(nc.routes[nc.routeCount]);
+ *(reinterpret_cast<InetAddress *>(&(r->target))) = t;
+ if (v.ss_family == t.ss_family)
+ *(reinterpret_cast<InetAddress *>(&(r->via))) = v;
+ ++nc.routeCount;
+ }
+ }
+ }
+ }
+
+ const bool noAutoAssignIps = _jB(member["noAutoAssignIps"],false);
+
+ if ((v6AssignMode.is_object())&&(!noAutoAssignIps)) {
+ if ((_jB(v6AssignMode["rfc4193"],false))&&(nc.staticIpCount < ZT_MAX_ZT_ASSIGNED_ADDRESSES)) {
+ nc.staticIps[nc.staticIpCount++] = InetAddress::makeIpv6rfc4193(nwid,identity.address().toInt());
+ nc.flags |= ZT_NETWORKCONFIG_FLAG_ENABLE_IPV6_NDP_EMULATION;
+ }
+ if ((_jB(v6AssignMode["6plane"],false))&&(nc.staticIpCount < ZT_MAX_ZT_ASSIGNED_ADDRESSES)) {
+ nc.staticIps[nc.staticIpCount++] = InetAddress::makeIpv66plane(nwid,identity.address().toInt());
+ nc.flags |= ZT_NETWORKCONFIG_FLAG_ENABLE_IPV6_NDP_EMULATION;
+ }
+ }
+
+ bool haveManagedIpv4AutoAssignment = false;
+ bool haveManagedIpv6AutoAssignment = false; // "special" NDP-emulated address types do not count
+ json ipAssignments = member["ipAssignments"]; // we want to make a copy
+ if (ipAssignments.is_array()) {
+ for(unsigned long i=0;i<ipAssignments.size();++i) {
+ if (!ipAssignments[i].is_string())
+ continue;
+ std::string ips = ipAssignments[i];
+ InetAddress ip(ips);
+
+ // IP assignments are only pushed if there is a corresponding local route. We also now get the netmask bits from
+ // this route, ignoring the netmask bits field of the assigned IP itself. Using that was worthless and a source
+ // of user error / poor UX.
+ int routedNetmaskBits = 0;
+ for(unsigned int rk=0;rk<nc.routeCount;++rk) {
+ if ( (!nc.routes[rk].via.ss_family) && (reinterpret_cast<const InetAddress *>(&(nc.routes[rk].target))->containsAddress(ip)) )
+ routedNetmaskBits = reinterpret_cast<const InetAddress *>(&(nc.routes[rk].target))->netmaskBits();
+ }
+
+ if (routedNetmaskBits > 0) {
+ if (nc.staticIpCount < ZT_MAX_ZT_ASSIGNED_ADDRESSES) {
+ ip.setPort(routedNetmaskBits);
+ nc.staticIps[nc.staticIpCount++] = ip;
+ }
+ if (ip.ss_family == AF_INET)
+ haveManagedIpv4AutoAssignment = true;
+ else if (ip.ss_family == AF_INET6)
+ haveManagedIpv6AutoAssignment = true;
+ }
+ }
+ } else {
+ ipAssignments = json::array();
+ }
+
+ if ( (ipAssignmentPools.is_array()) && ((v6AssignMode.is_object())&&(_jB(v6AssignMode["zt"],false))) && (!haveManagedIpv6AutoAssignment) && (!noAutoAssignIps) ) {
+ for(unsigned long p=0;((p<ipAssignmentPools.size())&&(!haveManagedIpv6AutoAssignment));++p) {
+ json &pool = ipAssignmentPools[p];
+ if (pool.is_object()) {
+ InetAddress ipRangeStart(_jS(pool["ipRangeStart"],""));
+ InetAddress ipRangeEnd(_jS(pool["ipRangeEnd"],""));
+ if ( (ipRangeStart.ss_family == AF_INET6) && (ipRangeEnd.ss_family == AF_INET6) ) {
+ uint64_t s[2],e[2],x[2],xx[2];
+ memcpy(s,ipRangeStart.rawIpData(),16);
+ memcpy(e,ipRangeEnd.rawIpData(),16);
+ s[0] = Utils::ntoh(s[0]);
+ s[1] = Utils::ntoh(s[1]);
+ e[0] = Utils::ntoh(e[0]);
+ e[1] = Utils::ntoh(e[1]);
+ x[0] = s[0];
+ x[1] = s[1];
+
+ for(unsigned int trialCount=0;trialCount<1000;++trialCount) {
+ if ((trialCount == 0)&&(e[1] > s[1])&&((e[1] - s[1]) >= 0xffffffffffULL)) {
+ // First see if we can just cram a ZeroTier ID into the higher 64 bits. If so do that.
+ xx[0] = Utils::hton(x[0]);
+ xx[1] = Utils::hton(x[1] + identity.address().toInt());
+ } else {
+ // Otherwise pick random addresses -- this technically doesn't explore the whole range if the lower 64 bit range is >= 1 but that won't matter since that would be huge anyway
+ Utils::getSecureRandom((void *)xx,16);
+ if ((e[0] > s[0]))
+ xx[0] %= (e[0] - s[0]);
+ else xx[0] = 0;
+ if ((e[1] > s[1]))
+ xx[1] %= (e[1] - s[1]);
+ else xx[1] = 0;
+ xx[0] = Utils::hton(x[0] + xx[0]);
+ xx[1] = Utils::hton(x[1] + xx[1]);
+ }
+
+ InetAddress ip6((const void *)xx,16,0);
+
+ // Check if this IP is within a local-to-Ethernet routed network
+ int routedNetmaskBits = 0;
+ for(unsigned int rk=0;rk<nc.routeCount;++rk) {
+ if ( (!nc.routes[rk].via.ss_family) && (nc.routes[rk].target.ss_family == AF_INET6) && (reinterpret_cast<const InetAddress *>(&(nc.routes[rk].target))->containsAddress(ip6)) )
+ routedNetmaskBits = reinterpret_cast<const InetAddress *>(&(nc.routes[rk].target))->netmaskBits();
+ }
+
+ // If it's routed, then try to claim and assign it and if successful end loop
+ if ((routedNetmaskBits > 0)&&(!nmi.allocatedIps.count(ip6))) {
+ ipAssignments.push_back(ip6.toIpString());
+ member["ipAssignments"] = ipAssignments;
+ ip6.setPort((unsigned int)routedNetmaskBits);
+ if (nc.staticIpCount < ZT_MAX_ZT_ASSIGNED_ADDRESSES)
+ nc.staticIps[nc.staticIpCount++] = ip6;
+ haveManagedIpv6AutoAssignment = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if ( (ipAssignmentPools.is_array()) && ((v4AssignMode.is_object())&&(_jB(v4AssignMode["zt"],false))) && (!haveManagedIpv4AutoAssignment) && (!noAutoAssignIps) ) {
+ for(unsigned long p=0;((p<ipAssignmentPools.size())&&(!haveManagedIpv4AutoAssignment));++p) {
+ json &pool = ipAssignmentPools[p];
+ if (pool.is_object()) {
+ InetAddress ipRangeStartIA(_jS(pool["ipRangeStart"],""));
+ InetAddress ipRangeEndIA(_jS(pool["ipRangeEnd"],""));
+ if ( (ipRangeStartIA.ss_family == AF_INET) && (ipRangeEndIA.ss_family == AF_INET) ) {
+ uint32_t ipRangeStart = Utils::ntoh((uint32_t)(reinterpret_cast<struct sockaddr_in *>(&ipRangeStartIA)->sin_addr.s_addr));
+ uint32_t ipRangeEnd = Utils::ntoh((uint32_t)(reinterpret_cast<struct sockaddr_in *>(&ipRangeEndIA)->sin_addr.s_addr));
+ if ((ipRangeEnd < ipRangeStart)||(ipRangeStart == 0))
+ continue;
+ uint32_t ipRangeLen = ipRangeEnd - ipRangeStart;
+
+ // Start with the LSB of the member's address
+ uint32_t ipTrialCounter = (uint32_t)(identity.address().toInt() & 0xffffffff);
+
+ for(uint32_t k=ipRangeStart,trialCount=0;((k<=ipRangeEnd)&&(trialCount < 1000));++k,++trialCount) {
+ uint32_t ip = (ipRangeLen > 0) ? (ipRangeStart + (ipTrialCounter % ipRangeLen)) : ipRangeStart;
+ ++ipTrialCounter;
+ if ((ip & 0x000000ff) == 0x000000ff)
+ continue; // don't allow addresses that end in .255
+
+ // Check if this IP is within a local-to-Ethernet routed network
+ int routedNetmaskBits = -1;
+ for(unsigned int rk=0;rk<nc.routeCount;++rk) {
+ if (nc.routes[rk].target.ss_family == AF_INET) {
+ uint32_t targetIp = Utils::ntoh((uint32_t)(reinterpret_cast<const struct sockaddr_in *>(&(nc.routes[rk].target))->sin_addr.s_addr));
+ int targetBits = Utils::ntoh((uint16_t)(reinterpret_cast<const struct sockaddr_in *>(&(nc.routes[rk].target))->sin_port));
+ if ((ip & (0xffffffff << (32 - targetBits))) == targetIp) {
+ routedNetmaskBits = targetBits;
+ break;
+ }
+ }
+ }
+
+ // If it's routed, then try to claim and assign it and if successful end loop
+ const InetAddress ip4(Utils::hton(ip),0);
+ if ((routedNetmaskBits > 0)&&(!nmi.allocatedIps.count(ip4))) {
+ ipAssignments.push_back(ip4.toIpString());
+ member["ipAssignments"] = ipAssignments;
+ if (nc.staticIpCount < ZT_MAX_ZT_ASSIGNED_ADDRESSES) {
+ struct sockaddr_in *const v4ip = reinterpret_cast<struct sockaddr_in *>(&(nc.staticIps[nc.staticIpCount++]));
+ v4ip->sin_family = AF_INET;
+ v4ip->sin_port = Utils::hton((uint16_t)routedNetmaskBits);
+ v4ip->sin_addr.s_addr = Utils::hton(ip);
+ }
+ haveManagedIpv4AutoAssignment = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ CertificateOfMembership com(now,credentialtmd,nwid,identity.address());
+ if (com.sign(signingId)) {
+ nc.com = com;
+ } else {
+ return NETCONF_QUERY_INTERNAL_SERVER_ERROR;
+ }
+
+ _writeJson(memberJP,member);
+ return NetworkController::NETCONF_QUERY_OK;
+}
+
+unsigned int EmbeddedNetworkController::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)
+{
+ if ((path.size() > 0)&&(path[0] == "network")) {
+
+ if ((path.size() >= 2)&&(path[1].length() == 16)) {
+ const uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
+ char nwids[24];
+ Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
+
+ json network(_readJson(_networkJP(nwid,false)));
+ if (!network.size())
+ return 404;
+
+ if (path.size() >= 3) {
+
+ if (path[2] == "member") {
+
+ if (path.size() >= 4) {
+ const uint64_t address = Utils::hexStrToU64(path[3].c_str());
+
+ json member(_readJson(_memberJP(nwid,Address(address),false)));
+ if (!member.size())
+ return 404;
+
+ _addMemberNonPersistedFields(member,OSUtils::now());
+ responseBody = member.dump(2);
+ responseContentType = "application/json";
+
+ return 200;
+ } else {
+
+ responseBody = "{";
+ std::vector<std::string> members(OSUtils::listSubdirectories((_networkBP(nwid,false) + ZT_PATH_SEPARATOR_S + "member").c_str()));
+ for(std::vector<std::string>::iterator i(members.begin());i!=members.end();++i) {
+ if (i->length() == ZT_ADDRESS_LENGTH_HEX) {
+ json member(_readJson(_memberJP(nwid,Address(Utils::hexStrToU64(i->c_str())),false)));
+ if (member.size()) {
+ responseBody.append((responseBody.length() == 1) ? "\"" : ",\"");
+ responseBody.append(*i);
+ responseBody.append("\":");
+ responseBody.append(_jS(member["revision"],"0"));
+ }
+ }
+ }
+ responseBody.push_back('}');
+ responseContentType = "application/json";
+
+ return 200;
+ }
+
+ } else if ((path[2] == "test")&&(path.size() >= 4)) {
+
+ Mutex::Lock _l(_circuitTests_m);
+ std::map< uint64_t,_CircuitTestEntry >::iterator cte(_circuitTests.find(Utils::hexStrToU64(path[3].c_str())));
+ if ((cte != _circuitTests.end())&&(cte->second.test)) {
+
+ responseBody = "[";
+ responseBody.append(cte->second.jsonResults);
+ responseBody.push_back(']');
+ responseContentType = "application/json";
+
+ return 200;
+
+ } // else 404
+
+ } // else 404
+
+ } else {
+
+ const uint64_t now = OSUtils::now();
+ _NetworkMemberInfo nmi;
+ _getNetworkMemberInfo(now,nwid,nmi);
+ _addNetworkNonPersistedFields(network,now,nmi);
+ responseBody = network.dump(2);
+ responseContentType = "application/json";
+ return 200;
+
+ }
+ } else if (path.size() == 1) {
+
+ responseBody = "[";
+ std::vector<std::string> networks(OSUtils::listSubdirectories((_path + ZT_PATH_SEPARATOR_S + "network").c_str()));
+ for(auto i(networks.begin());i!=networks.end();++i) {
+ if (i->length() == 16) {
+ responseBody.append((responseBody.length() == 1) ? "\"" : ",\"");
+ responseBody.append(*i);
+ responseBody.append("\"");
+ }
+ }
+ responseBody.push_back(']');
+ responseContentType = "application/json";
+ return 200;
+
+ } // else 404
+
+ } else {
+
+ char tmp[4096];
+ Utils::snprintf(tmp,sizeof(tmp),"{\n\t\"controller\": true,\n\t\"apiVersion\": %d,\n\t\"clock\": %llu\n}\n",ZT_NETCONF_CONTROLLER_API_VERSION,(unsigned long long)OSUtils::now());
+ responseBody = tmp;
+ responseContentType = "application/json";
+ return 200;
+
+ }
+
+ return 404;
+}
+
+unsigned int EmbeddedNetworkController::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;
+
+ json b;
+ try {
+ b = json::parse(body);
+ if (!b.is_object()) {
+ responseBody = "{ \"message\": \"body is not a JSON object\" }";
+ responseContentType = "application/json";
+ return 400;
+ }
+ } catch (std::exception &exc) {
+ responseBody = std::string("{ \"message\": \"body JSON is invalid: ") + exc.what() + "\" }";
+ responseContentType = "application/json";
+ return 400;
+ } catch ( ... ) {
+ responseBody = "{ \"message\": \"body JSON is invalid\" }";
+ responseContentType = "application/json";
+ return 400;
+ }
+ const uint64_t now = OSUtils::now();
+
+ 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);
+
+ if (path.size() >= 3) {
+ json network(_readJson(_networkJP(nwid,false)));
+ if (!network.size())
+ 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",(unsigned long long)address);
+
+ json member(_readJson(_memberJP(nwid,Address(address),true)));
+ _initMember(member);
+
+ try {
+ if (b.count("activeBridge")) member["activeBridge"] = _jB(b["activeBridge"],false);
+ if (b.count("noAutoAssignIps")) member["noAutoAssignIps"] = _jB(b["noAutoAssignIps"],false);
+ if ((b.count("identity"))&&(!member.count("identity"))) member["identity"] = _jS(b["identity"],""); // allow identity to be populated only if not already known
+
+ if (b.count("authorized")) {
+ const bool newAuth = _jB(b["authorized"],false);
+ if (newAuth != _jB(member["authorized"],false)) {
+ member["authorized"] = newAuth;
+ json ah;
+ ah["a"] = newAuth;
+ ah["by"] = "api";
+ ah["ts"] = now;
+ ah["ct"] = json();
+ ah["c"] = json();
+ member["authHistory"].push_back(ah);
+ }
+ }
+
+ if (b.count("ipAssignments")) {
+ auto ipa = b["ipAssignments"];
+ if (ipa.is_array()) {
+ json mipa(json::array());
+ for(unsigned long i=0;i<ipa.size();++i) {
+ std::string ips = ipa[i];
+ InetAddress ip(ips);
+ if ((ip.ss_family == AF_INET)||(ip.ss_family == AF_INET6)) {
+ mipa.push_back(ip.toIpString());
+ }
+ }
+ member["ipAssignments"] = mipa;
+ }
+ }
+
+ if (b.count("tags")) {
+ auto tags = b["tags"];
+ if (tags.is_array()) {
+ std::map<uint64_t,uint64_t> mtags;
+ for(unsigned long i=0;i<tags.size();++i) {
+ auto tag = tags[i];
+ if ((tag.is_array())&&(tag.size() == 2))
+ mtags[_jI(tag[0],0ULL) & 0xffffffffULL] = _jI(tag[1],0ULL) & 0xffffffffULL;
+ }
+ json mtagsa = json::array();
+ for(std::map<uint64_t,uint64_t>::iterator t(mtags.begin());t!=mtags.end();++t) {
+ json ta = json::array();
+ ta.push_back(t->first);
+ ta.push_back(t->second);
+ mtagsa.push_back(ta);
+ }
+ member["tags"] = mtagsa;
+ }
+ }
+
+ if (b.count("capabilities")) {
+ auto capabilities = b["capabilities"];
+ if (capabilities.is_array()) {
+ json mcaps = json::array();
+ for(unsigned long i=0;i<capabilities.size();++i) {
+ mcaps.push_back(_jI(capabilities[i],0ULL));
+ }
+ std::sort(mcaps.begin(),mcaps.end());
+ mcaps.erase(std::unique(mcaps.begin(),mcaps.end()),mcaps.end());
+ member["capabilities"] = mcaps;
+ }
+ }
+ } catch ( ... ) {
+ responseBody = "{ \"message\": \"exception while processing parameters in JSON body\" }";
+ responseContentType = "application/json";
+ return 400;
+ }
+
+ member["id"] = addrs;
+ member["address"] = addrs; // legacy
+ member["nwid"] = nwids;
+ member["lastModified"] = now;
+ auto revj = member["revision"];
+ member["revision"] = (revj.is_number() ? ((uint64_t)revj + 1ULL) : 1ULL);
+
+ _writeJson(_memberJP(nwid,Address(address),true).c_str(),member);
+
+ {
+ Mutex::Lock _l(_networkMemberCache_m);
+ _networkMemberCache[nwid][Address(address)] = member;
+ }
+
+ // Add non-persisted fields
+ member["clock"] = now;
+
+ responseBody = member.dump(2);
+ responseContentType = "application/json";
+ return 200;
+ } else if ((path.size() == 3)&&(path[2] == "test")) {
+
+ Mutex::Lock _l(_circuitTests_m);
+
+ ZT_CircuitTest *test = (ZT_CircuitTest *)malloc(sizeof(ZT_CircuitTest));
+ memset(test,0,sizeof(ZT_CircuitTest));
+
+ Utils::getSecureRandom(&(test->testId),sizeof(test->testId));
+ test->credentialNetworkId = nwid;
+ test->ptr = (void *)this;
+ json hops = b["hops"];
+ if (hops.is_array()) {
+ for(unsigned long i=0;i<hops.size();++i) {
+ auto hops2 = hops[i];
+ if (hops2.is_array()) {
+ for(unsigned long j=0;j<hops2.size();++j) {
+ std::string s = hops2[j];
+ test->hops[test->hopCount].addresses[test->hops[test->hopCount].breadth++] = Utils::hexStrToU64(s.c_str()) & 0xffffffffffULL;
+ }
+ } else if (hops2.is_string()) {
+ std::string s = hops2;
+ test->hops[test->hopCount].addresses[test->hops[test->hopCount].breadth++] = Utils::hexStrToU64(s.c_str()) & 0xffffffffffULL;
+ }
+ }
+ }
+ test->reportAtEveryHop = (_jB(b["reportAtEveryHop"],true) ? 1 : 0);
+
+ if (!test->hopCount) {
+ ::free((void *)test);
+ responseBody = "{ \"message\": \"a test must contain at least one hop\" }";
+ responseContentType = "application/json";
+ return 400;
+ }
+
+ test->timestamp = OSUtils::now();
+
+ _CircuitTestEntry &te = _circuitTests[test->testId];
+ te.test = test;
+ te.jsonResults = "";
+
+ if (_node)
+ _node->circuitTestBegin(test,&(EmbeddedNetworkController::_circuitTestCallback));
+ else return 500;
+
+ char json[1024];
+ Utils::snprintf(json,sizeof(json),"{\"testId\":\"%.16llx\"}",test->testId);
+ responseBody = json;
+ responseContentType = "application/json";
+ return 200;
+
+ } // else 404
+
+ } else {
+ // POST to network ID
+
+ // Magic ID ending with ______ picks a random unused network ID
+ if (path[1].substr(10) == "______") {
+ nwid = 0;
+ uint64_t nwidPrefix = (Utils::hexStrToU64(path[1].substr(0,10).c_str()) << 24) & 0xffffffffff000000ULL;
+ uint64_t nwidPostfix = 0;
+ for(unsigned long k=0;k<100000;++k) { // sanity limit on trials
+ Utils::getSecureRandom(&nwidPostfix,sizeof(nwidPostfix));
+ uint64_t tryNwid = nwidPrefix | (nwidPostfix & 0xffffffULL);
+ if ((tryNwid & 0xffffffULL) == 0ULL) tryNwid |= 1ULL;
+ Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)tryNwid);
+ if (!OSUtils::fileExists(_networkJP(tryNwid,false).c_str())) {
+ nwid = tryNwid;
+ break;
+ }
+ }
+ if (!nwid)
+ return 503;
+ }
+
+ json network(_readJson(_networkJP(nwid,true)));
+ _initNetwork(network);
+
+ try {
+ if (b.count("name")) network["name"] = _jS(b["name"],"");
+ if (b.count("private")) network["private"] = _jB(b["private"],true);
+ if (b.count("enableBroadcast")) network["enableBroadcast"] = _jB(b["enableBroadcast"],false);
+ if (b.count("allowPassiveBridging")) network["allowPassiveBridging"] = _jB(b["allowPassiveBridging"],false);
+ if (b.count("multicastLimit")) network["multicastLimit"] = _jI(b["multicastLimit"],32ULL);
+
+ if (b.count("v4AssignMode")) {
+ json nv4m;
+ json &v4m = b["v4AssignMode"];
+ if (v4m.is_string()) { // backward compatibility
+ nv4m["zt"] = (_jS(v4m,"") == "zt");
+ } else if (v4m.is_object()) {
+ nv4m["zt"] = _jB(v4m["zt"],false);
+ } else nv4m["zt"] = false;
+ network["v4AssignMode"] = nv4m;
+ }
+
+ if (b.count("v6AssignMode")) {
+ json nv6m;
+ json &v6m = b["v6AssignMode"];
+ if (!nv6m.is_object()) nv6m = json::object();
+ if (v6m.is_string()) { // backward compatibility
+ std::vector<std::string> v6ms(Utils::split(_jS(v6m,"").c_str(),",","",""));
+ std::sort(v6ms.begin(),v6ms.end());
+ v6ms.erase(std::unique(v6ms.begin(),v6ms.end()),v6ms.end());
+ nv6m["rfc4193"] = false;
+ nv6m["zt"] = false;
+ nv6m["6plane"] = false;
+ for(std::vector<std::string>::iterator i(v6ms.begin());i!=v6ms.end();++i) {
+ if (*i == "rfc4193")
+ nv6m["rfc4193"] = true;
+ else if (*i == "zt")
+ nv6m["zt"] = true;
+ else if (*i == "6plane")
+ nv6m["6plane"] = true;
+ }
+ } else if (v6m.is_object()) {
+ if (v6m.count("rfc4193")) nv6m["rfc4193"] = _jB(v6m["rfc4193"],false);
+ if (v6m.count("zt")) nv6m["zt"] = _jB(v6m["zt"],false);
+ if (v6m.count("6plane")) nv6m["6plane"] = _jB(v6m["6plane"],false);
+ } else {
+ nv6m["rfc4193"] = false;
+ nv6m["zt"] = false;
+ nv6m["6plane"] = false;
+ }
+ network["v6AssignMode"] = nv6m;
+ }
+
+ if (b.count("routes")) {
+ json &rts = b["routes"];
+ if (rts.is_array()) {
+ json nrts = json::array();
+ for(unsigned long i=0;i<rts.size();++i) {
+ json &rt = rts[i];
+ if (rt.is_object()) {
+ json &target = rt["target"];
+ json &via = rt["via"];
+ if (target.is_string()) {
+ InetAddress t(target.get<std::string>());
+ InetAddress v;
+ if (via.is_string()) v.fromString(via.get<std::string>());
+ if ( ((t.ss_family == AF_INET)||(t.ss_family == AF_INET6)) && (t.netmaskBitsValid()) ) {
+ json tmp;
+ tmp["target"] = t.toString();
+ if (v.ss_family == t.ss_family)
+ tmp["via"] = v.toIpString();
+ else tmp["via"] = json();
+ nrts.push_back(tmp);
+ }
+ }
+ }
+ }
+ network["routes"] = nrts;
+ }
+ }
+
+ if (b.count("ipAssignmentPools")) {
+ json &ipp = b["ipAssignmentPools"];
+ if (ipp.is_array()) {
+ json nipp = json::array();
+ for(unsigned long i=0;i<ipp.size();++i) {
+ auto ip = ipp[i];
+ if ((ip.is_object())&&(ip.count("ipRangeStart"))&&(ip.count("ipRangeEnd"))) {
+ InetAddress f(_jS(ip["ipRangeStart"],""));
+ InetAddress t(_jS(ip["ipRangeEnd"],""));
+ if ( ((f.ss_family == AF_INET)||(f.ss_family == AF_INET6)) && (f.ss_family == t.ss_family) ) {
+ json tmp = json::object();
+ tmp["ipRangeStart"] = f.toIpString();
+ tmp["ipRangeEnd"] = t.toIpString();
+ nipp.push_back(tmp);
+ }
+ }
+ }
+ network["ipAssignmentPools"] = nipp;
+ }
+ }
+
+ if (b.count("rules")) {
+ json &rules = b["rules"];
+ if (rules.is_array()) {
+ json nrules = json::array();
+ for(unsigned long i=0;i<rules.size();++i) {
+ json &rule = rules[i];
+ if (rule.is_object()) {
+ ZT_VirtualNetworkRule ztr;
+ if (_parseRule(rule,ztr))
+ nrules.push_back(_renderRule(ztr));
+ }
+ }
+ network["rules"] = nrules;
+ }
+ }
+
+ if (b.count("authTokens")) {
+ json &authTokens = b["authTokens"];
+ if (authTokens.is_array()) {
+ json nat = json::array();
+ for(unsigned long i=0;i<authTokens.size();++i) {
+ json &token = authTokens[i];
+ if (token.is_object()) {
+ std::string tstr = token["token"];
+ if (tstr.length() > 0) {
+ json t = json::object();
+ t["token"] = tstr;
+ t["expires"] = _jI(token["expires"],0ULL);
+ t["maxUsesPerMember"] = _jI(token["maxUsesPerMember"],0ULL);
+ nat.push_back(t);
+ }
+ }
+ }
+ network["authTokens"] = nat;
+ }
+ }
+
+ if (b.count("capabilities")) {
+ json &capabilities = b["capabilities"];
+ if (capabilities.is_array()) {
+ std::map< uint64_t,json > ncaps;
+ for(unsigned long i=0;i<capabilities.size();++i) {
+ json &cap = capabilities[i];
+ if (cap.is_object()) {
+ json ncap = json::object();
+ const uint64_t capId = _jI(cap["id"],0ULL);
+ ncap["id"] = capId;
+
+ json &rules = cap["rules"];
+ json nrules = json::array();
+ if (rules.is_array()) {
+ for(unsigned long i=0;i<rules.size();++i) {
+ json rule = rules[i];
+ if (rule.is_object()) {
+ ZT_VirtualNetworkRule ztr;
+ if (_parseRule(rule,ztr))
+ nrules.push_back(_renderRule(ztr));
+ }
+ }
+ }
+ ncap["rules"] = nrules;
+
+ ncaps[capId] = ncap;
+ }
+ }
+
+ json ncapsa = json::array();
+ for(std::map< uint64_t,json >::iterator c(ncaps.begin());c!=ncaps.end();++c)
+ ncapsa.push_back(c->second);
+ network["capabilities"] = ncapsa;
+ }
+ }
+ } catch ( ... ) {
+ responseBody = "{ \"message\": \"exception occurred while parsing body variables\" }";
+ responseContentType = "application/json";
+ return 400;
+ }
+
+ network["id"] = nwids;
+ network["nwid"] = nwids; // legacy
+ auto rev = network["revision"];
+ network["revision"] = (rev.is_number() ? ((uint64_t)rev + 1ULL) : 1ULL);
+ network["lastModified"] = now;
+
+ _writeJson(_networkJP(nwid,true),network);
+
+ _NetworkMemberInfo nmi;
+ _getNetworkMemberInfo(now,nwid,nmi);
+ _addNetworkNonPersistedFields(network,now,nmi);
+
+ responseBody = network.dump(2);
+ responseContentType = "application/json";
+ return 200;
+ } // else 404
+
+ } // else 404
+
+ } // else 404
+
+ return 404;
+}
+
+unsigned int EmbeddedNetworkController::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)
+{
+ if (path.empty())
+ return 404;
+
+ if (path[0] == "network") {
+ if ((path.size() >= 2)&&(path[1].length() == 16)) {
+ const uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
+
+ json network(_readJson(_networkJP(nwid,false)));
+ if (!network.size())
+ return 404;
+
+ if (path.size() >= 3) {
+ if ((path.size() == 4)&&(path[2] == "member")&&(path[3].length() == 10)) {
+ const uint64_t address = Utils::hexStrToU64(path[3].c_str());
+
+ json member(_readJson(_memberJP(nwid,Address(address),false)));
+ if (!member.size())
+ return 404;
+
+ OSUtils::rmDashRf(_memberBP(nwid,Address(address),false).c_str());
+
+ responseBody = member.dump(2);
+ responseContentType = "application/json";
+ return 200;
+ }
+ } else {
+ OSUtils::rmDashRf(_networkBP(nwid,false).c_str());
+ {
+ Mutex::Lock _l(_networkMemberCache_m);
+ _networkMemberCache.erase(nwid);
+ }
+ responseBody = network.dump(2);
+ responseContentType = "application/json";
+ return 200;
+ }
+ } // else 404
+
+ } // else 404
+
+ return 404;
+}
+
+void EmbeddedNetworkController::_circuitTestCallback(ZT_Node *node,ZT_CircuitTest *test,const ZT_CircuitTestReport *report)
+{
+ char tmp[65535];
+ EmbeddedNetworkController *const self = reinterpret_cast<EmbeddedNetworkController *>(test->ptr);
+
+ if (!test)
+ return;
+ if (!report)
+ return;
+
+ Mutex::Lock _l(self->_circuitTests_m);
+ std::map< uint64_t,_CircuitTestEntry >::iterator cte(self->_circuitTests.find(test->testId));
+
+ if (cte == self->_circuitTests.end()) { // sanity check: a circuit test we didn't launch?
+ self->_node->circuitTestEnd(test);
+ ::free((void *)test);
+ return;
+ }
+
+ Utils::snprintf(tmp,sizeof(tmp),
+ "%s{\n"
+ "\t\"timestamp\": %llu," ZT_EOL_S
+ "\t\"testId\": \"%.16llx\"," ZT_EOL_S
+ "\t\"upstream\": \"%.10llx\"," ZT_EOL_S
+ "\t\"current\": \"%.10llx\"," ZT_EOL_S
+ "\t\"receivedTimestamp\": %llu," ZT_EOL_S
+ "\t\"sourcePacketId\": \"%.16llx\"," ZT_EOL_S
+ "\t\"flags\": %llu," ZT_EOL_S
+ "\t\"sourcePacketHopCount\": %u," ZT_EOL_S
+ "\t\"errorCode\": %u," ZT_EOL_S
+ "\t\"vendor\": %d," ZT_EOL_S
+ "\t\"protocolVersion\": %u," ZT_EOL_S
+ "\t\"majorVersion\": %u," ZT_EOL_S
+ "\t\"minorVersion\": %u," ZT_EOL_S
+ "\t\"revision\": %u," ZT_EOL_S
+ "\t\"platform\": %d," ZT_EOL_S
+ "\t\"architecture\": %d," ZT_EOL_S
+ "\t\"receivedOnLocalAddress\": \"%s\"," ZT_EOL_S
+ "\t\"receivedFromRemoteAddress\": \"%s\"" ZT_EOL_S
+ "}",
+ ((cte->second.jsonResults.length() > 0) ? ",\n" : ""),
+ (unsigned long long)report->timestamp,
+ (unsigned long long)test->testId,
+ (unsigned long long)report->upstream,
+ (unsigned long long)report->current,
+ (unsigned long long)OSUtils::now(),
+ (unsigned long long)report->sourcePacketId,
+ (unsigned long long)report->flags,
+ report->sourcePacketHopCount,
+ report->errorCode,
+ (int)report->vendor,
+ report->protocolVersion,
+ report->majorVersion,
+ report->minorVersion,
+ report->revision,
+ (int)report->platform,
+ (int)report->architecture,
+ reinterpret_cast<const InetAddress *>(&(report->receivedOnLocalAddress))->toString().c_str(),
+ reinterpret_cast<const InetAddress *>(&(report->receivedFromRemoteAddress))->toString().c_str());
+
+ cte->second.jsonResults.append(tmp);
+}
+
+void EmbeddedNetworkController::_getNetworkMemberInfo(uint64_t now,uint64_t nwid,_NetworkMemberInfo &nmi)
+{
+ Mutex::Lock _mcl(_networkMemberCache_m);
+ std::map< Address,nlohmann::json > &memberCacheEntry = _networkMemberCache[nwid];
+ nmi.totalMemberCount = memberCacheEntry.size();
+ for(std::map< Address,nlohmann::json >::iterator nm(memberCacheEntry.begin());nm!=memberCacheEntry.end();++nm) {
+ if (_jB(nm->second["authorized"],false)) {
+ ++nmi.authorizedMemberCount;
+
+ if (nm->second.count("recentLog")) {
+ json &mlog = nm->second["recentLog"];
+ if ((mlog.is_array())&&(mlog.size() > 0)) {
+ json &mlog1 = mlog[0];
+ if (mlog1.is_object()) {
+ if ((now - _jI(mlog1["ts"],0ULL)) < ZT_NETCONF_NODE_ACTIVE_THRESHOLD)
+ ++nmi.activeMemberCount;
+ }
+ }
+ }
+
+ if (_jB(nm->second["activeBridge"],false)) {
+ nmi.activeBridges.insert(nm->first);
+ }
+
+ if (nm->second.count("ipAssignments")) {
+ json &mips = nm->second["ipAssignments"];
+ if (mips.is_array()) {
+ for(unsigned long i=0;i<mips.size();++i) {
+ InetAddress mip(_jS(mips[i],""));
+ if ((mip.ss_family == AF_INET)||(mip.ss_family == AF_INET6))
+ nmi.allocatedIps.insert(mip);
+ }
+ }
+ }
+ } else {
+ nmi.mostRecentDeauthTime = std::max(nmi.mostRecentDeauthTime,_jI(nm->second["lastDeauthorizedTime"],0ULL));
+ }
+ }
+}
+
+} // namespace ZeroTier
diff --git a/controller/EmbeddedNetworkController.hpp b/controller/EmbeddedNetworkController.hpp
new file mode 100644
index 00000000..99e1836d
--- /dev/null
+++ b/controller/EmbeddedNetworkController.hpp
@@ -0,0 +1,213 @@
+/*
+ * ZeroTier One - Network Virtualization Everywhere
+ * Copyright (C) 2011-2015 ZeroTier, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ZT_SQLITENETWORKCONTROLLER_HPP
+#define ZT_SQLITENETWORKCONTROLLER_HPP
+
+#include <stdint.h>
+
+#include <string>
+#include <map>
+#include <vector>
+#include <set>
+#include <list>
+
+#include "../node/Constants.hpp"
+
+#include "../node/NetworkController.hpp"
+#include "../node/Mutex.hpp"
+#include "../node/Utils.hpp"
+#include "../node/Address.hpp"
+#include "../node/InetAddress.hpp"
+
+#include "../osdep/OSUtils.hpp"
+#include "../osdep/Thread.hpp"
+
+#include "../ext/json/json.hpp"
+
+namespace ZeroTier {
+
+class Node;
+
+class EmbeddedNetworkController : public NetworkController
+{
+public:
+ EmbeddedNetworkController(Node *node,const char *dbPath);
+ virtual ~EmbeddedNetworkController();
+
+ // Thread main method -- do not call directly
+ void threadMain()
+ throw();
+
+ virtual NetworkController::ResultCode doNetworkConfigRequest(
+ const InetAddress &fromAddr,
+ const Identity &signingId,
+ const Identity &identity,
+ uint64_t nwid,
+ const Dictionary<ZT_NETWORKCONFIG_METADATA_DICT_CAPACITY> &metaData,
+ NetworkConfig &nc);
+
+ unsigned int 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);
+ unsigned int 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);
+ unsigned int 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);
+
+private:
+ static void _circuitTestCallback(ZT_Node *node,ZT_CircuitTest *test,const ZT_CircuitTestReport *report);
+
+ // Network base path and network JSON path
+ inline std::string _networkBP(const uint64_t nwid,bool create)
+ {
+ char tmp[64];
+ std::string p(_path + ZT_PATH_SEPARATOR_S + "network");
+ if (create) OSUtils::mkdir(p.c_str());
+ p.push_back(ZT_PATH_SEPARATOR);
+ Utils::snprintf(tmp,sizeof(tmp),"%.16llx",nwid);
+ p.append(tmp);
+ if (create) OSUtils::mkdir(p.c_str());
+ return p;
+ }
+ inline std::string _networkJP(const uint64_t nwid,bool create)
+ {
+ return (_networkBP(nwid,create) + ZT_PATH_SEPARATOR + "config.json");
+ }
+
+ // Member base path and member JSON path
+ inline std::string _memberBP(const uint64_t nwid,const Address &member,bool create)
+ {
+ std::string p(_networkBP(nwid,create));
+ p.push_back(ZT_PATH_SEPARATOR);
+ p.append("member");
+ if (create) OSUtils::mkdir(p.c_str());
+ p.push_back(ZT_PATH_SEPARATOR);
+ p.append(member.toString());
+ if (create) OSUtils::mkdir(p.c_str());
+ return p;
+ }
+ inline std::string _memberJP(const uint64_t nwid,const Address &member,bool create)
+ {
+ return (_memberBP(nwid,member,create) + ZT_PATH_SEPARATOR + "config.json");
+ }
+
+ // In-memory cache of network members
+ std::map< uint64_t,std::map< Address,nlohmann::json > > _networkMemberCache;
+ Mutex _networkMemberCache_m;
+
+ // Gathers a bunch of statistics about members of a network, IP assignments, etc. that we need in various places
+ // This does lock _networkMemberCache_m
+ struct _NetworkMemberInfo
+ {
+ _NetworkMemberInfo() : authorizedMemberCount(0),activeMemberCount(0),totalMemberCount(0),mostRecentDeauthTime(0) {}
+ std::set<Address> activeBridges;
+ std::set<InetAddress> allocatedIps;
+ unsigned long authorizedMemberCount;
+ unsigned long activeMemberCount;
+ unsigned long totalMemberCount;
+ uint64_t mostRecentDeauthTime;
+ };
+ void _getNetworkMemberInfo(uint64_t now,uint64_t nwid,_NetworkMemberInfo &nmi);
+
+ // These init objects with default and static/informational fields
+ inline void _initMember(nlohmann::json &member)
+ {
+ if (!member.count("authorized")) member["authorized"] = false;
+ if (!member.count("authHistory")) member["authHistory"] = nlohmann::json::array();
+ if (!member.count("ipAssignments")) member["ipAssignments"] = nlohmann::json::array();
+ if (!member.count("recentLog")) member["recentLog"] = nlohmann::json::array();
+ if (!member.count("activeBridge")) member["activeBridge"] = false;
+ if (!member.count("tags")) member["tags"] = nlohmann::json::array();
+ if (!member.count("capabilities")) member["capabilities"] = nlohmann::json::array();
+ if (!member.count("creationTime")) member["creationTime"] = OSUtils::now();
+ if (!member.count("noAutoAssignIps")) member["noAutoAssignIps"] = false;
+ if (!member.count("revision")) member["revision"] = 0ULL;
+ if (!member.count("enableBroadcast")) member["enableBroadcast"] = true;
+ member["objtype"] = "member";
+ }
+ inline void _initNetwork(nlohmann::json &network)
+ {
+ if (!network.count("private")) network["private"] = true;
+ if (!network.count("creationTime")) network["creationTime"] = OSUtils::now();
+ if (!network.count("name")) network["name"] = "";
+ if (!network.count("multicastLimit")) network["multicastLimit"] = (uint64_t)32;
+ if (!network.count("v4AssignMode")) network["v4AssignMode"] = {{"zt",false}};
+ if (!network.count("v6AssignMode")) network["v6AssignMode"] = {{"rfc4193",false},{"zt",false},{"6plane",false}};
+ if (!network.count("authTokens")) network["authTokens"] = nlohmann::json::array();
+ if (!network.count("capabilities")) network["capabilities"] = nlohmann::json::array();
+ if (!network.count("ipAssignmentPools")) network["ipAssignmentPools"] = nlohmann::json::array();
+ if (!network.count("rules")) {
+ // If unspecified, rules are set to allow anything and behave like a flat L2 segment
+ network["rules"] = {{
+ { "not",false },
+ { "type","ACTION_ACCEPT" }
+ }};
+ }
+ network["objtype"] = "network";
+ }
+ inline void _addNetworkNonPersistedFields(nlohmann::json &network,uint64_t now,const _NetworkMemberInfo &nmi)
+ {
+ network["clock"] = now;
+ network["authorizedMemberCount"] = nmi.authorizedMemberCount;
+ network["activeMemberCount"] = nmi.activeMemberCount;
+ network["totalMemberCount"] = nmi.totalMemberCount;
+ }
+ inline void _addMemberNonPersistedFields(nlohmann::json &member,uint64_t now)
+ {
+ member["clock"] = now;
+ }
+
+ // These are const after construction
+ Node *const _node;
+ std::string _path;
+
+ // Circuit tests outstanding
+ struct _CircuitTestEntry
+ {
+ ZT_CircuitTest *test;
+ std::string jsonResults;
+ };
+ std::map< uint64_t,_CircuitTestEntry > _circuitTests;
+ Mutex _circuitTests_m;
+
+ // Last request time by address, for rate limitation
+ std::map< std::pair<uint64_t,uint64_t>,uint64_t > _lastRequestTime;
+ Mutex _lastRequestTime_m;
+
+ Thread _daemon;
+ volatile bool _daemonRun;
+};
+
+} // namespace ZeroTier
+
+#endif
diff --git a/controller/README.md b/controller/README.md
new file mode 100644
index 00000000..805641d9
--- /dev/null
+++ b/controller/README.md
@@ -0,0 +1,253 @@
+Network Controller Microservice
+======
+
+Every ZeroTier virtual network has a *network controller*. This is our reference implementation and is the same one we use to power our own hosted services at [my.zerotier.com](https://my.zerotier.com/). Network controllers act as configuration servers and certificate authorities for the members of networks. Controllers are located on the network by simply parsing out the first 10 digits of a network's 16-digit network ID: these are the address of the controller.
+
+As of ZeroTier One version 1.2.0 this code is included in normal builds for desktop, laptop, and server (Linux, etc.) targets, allowing any device to create virtual networks without having to be rebuilt from source with special flags to enable this feature. While this does offer a convenient way to create ad-hoc networks or experiment, we recommend running a dedicated controller somewhere secure and stable for any "serious" use case.
+
+Controller data is stored in JSON format under `controller.d` in the ZeroTier working directory. It can be copied, rsync'd, placed in `git`, etc. The files under `controller.d` should not be modified in place while the controller is running or data loss may result, and if they are edited directly take care not to save corrupt JSON since that can also lead to data loss when the controller is restarted. Going through the API is strongly preferred to directly modifying these files.
+
+### Upgrading from Older Versions
+
+Older versions of this code used a SQLite database instead of in-filesystem JSON. A migration utility called `migrate-sqlite` is included here and *must* be used to migrate this data to the new format. If the controller is started with an old `controller.db` in its working directory it will terminate after printing an error to *stderr*. This is done to prevent "surprises" for those running DIY controllers using the old code.
+
+The migration tool is written in nodeJS and can be used like this:
+
+ cd migrate-sqlite
+ npm install
+ node migrate-sqlite.js <path to ZeroTier working directory>
+
+You may need to `sudo node ...` if the ZeroTier working directory is owned by root.
+
+This code will dump the contents of any `controller.db` in the ZeroTier working directory and recreate its data in the form of JSON objects under `controller.d`. The old `controller.db` will then be renamed to `controller.db.migrated` and the controller will start normally.
+
+After migrating make sure that the contents of `controller.d` are owned and writable by the user that will be running the ZeroTier controller process! (Usually this is root but controllers that don't also join networks are sometimes run as unprivileged users.)
+
+If you don't have nodeJS on the machine running ZeroTier it is perfectly fine to just copy `controller.db` to a directory on another machine and run the migration tool there. After running your migration the contents of `controller.d` can be tar'd up and copied back over to the controller. Just remember to rename or remove `controller.db` on the controller machine afterwords so the controller will start.
+
+### Scalability and Reliability
+
+Controllers can in theory host up to 2^24 networks and serve many millions of devices (or more), but we recommend spreading large numbers of networks across many controllers for load balancing and fault tolerance reasons. Since the controller uses the filesystem as its data store we recommend fast filesystems and fast SSD drives for heavily loaded controllers.
+
+Since ZeroTier nodes are mobile and do not need static IPs, implementing high availability fail-over for controllers is easy. Just replicate their working directories from master to backup and have something automatically fire up the backup if the master goes down. Many modern orchestration tools have built-in support for this. It would also be possible in theory to run controllers on a replicated or distributed filesystem, but we haven't tested this yet.
+
+### Dockerizing Controllers
+
+ZeroTier network controllers can easily be run in Docker or other container systems. Since containers do not need to actually join networks, extra privilege options like "--device=/dev/net/tun --privileged" are not needed. You'll just need to map the local JSON API port of the running controller and allow it to access the Internet (over UDP/9993 at a minimum) so things can reach and query it.
+
+### Network Controller API
+
+The controller API is hosted via the same JSON API endpoint that ZeroTier One uses for local control (usually at 127.0.0.1 port 9993). All controller options are routed under the `/controller` base path.
+
+The controller microservice does not implement any fine-grained access control (authentication is via authtoken.secret just like the regular JSON API) or other complex mangement features. It just takes network and network member configurations and reponds to controller queries. We have an enterprise product called [ZeroTier Central](https://my.zerotier.com/) that we host as a service (and that companies can license to self-host) that does this.
+
+All working network IDs on a controller must begin with the controller's ZeroTier address. The API will *allow* "foreign" networks to be added but the controller will have no way of doing anything with them since nobody will know to query it. (In the future we might support secondaries, which would make this relevant.)
+
+The JSON API is *very* sensitive about types. Integers must be integers and strings strings, etc. Incorrectly typed and unrecognized fields may result in ignored fields or a 400 (bad request) error.
+
+#### `/controller`
+
+ * Purpose: Check for controller function and return controller status
+ * Methods: GET
+ * Returns: { object }
+
+| Field | Type | Description | Writable |
+| ------------------ | ----------- | ------------------------------------------------- | -------- |
+| controller | boolean | Always 'true' | no |
+| apiVersion | integer | Controller API version, currently 3 | no |
+| clock | integer | Current clock on controller, ms since epoch | no |
+
+#### `/controller/network`
+
+ * Purpose: List all networks hosted by this controller
+ * Methods: GET
+ * Returns: [ string, ... ]
+
+This returns an array of 16-digit hexadecimal network IDs.
+
+#### `/controller/network/<network ID>`
+
+ * Purpose: Create, configure, and delete hosted networks
+ * Methods: GET, POST, DELETE
+ * Returns: { object }
+
+By making queries to this path you can create, configure, and delete networks. DELETE is final, so don't do it unless you really mean it.
+
+When POSTing new networks take care that their IDs are not in use, otherwise you may overwrite an existing one. To create a new network with a random unused ID, POST to `/controller/network/##########______`. The #'s are the controller's 10-digit ZeroTier address and they're followed by six underscores. Check the `nwid` field of the returned JSON object for your network's newly allocated ID. Subsequent POSTs to this network must refer to its actual path.
+
+| Field | Type | Description | Writable |
+| --------------------- | ------------- | ------------------------------------------------- | -------- |
+| id | string | 16-digit network ID | no |
+| nwid | string | 16-digit network ID (old, but still around) | no |
+| clock | integer | Current clock, ms since epoch | no |
+| name | string | A short name for this network | YES |
+| private | boolean | Is access control enabled? | YES |
+| enableBroadcast | boolean | Ethernet ff:ff:ff:ff:ff:ff allowed? | YES |
+| allowPassiveBridging | boolean | Allow any member to bridge (very experimental) | YES |
+| v4AssignMode | object | IPv4 management and assign options (see below) | YES |
+| v6AssignMode | object | IPv6 management and assign options (see below) | YES |
+| multicastLimit | integer | Maximum recipients for a multicast packet | YES |
+| creationTime | integer | Time network was first created | no |
+| revision | integer | Network config revision counter | no |
+| authorizedMemberCount | integer | Number of authorized members (for private nets) | no |
+| activeMemberCount | integer | Number of members that appear to be online | no |
+| totalMemberCount | integer | Total known members of this network | no |
+| routes | array[object] | Managed IPv4 and IPv6 routes; see below | YES |
+| ipAssignmentPools | array[object] | IP auto-assign ranges; see below | YES |
+| rules | array[object] | Traffic rules; see below | YES |
+
+Recent changes:
+
+ * The `ipLocalRoutes` field appeared in older versions but is no longer present. Routes will now show up in `routes`.
+ * The `relays` field is gone since network preferred relays are gone. This capability is replaced by VL1 level federation ("federated roots").
+
+Other important points:
+
+ * Networks without rules won't carry any traffic. If you don't specify any on network creation an "accept anything" rule set will automatically be added.
+ * Managed IP address assignments and IP assignment pools that do not fall within a route configured in `routes` are ignored and won't be used or sent to members.
+ * The default for `private` is `true` and this is probably what you want. Turning `private` off means *anyone* can join your network with only its 16-digit network ID. It's also impossible to de-authorize a member as these networks don't issue or enforce certificates. Such "party line" networks are used for decentralized app backplanes, gaming, and testing but are otherwise not common.
+
+**Auto-Assign Modes:**
+
+Auto assign modes (`v4AssignMode` and `v6AssignMode`) contain objects that map assignment modes to booleans.
+
+For IPv4 the only valid setting is `zt` which, if true, causes IPv4 addresses to be auto-assigned from `ipAssignmentPools` to members that do not have an IPv4 assignment. Note that active bridges are exempt and will not get auto-assigned IPs since this can interfere with bridging. (You can still manually assign one if you want.)
+
+IPv6 includes this option and two others: `6plane` and `rfc4193`. These assign private IPv6 addresses to each member based on a deterministic assignment scheme that allows members to emulate IPv6 NDP to skip multicast for better performance and scalability. The `rfc4193` mode gives every member a /128 on a /88 network, while `6plane` gives every member a /80 within a /40 network but uses NDP emulation to route *all* IPs under that /80 to its owner. The `6plane` mode is great for use cases like Docker since it allows every member to assign IPv6 addresses within its /80 that just work instantly and globally across the network.
+
+**IP assignment pool object format:**
+
+| Field | Type | Description |
+| --------------------- | ------------- | ------------------------------------------------- |
+| ipRangeStart | string | Starting IP address in range |
+| ipRangeEnd | string | Ending IP address in range (inclusive) |
+
+Pools are only used if auto-assignment is on for the given address type (IPv4 or IPv6) and if the entire range falls within a managed route.
+
+IPv6 ranges work just like IPv4 ranges and look like this:
+
+ {
+ "ipRangeStart": "fd00:feed:feed:beef:0000:0000:0000:0000",
+ "ipRangeEnd": "fd00:feed:feed:beef:ffff:ffff:ffff:ffff"
+ }
+
+(You can POST a shortened-form IPv6 address but the API will always report back un-shortened canonical form addresses.)
+
+That defines a range within network `fd00:feed:feed:beef::/64` that contains up to 2^64 addresses. If an IPv6 range is large enough, the controller will assign addresses by placing each member's device ID into the address in a manner similar to the RFC4193 and 6PLANE modes. Otherwise it will assign addresses at random.
+
+**Rule object format:**
+
+Each rule is actually a sequence of zero or more `MATCH_` entries in the rule array followed by an `ACTION_` entry that describes what to do if all the preceding entries match. An `ACTION_` without any preceding `MATCH_` entries is always taken, so setting a single `ACTION_ACCEPT` rule yields a network that allows all traffic. If no rules are present the default action is `ACTION_DROP`.
+
+Rules are evaluated in the order in which they appear in the array. There is currently a limit of 256 entries per network. Capabilities should be used if a larger and more complex rule set is needed since they allow rules to be grouped by purpose and only shipped to members that need them.
+
+Each rule table entry has two common fields.
+
+| Field | Type | Description |
+| --------------------- | ------------- | ------------------------------------------------- |
+| type | string | Entry type (all caps, case sensitive) |
+| not | boolean | If true, MATCHes match if they don't match |
+
+The following fields may or may not be present depending on rule type:
+
+| Field | Type | Description |
+| --------------------- | ------------- | ------------------------------------------------- |
+| zt | string | 10-digit hex ZeroTier address |
+| etherType | integer | Ethernet frame type |
+| mac | string | Hex MAC address (with or without :'s) |
+| ip | string | IPv4 or IPv6 address |
+| ipTos | integer | IP type of service |
+| ipProtocol | integer | IP protocol (e.g. TCP) |
+| start | integer | Start of an integer range (e.g. port range) |
+| end | integer | End of an integer range (inclusive) |
+| id | integer | Tag ID |
+| value | integer | Tag value or comparison value |
+| mask | integer | Bit mask (for characteristics flags) |
+
+The entry types and their additional fields are:
+
+| Entry type | Description | Fields |
+| ------------------------------- | ----------------------------------------------------------------- | -------------- |
+| `ACTION_DROP` | Drop any packets matching this rule | (none) |
+| `ACTION_ACCEPT` | Accept any packets matching this rule | (none) |
+| `ACTION_TEE` | Send a copy of this packet to a node (rule parsing continues) | `zt` |
+| `ACTION_REDIRECT` | Redirect this packet to another node | `zt` |
+| `ACTION_DEBUG_LOG` | Output debug info on match (if built with rules engine debug) | (none) |
+| `MATCH_SOURCE_ZEROTIER_ADDRESS` | Match VL1 ZeroTier address of packet sender. | `zt` |
+| `MATCH_DEST_ZEROTIER_ADDRESS` | Match VL1 ZeroTier address of recipient | `zt` |
+| `MATCH_ETHERTYPE` | Match Ethernet frame type | `etherType` |
+| `MATCH_MAC_SOURCE` | Match source Ethernet MAC address | `mac` |
+| `MATCH_MAC_DEST` | Match destination Ethernet MAC address | `mac` |
+| `MATCH_IPV4_SOURCE` | Match source IPv4 address | `ip` |
+| `MATCH_IPV4_DEST` | Match destination IPv4 address | `ip` |
+| `MATCH_IPV6_SOURCE` | Match source IPv6 address | `ip` |
+| `MATCH_IPV6_DEST` | Match destination IPv6 address | `ip` |
+| `MATCH_IP_TOS` | Match IP TOS field | `ipTos` |
+| `MATCH_IP_PROTOCOL` | Match IP protocol field | `ipProtocol` |
+| `MATCH_IP_SOURCE_PORT_RANGE` | Match a source IP port range | `start`,`end` |
+| `MATCH_IP_DEST_PORT_RANGE` | Match a destination IP port range | `start`,`end` |
+| `MATCH_CHARACTERISTICS` | Match on characteristics flags | `mask`,`value` |
+| `MATCH_FRAME_SIZE_RANGE` | Match a range of Ethernet frame sizes | `start`,`end` |
+| `MATCH_TAGS_SAMENESS` | Match if both sides' tags differ by no more than value | `id`,`value` |
+| `MATCH_TAGS_BITWISE_AND` | Match if both sides' tags AND to value | `id`,`value` |
+| `MATCH_TAGS_BITWISE_OR` | Match if both sides' tags OR to value | `id`,`value` |
+| `MATCH_TAGS_BITWISE_XOR` | Match if both sides` tags XOR to value | `id`,`value` |
+
+Important notes about rules engine behavior:
+
+ * IPv4 and IPv6 IP address rules do not match for frames that are not IPv4 or IPv6 respectively.
+ * `ACTION_DEBUG_LOG` is a no-op on nodes not built with `ZT_RULES_ENGINE_DEBUGGING` enabled (see Network.cpp). If that is enabled nodes will dump a trace of rule evaluation results to *stdout* when this action is encountered but will otherwise keep evaluating rules. This is used for basic "smoke testing" of the rules engine.
+ * Multicast packets and packets destined for bridged devices treated a little differently. They are matched more than once. They are matched at the point of send with a NULL ZeroTier destination address, meaning that `MATCH_DEST_ZEROTIER_ADDRESS` is useless. That's because the true VL1 destination is not yet known. Then they are matched again for each true VL1 destination. On these later subsequent matches TEE actions are ignored and REDIRECT rules are interpreted as DROPs. This prevents multiple TEE or REDIRECT packets from being sent to third party devices.
+ * Rules in capabilities are always matched as if the current device is the sender (inbound == false). A capability specifies sender side rules that can be enforced on both sides.
+
+#### `/controller/network/<network ID>/member`
+
+ * Purpose: Get a set of all members on this network
+ * Methods: GET
+ * Returns: { object }
+
+This returns a JSON object containing all member IDs as keys and their `memberRevisionCounter` values as values.
+
+#### `/controller/network/<network ID>/active`
+
+ * Purpose: Get a set of all active members on this network
+ * Methods: GET
+ * Returns: { object }
+
+This returns an object containing all currently online members and the most recent `recentLog` entries for their last request.
+
+#### `/controller/network/<network ID>/member/<address>`
+
+ * Purpose: Create, authorize, or remove a network member
+ * Methods: GET, POST, DELETE
+ * Returns: { object }
+
+| Field | Type | Description | Writable |
+| --------------------- | ------------- | ------------------------------------------------- | -------- |
+| id | string | Member's 10-digit ZeroTier address | no |
+| address | string | Member's 10-digit ZeroTier address | no |
+| nwid | string | 16-digit network ID | no |
+| clock | integer | Current clock, ms since epoch | no |
+| authorized | boolean | Is member authorized? (for private networks) | YES |
+| authHistory | array[object] | History of auth changes, latest at end | no |
+| activeBridge | boolean | Member is able to bridge to other Ethernet nets | YES |
+| identity | string | Member's public ZeroTier identity (if known) | no |
+| ipAssignments | array[string] | Managed IP address assignments | YES |
+| memberRevision | integer | Member revision counter | no |
+| recentLog | array[object] | Recent member activity log; see below | no |
+
+Note that managed IP assignments are only used if they fall within a managed route. Otherwise they are ignored.
+
+**Recent log object format:**
+
+| Field | Type | Description |
+| --------------------- | ------------- | ------------------------------------------------- |
+| ts | integer | Time of request, ms since epoch |
+| authorized | boolean | Was member authorized? |
+| clientMajorVersion | integer | Client major version or -1 if unknown |
+| clientMinorVersion | integer | Client minor version or -1 if unknown |
+| clientRevision | integer | Client revision or -1 if unknown |
+| clientProtocolVersion | integer | ZeroTier protocol version reported by client |
+| fromAddr | string | Physical address if known |
+
+The controller can only know a member's `fromAddr` if it's able to establish a direct path to it. Members behind very restrictive firewalls may not have this information since the controller will be receiving the member's requests by way of a relay. ZeroTier does not back-trace IP paths as packets are relayed since this would add a lot of protocol overhead.