diff options
Diffstat (limited to 'cluster-geo')
-rw-r--r-- | cluster-geo/README.md | 14 | ||||
-rwxr-xr-x | cluster-geo/cluster-geo.exe | 13 | ||||
-rw-r--r-- | cluster-geo/cluster-geo/cluster-geo.js | 102 | ||||
-rw-r--r-- | cluster-geo/cluster-geo/config.js.sample | 7 | ||||
-rw-r--r-- | cluster-geo/cluster-geo/package.json | 16 |
5 files changed, 152 insertions, 0 deletions
diff --git a/cluster-geo/README.md b/cluster-geo/README.md new file mode 100644 index 00000000..492ffc0d --- /dev/null +++ b/cluster-geo/README.md @@ -0,0 +1,14 @@ +Cluster GeoIP Service +====== + +In cluster mode (build with ZT\_ENABLE\_CLUSTER and install a cluster definition file), ZeroTier One can use geographic IP lookup to steer clients toward members of a cluster that are physically closer and are therefore very likely to offer lower latency and better performance. Ordinary non-clustered ZeroTier endpoints will have no use for this code. + +If a cluster-mode instance detects a file in the ZeroTier home folder called *cluster-geo.exe*, it attempts to execute it. If this program runs, it receives IP addresses on STDIN and produces lines of CSV on STDOUT with the following format: + + IP,result code,latitude,longitude,x,y,z + +The first field is the IP echoed back. The second field is 0 if the result is pending and may be ready in the future or 1 if the result is ready now. If the second field is 0 the remaining fields should be 0. Otherwise the remaining fields contain the IP's latitude, longitude, and X/Y/Z coordinates. + +ZeroTier's cluster route optimization code only uses the X/Y/Z values. These are computed by this cluster-geo code as the spherical coordinates of the IP address using the Earth's center as the point of origin and using an approximation of the Earth as a sphere. This doesn't yield *exact* coordinates, but it's good enough for our purposes since the goal is to route clients to the geographically closest endpoint. + +To install, copy *cluster-geo.exe* and the *cluster-geo/* subfolder into the ZeroTier home. Then go into *cluster-geo/* and run *npm install* to install the project's dependencies. A recent (4.x or newer) version of NodeJS is recommended. You will also need a [MaxMind GeoIP2 Precision Services](https://www.maxmind.com/) license key. The *MaxMind GeoIP2 City* tier is required since this supplies actual coordinates. It's a commercial service but is very inexpensive and offers very good accuracy for both IPv4 and IPv6 addresses. The *cluster-geo.js* program caches results in a LevelDB database for up to 120 days to reduce GeoIP API queries. diff --git a/cluster-geo/cluster-geo.exe b/cluster-geo/cluster-geo.exe new file mode 100755 index 00000000..56b76e0d --- /dev/null +++ b/cluster-geo/cluster-geo.exe @@ -0,0 +1,13 @@ +#!/bin/bash + +export PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin + +cd `dirname $0` +if [ ! -d cluster-geo -o ! -f cluster-geo/cluster-geo.js ]; then + echo 'Cannot find ./cluster-geo containing NodeJS script files.' + exit 1 +fi + +cd cluster-geo + +exec node --harmony cluster-geo.js diff --git a/cluster-geo/cluster-geo/cluster-geo.js b/cluster-geo/cluster-geo/cluster-geo.js new file mode 100644 index 00000000..3cbc60be --- /dev/null +++ b/cluster-geo/cluster-geo/cluster-geo.js @@ -0,0 +1,102 @@ +"use strict"; + +// +// GeoIP lookup service +// + +// GeoIP cache TTL in ms +var CACHE_TTL = (60 * 60 * 24 * 120 * 1000); // 120 days + +// Globally increase event emitter maximum listeners +//var EventEmitter = require('events'); +//EventEmitter.prototype._maxListeners = 1000; +//process.setMaxListeners(1000); + +// Load config +var config = require(__dirname + '/config.js'); + +if (!config.maxmind) { + console.error('FATAL: only MaxMind GeoIP2 is currently supported and is not configured in config.js'); + process.exit(1); +} +var geo = require('geoip2ws')(config.maxmind); + +var cache = require('levelup')(__dirname + '/cache.leveldb'); + +function lookup(ip,callback) +{ + cache.get(ip,function(err,cachedEntryJson) { + if ((!err)&&(cachedEntryJson)) { + try { + let cachedEntry = JSON.parse(cachedEntryJson.toString()); + if (cachedEntry) { + let ts = cachedEntry.ts; + let r = cachedEntry.r; + if ((ts)&&(r)) { + if ((Date.now() - ts) < CACHE_TTL) { + r._cached = true; + return callback(null,r); + } + } + } + } catch (e) {} + } + + geo(ip,function(err,result) { + if (err) + return callback(err,null); + if ((!result)||(!result.location)) + return callback(new Error('null result'),null); + + cache.put(ip,JSON.stringify({ + ts: Date.now(), + r: result + }),function(err) { + if (err) + console.error('Error saving to cache: '+err); + return callback(null,result); + }); + }); + }); +}; + +var linebuf = ''; +process.stdin.on('readable',function() { + var chunk; + while (null !== (chunk = process.stdin.read())) { + for(var i=0;i<chunk.length;++i) { + let c = chunk[i]; + if ((c == 0x0d)||(c == 0x0a)) { + if (linebuf.length > 0) { + let ip = linebuf; + lookup(ip,function(err,result) { + if ((err)||(!result)||(!result.location)) { + return process.stdout.write(ip+',0,0,0,0,0,0\n'); + } else { + let lat = parseFloat(result.location.latitude); + let lon = parseFloat(result.location.longitude); + + // Convert to X,Y,Z coordinates from Earth's origin, Earth-as-sphere approximation. + let latRadians = lat * 0.01745329251994; // PI / 180 + let lonRadians = lon * 0.01745329251994; // PI / 180 + let cosLat = Math.cos(latRadians); + let x = Math.round((-6371.0) * cosLat * Math.cos(lonRadians)); // 6371 == Earth's approximate radius in kilometers + let y = Math.round(6371.0 * Math.sin(latRadians)); + let z = Math.round(6371.0 * cosLat * Math.sin(lonRadians)); + + return process.stdout.write(ip+',1,'+lat+','+lon+','+x+','+y+','+z+'\n'); + } + }); + } + linebuf = ''; + } else { + linebuf += String.fromCharCode(c); + } + } + } +}); + +process.stdin.on('end',function() { + cache.close(); + process.exit(0); +}); diff --git a/cluster-geo/cluster-geo/config.js.sample b/cluster-geo/cluster-geo/config.js.sample new file mode 100644 index 00000000..ec1ebfea --- /dev/null +++ b/cluster-geo/cluster-geo/config.js.sample @@ -0,0 +1,7 @@ +// MaxMind GeoIP2 config +module.exports.maxmind = { + userId: 1234, + licenseKey: 'asdf', + service: 'city', + requestTimeout: 1000 +}; diff --git a/cluster-geo/cluster-geo/package.json b/cluster-geo/cluster-geo/package.json new file mode 100644 index 00000000..53fbc5f4 --- /dev/null +++ b/cluster-geo/cluster-geo/package.json @@ -0,0 +1,16 @@ +{ + "name": "cluster-geo", + "version": "1.0.0", + "description": "Cluster GEO-IP Query Service", + "main": "cluster-geo.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "ZeroTier, Inc.", + "license": "GPL-3.0", + "dependencies": { + "geoip2ws": "^1.7.1", + "leveldown": "^1.4.2", + "levelup": "^1.3.0" + } +} |