summaryrefslogtreecommitdiff
path: root/netconf-service
diff options
context:
space:
mode:
authorAdam Ierymenko <adam.ierymenko@gmail.com>2014-05-07 02:45:15 +0000
committerAdam Ierymenko <adam.ierymenko@gmail.com>2014-05-07 02:45:15 +0000
commitd5f95b721de4eda1173fa91b91bb26a03202b46a (patch)
treec2ff2840d15a55691cbab52dd0a95e2f60e1bcf0 /netconf-service
parentae2eeff5c6bf759eec17a4608318dd499e222b38 (diff)
downloadinfinitytier-d5f95b721de4eda1173fa91b91bb26a03202b46a.tar.gz
infinitytier-d5f95b721de4eda1173fa91b91bb26a03202b46a.zip
Yet more work on netconf master, add redis docs.
Diffstat (limited to 'netconf-service')
-rw-r--r--netconf-service/README.md33
-rw-r--r--netconf-service/index.js259
-rw-r--r--netconf-service/redis-schema.md100
3 files changed, 365 insertions, 27 deletions
diff --git a/netconf-service/README.md b/netconf-service/README.md
new file mode 100644
index 00000000..8f4abfa2
--- /dev/null
+++ b/netconf-service/README.md
@@ -0,0 +1,33 @@
+# ZeroTier One Network Configuration Service Helper
+
+This code will at the moment probably not be of much interest to end users.
+It's a "service," which is a sub-process that ZeroTier One can execute that
+provides additional functionaly not found in the core executable.
+
+ZeroTier One communicates with services by sending and receiving string-serialized
+Dictionary objects terminated by an empty line for each Dictionary.
+
+The netconf service, written in node.js and making use of the zerotier-idtool
+binary (see index.js) handles responding to virtual network config requests.
+It only runs on netconf masters. It fetches its configuration information from
+a Redis database.
+
+The schama for that database is in redis-schema.txt. Some record types such
+as users will not be of interest to outside people. Pay attention to the
+network record type.
+
+To install, copy the netconf-service folder to /var/lib/zerotier-one/services.d
+(create services.d first). Then run "npm install" from the netconf-service
+folder there and finally copy netconf.service to the parent services.d folder.
+Make sure that shell script is executable. This is what the ZeroTier One
+service will execute. It in turn sets up the node environment and runs index.js.
+
+The utility zerotier-idtool must be present as /usr/local/bin/zerotier-idtool. This
+is hard coded for the moment. To get it build ZeroTier One from source and then
+"sudo cp zerotier-one /usr/local/bin/zerotier-idtool" (the binary determines its
+personality from argv[0] on execution).
+
+Note: Windows builds of ZeroTier One do not support services. This code has only
+been tested on Linux but will probably work on Mac too.
+
+*Adam Ierymenko @ ZeroTier*
diff --git a/netconf-service/index.js b/netconf-service/index.js
index 5af2aa16..d76b4c6a 100644
--- a/netconf-service/index.js
+++ b/netconf-service/index.js
@@ -45,7 +45,7 @@ var ZT_NETWORKCONFIG_DICT_KEY_IPV6_STATIC = "v6s";
var ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_RATES = "mr";
var ZT_NETWORKCONFIG_DICT_KEY_CERTIFICATE_OF_MEMBERSHIP = "com";
-// Path to zerotier-idtool
+// Path to zerotier-idtool binary, invoked to enerate certificates of membership
var ZEROTIER_IDTOOL = '/usr/local/bin/zerotier-idtool';
// Connect to redis, assuming database 0 and no auth (for now)
@@ -55,7 +55,17 @@ DB.on("error",function(err) {
console.error('redis query error: '+err);
});
-// Encoding-compatible with Dictionary.hpp in ZeroTier One C++ code base
+// Global variables -- these are initialized on startup or netconf-init message
+var netconfSigningIdentity = null; // identity of netconf master, with private key portion
+
+function ztDbTrue(v) { return ((v === '1')||(v === 'true')||(v > 0)); }
+function csvToArray(csv) { return (((typeof csv === 'string')&&(csv.length > 0)) ? csv.split(',') : []); }
+function arrayToCsv(a) { return ((Array.isArray(a)) ? ((a.length > 0) ? a.join(',') : '') : (((a !== null)&&(typeof a !== 'undefined')) ? a.toString() : '')); }
+
+//
+// ZeroTier One Dictionary -- encoding-compatible with Dictionary in C++ code base
+//
+
function Dictionary(fromStr)
{
var thiz = this;
@@ -150,22 +160,6 @@ function Dictionary(fromStr)
thiz.fromString(fromStr);
};
-/* Dictionary tester
-var testDict1 = new Dictionary();
-var testDict2 = new Dictionary();
-testDict1.data['foo'] = '1';
-testDict1.data['bar'] = 'The quick brown fox\ncontained a carriage return.';
-testDict2.data['embeddedDictionary'] = testDict1.toString();
-testDict2.data['baz'] = 'eklrjelkrnlqkejrnlkqerne';
-console.log(testDict2.toString());
-console.log('After fromString(toString())...\n');
-console.log((new Dictionary(testDict2.toString())).toString());
-process.exit(0);
-*/
-
-// Variables initialized by netconf-init message
-var netconfSigningIdentity = null;
-
//
// Identity implementation using zerotier-idtool as subprocess to do actual crypto work
//
@@ -181,6 +175,10 @@ function Identity(idstr)
return thiz.str;
};
+ this.address = function() {
+ return ((thiz.fields.length > 0) ? thiz.fields[0] : '0000000000');
+ };
+
this.fromString = function(str) {
thiz.str = '';
thiz.fields = [];
@@ -202,7 +200,7 @@ function Identity(idstr)
return false;
};
- this.hasSecretKey = function() {
+ this.hasPrivate = function() {
return ((thiz.isValid())&&(thiz.fields.length >= 4));
};
@@ -217,17 +215,26 @@ function Identity(idstr)
function handleMessage(dictStr)
{
var message = new Dictionary(dictStr);
- var response = new Dictionary();
- if (!('type' in message.data))
- return; // no request type
+ if (!('type' in message.data)) {
+ console.error('ignored message without request type field');
+ return;
+ }
if (message.data['type'] === 'netconf-init') {
+
netconfSigningIdentity = new Identity(message.data['netconfId']);
- if (!netconfSigningIdentity.isValid())
- netconfSigningIdentity = null; // empty strings and such are not valid
- return; // no response expected
+ if (!netconfSigningIdentity.hasPrivate()) {
+ netconfSigningIdentity = null;
+ console.error('got invalid netconf signing identity');
+ }
+
} else if (message.data['type'] === 'netconf-request') {
+ if ((!netconfSigningIdentity)||(!netconfSigningIdentity.hasPrivate())) {
+ console.error('got netconf-request before netconf-init, ignored');
+ return;
+ }
+
// Get required fields
var peerId = new Identity(message.data['peerId']);
var fromIpAndPort = message.data['from'];
@@ -238,9 +245,207 @@ function handleMessage(dictStr)
// Get optional fields
var meta = new Dictionary(message.data['meta']);
- } else return;
+ var clientVersion = message.data['clientVersion'];
+ var clientOs = message.data['clientOs'];
+
+ var network = null;
+ var member = null;
+ var authorized = false;
+ var v4NeedAssign = false;
+ var v6NeedAssign = false;
+ var v4Assignments = [];
+ var v6Assignments = [];
+
+ async.series([function(next) { // network lookup
+ DB.hgetall('zt1:network:'+nwid+':~',function(err,obj) {
+ network = obj;
+ return next(err);
+ });
+ },function(next) { // member record lookup, unless public network
+ if ((!network)||(!('nwid' in network)||(network['nwid'] !== nwid))
+ return next(null);
+ var memberKey = 'zt1:network:'+nwid+':member:'+peerId.address()+':~';
+ DB.hgetall(memberKey,function(err,obj) {
+ if (err)
+ return next(err);
+ else if (obj) {
+ // Update member object
+ member = obj;
+ authorized = (ztDbTrue(network['private']) || ztDbTrue(member['authorized']));
+ DB.hmset(memberKey,{
+ 'lastSeen': Date.now(),
+ 'lastAt': fromIpAndPort,
+ 'clientVersion': (clientVersion) ? clientVersion : '?.?.?',
+ 'clientOs': (clientOs) ? clientOs : '?'
+ },next);
+ } else {
+ // Add member object for new and unauthorized member
+ authorized = false;
+ member = {
+ 'id': peerId.address(),
+ 'nwid': nwid,
+ 'authorized': 0,
+ 'identity': peerId.toString(),
+ 'firstSeen': Date.now(),
+ 'lastSeen': Date.now(),
+ 'lastAt': fromIpAndPort,
+ 'clientVersion': (clientVersion) ? clientVersion : '?.?.?',
+ 'clientOs': (clientOs) ? clientOs : '?'
+ };
+ DB.hmset(memberKey,member,next);
+ }
+ });
+ },function(next) { // IP address auto-assignment, if needed
+ if (!authorized)
+ return next(null);
+
+ v4NeedAssign = (network['v4AssignMode'] === 'zt');
+ v6NeedAssign = (network['v6AssignMode'] === 'zt');
+
+ var ipa = csvToArray(member['ipAssignments']);
+ for(var i=0;i<ipa.length;++i) {
+ if ((ipa[i].indexOf('.') > 0)&&(v4NeedAssign))
+ v4Assignments.push(ipa[i]);
+ else if ((ipa[i].indexOf(':') > 0)&&(v6NeedAssign))
+ v6Assignments.push(ipa[i]);
+ }
- process.stdout.write(response.toString()+'\n');
+ return next(null);
+ },function(next) { // assign IPv4 if needed
+ if ((!authorized)||(!v4NeedAssign))
+ return next(null);
+
+ var ipAssignmentAttempts = 0; // for sanity-checking
+ var v4pool = network['v4AssignPool'];
+ var ztaddr = peerId.address();
+
+ var network = 0;
+ var netmask = 0;
+ var netmaskBits = 0;
+ if (v4pool) {
+ var v4poolSplit = v4Pool.split('/');
+ if (v4poolSplit.length === 2) {
+ var networkSplit = v4poolSplit[0].split('.');
+ if (networkSplit.length === 4) {
+ network |= (parseInt(networkSplit[0],10) << 24) & 0xff000000;
+ network |= (parseInt(networkSplit[1],10) << 16) & 0x00ff0000;
+ network |= (parseInt(networkSplit[2],10) << 8) & 0x0000ff00;
+ network |= parseInt(networkSplit[3],10) & 0x000000ff;
+ netmaskBits = parseInt(v4poolSplit[1],10);
+ if (netmaskBits > 32)
+ netmaskBits = 32; // sanity check
+ for(var i=0;i<netmaskBits;++i)
+ netmask |= (0x80000000 >> i);
+ }
+ }
+ }
+ var invmask = netmask ^ 0xffffffff;
+ var abcd = 0;
+ var assignment = null;
+
+ var ipAssignmentsKey = 'zt1:network:'+nwid+':ipAssignments';
+ var memberKey = 'zt1:network:'+nwid+':member:'+ztaddr+':~';
+
+ async.whilst(
+ function() { return ((v4NeedAssign)&&(v4Assignments.length === 0)&&(network !== 0)&&(netmask !== 0xffffffff)&&(ipAssignmentAttempts < 1000)); },
+ function(next2) {
+ ++ipAssignmentAttempts;
+
+ // Generate or increment IP address
+ if (abcd === 0) {
+ var a = parseInt(ztaddr.substr(2,2),16) & 0xff;
+ var b = parseInt(ztaddr.substr(4,2),16) & 0xff;
+ var c = parseInt(ztaddr.substr(6,2),16) & 0xff;
+ var d = parseInt(ztaddr.substr(8,2),16) & 0xff;
+ abcd = (a << 24) | (b << 16) | (c << 8) | d;
+ } else ++abcd;
+ if ((abcd & 0xff) === 0)
+ abcd |= 1;
+
+ // Derive an IP to test and generate assignment ip/bits string
+ var ip = (abcd & invmask) | (network & netmask);
+ assignment = ((ip >> 24) & 0xff).toString(10) + '.' + ((ip >> 16) & 0xff).toString(10) + '.' + ((ip >> 8) & 0xff).toString(10) + '.' + (ip & 0xff).toString(10) + '/' + netmaskBits.toString(10);
+
+ DB.hget(ipAssignmentsKey,assignment,function(err,value) {
+ if (err)
+ return next2(err);
+ if ((value)&&(value !== ztaddr))
+ return next2(null); // if someone's already got this IP, keep looking
+
+ v4Assignments.push(assignment);
+
+ // Save assignment to :ipAssignments hash
+ DB.hset(ipAssignmentsKey,assignment,ztaddr,function(err) {
+ if (err)
+ return next2(err);
+
+ // Save updated CSV list of assignments to member record
+ var ipAssignments = member['ipAssignments'];
+ if (!ipAssignments)
+ ipAssignments = '';
+ if (ipAssignments.length > 0)
+ ipAssignments += ',';
+ ipAssignments += assignment;
+ member['ipAssignments'] = ipAssignments;
+ DB.hset(memberKey,'ipAssignments',ipAssignments,next2);
+ });
+ });
+ },
+ next
+ );
+
+ },function(next) { // assign IPv6 if needed -- TODO
+ if ((!authorized)||(!v6NeedAssign))
+ return next(null);
+
+ return next(null);
+ }],function(err) {
+ if (err) {
+ console.log('error composing response for '+peerId.address()+': '+err);
+ return;
+ } else if (authorized) {
+ // TODO: COM!!!
+ var certificateOfMembership = null;
+
+ var netconf = new Dictionary();
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_NETCONF_SERVICE_VERSION] = '0.0.0';
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_ALLOWED_ETHERNET_TYPES] = network['etherTypes'];
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_NETWORK_ID] = nwid;
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_TIMESTAMP] = Date.now().toString();
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_ISSUED_TO] = peerId.address();
+ //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_PREFIX_BITS] = 0;
+ //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_DEPTH] = 0;
+ //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_MULTICAST_RATES] = '';
+ //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_ARP_CACHE_TTL] = 0;
+ //netconf.data[ZT_NETWORKCONFIG_DICT_KEY_NDP_CACHE_TTL] = 0;
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_EMULATE_ARP] = '0';
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_EMULATE_NDP] = '0';
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_IS_OPEN] = ztDbTrue(network['private']) ? '0' : '1';
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_NAME] = network['name'];
+ if (network['desc'])
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_DESC] = network['desc'];
+ if (v4NeedAssign)
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_IPV4_STATIC] = (v4Assignments.length > 0) ? v4Assignments.join(',') : '';
+ if (v6NeedAssign)
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_IPV6_STATIC] = (v6Assignments.length > 0) ? v6Assignments.join(',') : '';
+ if (certificateOfMembership !== null)
+ netconf.data[ZT_NETWORKCONFIG_DICT_KEY_CERTIFICATE_OF_MEMBERSHIP] = certificateOfMembership;
+
+ var response = new Dictionary();
+ response.data['peer'] = peerId.address();
+ response.data['nwid'] = nwid;
+ response.data['type'] = 'netconf-response';
+ response.data['requestId'] = requestId;
+ response.data['netconf'] = netconf.toString();
+
+ process.stdout.write(response.toString()+'\n');
+ return;
+ } else {
+ }
+ });
+ } else {
+ console.error('ignored unrecognized message type: '+message.data['type']);
+ }
};
//
diff --git a/netconf-service/redis-schema.md b/netconf-service/redis-schema.md
new file mode 100644
index 00000000..58593d3c
--- /dev/null
+++ b/netconf-service/redis-schema.md
@@ -0,0 +1,100 @@
+# ZeroTier One Redis Database Schema
+
+## Notes
+
+- Top-level key format is zt1:top-level-record-type:... using :'s as a separator as per de-facto Redis standard.
+- Each top-level record type has a :~ child containing a hash. This is its root "document" and stores anything not requiring a special Redis data structure.
+- Right now there are no SET/ZSET fields to optimize searches for sub-keys such as :\<nwid\>:member:*. The code uses the Redis KEYS command for this, which is not efficient for (very) large databases. This will need to be refactored to use sets if ZT1 is wildly successful since key-globbing will slow things down with hundreds of thousands of keys. At current scales it's trivial so make it work first then make it fast.
+- ! before hash field names denotes required fields for a record to be valid.
+- M before hash field names denotes fields that are user-editable via the web API. (M for malleable/mutable.)
+- R before hash field names denotes read-only fields that are computed or can only be changed by special operations.
+- H before hash field names indicates fields that are not returned by web API queries. These are also read-only and are for internal use only.
+- @ before hash field names denotes fields that are presented by the web API as if they're part of the regular hash but are actually stored in sub-keys as SETs or other Redis data types.
+- Booleans: any value other than "1" or "true" is false, including a missing field.
+- Timestamps are in milliseconds since the epoch and are stored as base-10 integers.
+- IPv6 addresses must be stored *without* shortening, e.g. \:0000\: must be used instead of \:\:. It must be possible to strip colons and convert directly from hex to a 128-bit address.
+- All hexadecimal values must use lower-case letters a-f.
+- 16-digit hex network IDs and 10-digit hex addresses are zero-padded to 16 and 10 digits respectively (as they are everywhere else in the ZT1 universe).
+
+## Base Configuration
+
+### zt1 (value: 1)
+
+If this key is not present with a value of 1, the API server code will auto-init the DB with initial data containing stuff like the Earth network and default users. This is not used by netconf-service.
+
+## Users (ZeroTier Networks only)
+
+This record type is only of interest to ZeroTier Networks itself. It holds user records, billing information, subscriptions, etc. Netconf masters do not use these records so you don't need to worry about this if you are trying to run your own.
+
+### zt1:user:\<auth\>:\<authUserId\>:~
+
+Note: users are referred to elsewhere in the database by their compound key \<auth\>:\<authUserId\> as stored here in the id field.
+
+- !R id :: must be auth:authUserId
+- !R auth :: authentication type e.g. 'google' or 'local'
+- !R authUserId :: user ID under auth schema, like an e-mail address or a Google profile ID.
+- M email :: user's email address
+- R confirmed :: is e-mail confirmed?
+- R lastLogin :: timestamp of last login
+- R creationTime: :: timestamp of account creation
+- M displayName :: usually First Last, defaults to e-mail address for 'local' auth and whatever the OpenID API says for third party auth such as Google.
+- R stripeCustomerId :: customer ID for Stripe credit card service if the user has cards on file (we don't store cards, we let Stripe do that)
+- R referrals :: stringified JSON field containing referrals, coupons, etc.
+
+## Networks
+
+Network records are used by the netconf master to issue network configuration information to peers on the ZT1 network itself. Here is where you should look if you want to play with running your own!
+
+### zt1:network:\<nwid\>:~
+
+Each network has a network record indexed by its 64-bit network ID in lower-case hexadecimal.
+
+- !R id :: must be \<nwid\>
+- !M name :: network's globally unique short name, which can contain only characters valid in an e-mail address. It's the job of the code that populates this DB to ensure that this is globally unique.
+- R owner :: id of user who owns this network (not used by netconf master, only for web UI and web API)
+- R billingUser :: user paying for premium subscriptions (also unused by netconf-master)
+- R billingUserConfirmed :: if true, billingUser has confirmed and authorized billing
+- M desc :: a longer network description
+- R infrastructure :: if true, network can't be deleted through API or web UI
+- M private :: if true, network requires authentication
+- R creationTime :: timestamp of network creation
+- M etherTypes :: comma-delimited list of integers indicating Ethernet types permitted on network
+- M enableBroadcast :: if true, ff:ff:ff:ff:ff:ff is enabled network-wide
+- M v4AssignMode :: 'none' (or null/empty/etc.), 'zt', 'dhcp'
+- M v4AssignPool :: network/bits from which to assign IPs
+- M v6AssignMode :: 'none' (or null/empty/etc.), 'zt', 'v6native', 'dhcp6'
+- M v6AssignPool :: network/bits from which to assign IPs
+- M ui :: string-serialized JSON blob for use by the user interface, ignored by netconf-master
+
+### zt1:network:\<nwid\>:member:\<address\>:~
+
+For private networks, each member of the network must have a record that indicates whether it is allowed to communicate. The address is the 10-digit lower-case hexadecimal ZeroTier address.
+
+The netconf-master will automatically add any peer that even attempts to request a netconf / certificate. These are added with authorized set to false. The hideInSlushpile field is used in the UI to allow network admins to hide unknown/bogus join attempts that they don't want to keep seeing.
+
+- !R id :: must be \<address\>
+- !R nwid :: must be \<nwid\>
+- M authorized :: true if node is authorized and will be issued valid certificates and network configurations
+- M bridge :: true if node is an active bridge that should receive all multicast traffic for this net (e.g. a physical-virtual bridge extending a physical network)
+- M name :: name of system
+- M notes :: annotation field
+- R authorizedBy :: user ID of user who authorized membership
+- R authorizedAt :: timestamp of authorization
+- R identity :: string-serialized full public node identity as last seen by netconf-master
+- R firstSeen :: timestamp node first tried to authorize to this net
+- R lastSeen :: timestamp node last tried to authorize to this net
+- R lastAt :: real network address from which node was last seen
+- R clientVersion :: software version last seen by netconf-master
+- R clientOs :: operating system last seen by netconf-master
+- R ipAssignments :: comma-delimited list of IP address assignments (see below)
+- M ui :: string-serialized JSON blob for use by the user interface, ignored by netconf-master
+
+The name, notes, authorizedBy, and authorizedAt fields are only for use by the administration UI. The netconf-master does not care about this. The identity, firstSeen, lastSeen, lastAt, version, and os fields are populated by netconf-master.
+
+The ipAssignments field is re-generated whenever the zt1:network:\<nwid\>:ipAssignments hash is modified for this member. Both the API code and the netconf-master code must keep this in sync. This field is read-only for API users; the ipAssignments part of the API must be used to modify member IP address assignments.
+
+### zt1:network:\<nwid\>:ipAssignments
+
+This is a hash mapping IP/netmask bits fields to 10-digit ZeroTier addresses of network members. IPv4 fields contain dots, e.g. "10.2.3.4/24" or "29.1.1.1/7". IPv6 fields contain colons. Note that IPv6 IP abbreviations should *not* be used; use \:0000\: instead of \:\: for zero parts of addresses. This is to simplify parser code and canonicalize for rapid search. All hex digits must be lower-case.
+
+This is only used if the network's IPv4 and/or IPv6 auto-assign mode is 'zt' for ZeroTier assignment. The netconf-master will auto-populate by choosing unused IPs, and it can be edited via the API as well.